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. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "account"
version = "0.2.11"
dependencies = [
"anyhow",
"gpui",
"log",
"nostr",
"nostr-sdk",
"smallvec",
"smol",
"states",
]
[[package]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.1" version = "2.0.1"
@@ -596,7 +610,7 @@ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"itertools 0.11.0", "itertools 0.13.0",
"log", "log",
"prettyplease", "prettyplease",
"proc-macro2", "proc-macro2",
@@ -616,7 +630,7 @@ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"itertools 0.11.0", "itertools 0.13.0",
"log", "log",
"prettyplease", "prettyplease",
"proc-macro2", "proc-macro2",
@@ -968,9 +982,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.43" version = "1.2.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@@ -1048,6 +1062,59 @@ dependencies = [
"zeroize", "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]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.42" version = "0.4.42"
@@ -1166,7 +1233,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@@ -1276,9 +1343,12 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
name = "coop" name = "coop"
version = "0.2.11" version = "0.2.11"
dependencies = [ dependencies = [
"account",
"anyhow", "anyhow",
"assets", "assets",
"auto_update", "auto_update",
"chat",
"chat_ui",
"common", "common",
"dirs 5.0.1", "dirs 5.0.1",
"flume", "flume",
@@ -1294,7 +1364,7 @@ dependencies = [
"nostr-connect", "nostr-connect",
"nostr-sdk", "nostr-sdk",
"oneshot", "oneshot",
"registry", "person",
"reqwest_client", "reqwest_client",
"rust-i18n", "rust-i18n",
"serde", "serde",
@@ -1606,7 +1676,7 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1873,7 +1943,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -2503,13 +2573,14 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.2.2" version = "0.2.2"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
"ashpd 0.11.0", "ashpd 0.11.0",
"async-task", "async-task",
"bindgen 0.71.1", "bindgen 0.71.1",
"bitflags 2.10.0",
"blade-graphics", "blade-graphics",
"blade-macros", "blade-macros",
"blade-util", "blade-util",
@@ -2583,6 +2654,7 @@ dependencies = [
"wayland-cursor", "wayland-cursor",
"wayland-protocols 0.31.2", "wayland-protocols 0.31.2",
"wayland-protocols-plasma", "wayland-protocols-plasma",
"wayland-protocols-wlr",
"windows 0.61.3", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-numerics", "windows-numerics",
@@ -2598,7 +2670,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -2609,7 +2681,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_tokio" name = "gpui_tokio"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -2838,7 +2910,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-compression", "async-compression",
@@ -2863,7 +2935,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client_tls" name = "http_client_tls"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
@@ -3091,9 +3163,9 @@ dependencies = [
[[package]] [[package]]
name = "ignore" name = "ignore"
version = "0.4.24" version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81776e6f9464432afcc28d03e52eb101c93b6f0566f52aef2427663e700f0403" checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
dependencies = [ dependencies = [
"crossbeam-deque", "crossbeam-deque",
"globset", "globset",
@@ -3350,23 +3422,16 @@ name = "key_store"
version = "0.2.11" version = "0.2.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common",
"futures", "futures",
"gpui", "gpui",
"i18n",
"itertools 0.13.0",
"log", "log",
"nostr", "nostr",
"nostr-sdk", "nostr-sdk",
"rust-i18n",
"serde", "serde",
"serde_json", "serde_json",
"settings",
"smallvec", "smallvec",
"smol", "smol",
"states", "states",
"theme",
"ui",
] ]
[[package]] [[package]]
@@ -3676,7 +3741,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen 0.71.1", "bindgen 0.71.1",
@@ -3932,7 +3997,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf"
dependencies = [ dependencies = [
"aes", "aes",
"base64", "base64",
@@ -3956,7 +4021,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3968,7 +4033,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf"
dependencies = [ dependencies = [
"flatbuffers", "flatbuffers",
"lru", "lru",
@@ -3979,7 +4044,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-gossip" name = "nostr-gossip"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf"
dependencies = [ dependencies = [
"nostr", "nostr",
] ]
@@ -3987,7 +4052,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-gossip-memory" name = "nostr-gossip-memory"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"lru", "lru",
@@ -3999,7 +4064,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"flume", "flume",
@@ -4013,7 +4078,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-relay-pool" name = "nostr-relay-pool"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -4030,7 +4095,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#8927f6630c071d982f9aa79fb9cd6807ae554abe" source = "git+https://github.com/rust-nostr/nostr#6befa6de8ab080a8153b7f8b788981d7be365ebf"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -4056,7 +4121,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -4548,13 +4613,29 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "perf" name = "perf"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"collections", "collections",
"serde", "serde",
"serde_json", "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]] [[package]]
name = "phf" name = "phf"
version = "0.11.3" version = "0.11.3"
@@ -4932,7 +5013,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.59.0", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -5165,7 +5246,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
] ]
@@ -5199,27 +5280,6 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 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]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.24" version = "0.12.24"
@@ -5272,7 +5332,7 @@ dependencies = [
[[package]] [[package]]
name = "reqwest_client" name = "reqwest_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -5326,11 +5386,11 @@ dependencies = [
[[package]] [[package]]
name = "rope" name = "rope"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"gpui",
"log", "log",
"rayon",
"sum_tree", "sum_tree",
"unicode-segmentation", "unicode-segmentation",
"util", "util",
@@ -5485,7 +5545,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.11.0", "linux-raw-sys 0.11.0",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -5792,7 +5852,7 @@ checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@@ -6239,11 +6299,12 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"futures",
"itertools 0.14.0",
"log", "log",
"rayon",
] ]
[[package]] [[package]]
@@ -6516,7 +6577,7 @@ dependencies = [
"getrandom 0.3.4", "getrandom 0.3.4",
"once_cell", "once_cell",
"rustix 1.1.2", "rustix 1.1.2",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -7088,20 +7149,13 @@ version = "0.2.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common", "common",
"emojis",
"gpui", "gpui",
"i18n",
"image", "image",
"itertools 0.13.0", "itertools 0.13.0",
"linkify",
"log", "log",
"lsp-types", "lsp-types",
"nostr-sdk",
"once_cell",
"regex", "regex",
"registry",
"rope", "rope",
"rust-i18n",
"serde", "serde",
"serde_json", "serde_json",
"smallvec", "smallvec",
@@ -7150,9 +7204,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.20" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]] [[package]]
name = "unicode-linebreak" name = "unicode-linebreak"
@@ -7162,18 +7216,18 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
version = "0.1.24" version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]] [[package]]
name = "unicode-properties" name = "unicode-properties"
version = "0.1.3" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]] [[package]]
name = "unicode-script" name = "unicode-script"
@@ -7275,7 +7329,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",
@@ -7310,7 +7364,7 @@ dependencies = [
[[package]] [[package]]
name = "util_macros" name = "util_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#8b051d6cc3c7c3bcda16702f30dc0fabe7b9f881" source = "git+https://github.com/zed-industries/zed#ecbdffc84f1165323f256e8485ae84320550c759"
dependencies = [ dependencies = [
"perf", "perf",
"quote", "quote",
@@ -7391,9 +7445,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version-compare" name = "version-compare"
version = "0.2.0" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
[[package]] [[package]]
name = "version_check" name = "version_check"
@@ -7612,6 +7666,19 @@ dependencies = [
"wayland-scanner", "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]] [[package]]
name = "wayland-scanner" name = "wayland-scanner"
version = "0.31.7" version = "0.31.7"
@@ -7758,7 +7825,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]

View File

@@ -32,7 +32,6 @@ nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96",
anyhow = "1.0.44" anyhow = "1.0.44"
chrono = "0.4.38" chrono = "0.4.38"
dirs = "5.0" dirs = "5.0"
emojis = "0.6.4"
futures = "0.3" futures = "0.3"
itertools = "0.13.0" itertools = "0.13.0"
log = "0.4" 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] [package]
name = "registry" name = "chat"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
publish.workspace = true publish.workspace = true
@@ -7,6 +7,8 @@ publish.workspace = true
[dependencies] [dependencies]
common = { path = "../common" } common = { path = "../common" }
states = { path = "../states" } states = { path = "../states" }
account = { path = "../account" }
person = { path = "../person" }
settings = { path = "../settings" } settings = { path = "../settings" }
gpui.workspace = true gpui.workspace = true

View File

@@ -1,18 +1,17 @@
use std::cmp::Reverse; use std::cmp::Reverse;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use account::Account;
use anyhow::Error; use anyhow::Error;
use common::event::EventUtils; use common::event::EventUtils;
use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use gpui::{ use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, Window};
App, AppContext, AsyncApp, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window,
};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use room::RoomKind; use room::RoomKind;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::app_state; use states::{app_state, NewMessage};
use crate::room::Room; use crate::room::Room;
@@ -20,162 +19,54 @@ pub mod message;
pub mod room; pub mod room;
pub fn init(cx: &mut App) { 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>); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChatEvent {
impl Global for GlobalRegistry {} OpenRoom(u64),
CloseRoom(u64),
#[derive(Debug)] NewChatRequest(RoomKind),
pub enum RegistryEvent {
Open(WeakEntity<Room>),
Close(u64),
NewRequest(RoomKind),
} }
pub struct Registry { struct GlobalChatRegistry(Entity<ChatRegistry>);
impl Global for GlobalChatRegistry {}
pub struct ChatRegistry {
/// Collection of all chat rooms /// Collection of all chat rooms
pub rooms: Vec<Entity<Room>>, pub rooms: Vec<Entity<Room>>,
/// Collection of all persons (user profiles)
pub persons: HashMap<PublicKey, Entity<Profile>>,
/// Loading status of the registry /// Loading status of the registry
pub loading: bool, pub loading: bool,
/// Public Key of the currently activated signer
signer_pubkey: Option<PublicKey>,
/// Tasks for asynchronous operations /// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 2]>, _tasks: SmallVec<[Task<()>; 2]>,
} }
impl EventEmitter<RegistryEvent> for Registry {} impl EventEmitter<ChatEvent> for ChatRegistry {}
impl Registry { impl ChatRegistry {
/// Retrieve the global registry state /// Retrieve the global registry state
pub fn global(cx: &App) -> Entity<Self> { pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalRegistry>().0.clone() cx.global::<GlobalChatRegistry>().0.clone()
}
/// Retrieve the registry instance
pub fn read_global(cx: &App) -> &Self {
cx.global::<GlobalRegistry>().0.read(cx)
} }
/// Set the global registry instance /// Set the global registry instance
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) { 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 /// Create a new registry instance
pub(crate) fn new(cx: &mut Context<Self>) -> Self { 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}");
}
};
}),
);
Self { Self {
rooms: vec![], rooms: vec![],
persons: HashMap::new(),
signer_pubkey: None,
loading: true, loading: true,
_tasks: tasks, _tasks: smallvec![],
}
}
/// 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));
}
} }
} }
/// Set the loading status of the chat registry
pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) { pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
self.loading = loading; self.loading = loading;
cx.notify(); cx.notify();
@@ -216,7 +107,7 @@ impl Registry {
/// Close a room. /// Close a room.
pub fn close_room(&mut self, id: u64, cx: &mut Context<Self>) { pub fn close_room(&mut self, id: u64, cx: &mut Context<Self>) {
if self.rooms.iter().any(|r| r.read(cx).id == id) { 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. /// Reset the registry.
pub fn reset(&mut self, cx: &mut Context<Self>) { pub fn reset(&mut self, cx: &mut Context<Self>) {
// Clear the current identity
self.signer_pubkey = None;
// Clear all current rooms
self.rooms.clear(); self.rooms.clear();
cx.notify(); 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>) { pub fn push_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) {
let other_id = room.read(cx).id; let id = room.read(cx).id;
let find_room = self.rooms.iter().find(|this| this.read(cx).id == other_id);
let weak_room = if let Some(room) = find_room { if !self.rooms.iter().any(|r| r.read(cx).id == id) {
room.downgrade()
} else {
let weak_room = room.downgrade();
// Add this room to the registry
self.add_room(room, cx); self.add_room(room, cx);
}
weak_room cx.emit(ChatEvent::OpenRoom(id));
};
cx.emit(RegistryEvent::Open(weak_room));
} }
/// Refresh messages for a room in the global registry /// 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. /// If the room doesn't exist, it will be created.
/// Updates room ordering based on the most recent messages. /// Updates room ordering based on the most recent messages.
pub fn event_to_message( pub fn new_message(&mut self, msg: NewMessage, window: &mut Window, cx: &mut Context<Self>) {
&mut self, let id = msg.rumor.uniq_id();
gift_wrap: EventId, let author = msg.rumor.pubkey;
event: UnsignedEvent, let account = Account::global(cx);
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;
};
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) { 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 is_new_event = msg.rumor.created_at > room.read(cx).created_at;
let created_at = event.created_at; let created_at = msg.rumor.created_at;
let event_for_emit = event.clone(); let event_for_emit = msg.rumor.clone();
// Update room // Update room
room.update(cx, |this, cx| { room.update(cx, |this, cx| {
@@ -439,14 +309,14 @@ impl Registry {
} }
// Set this room is ongoing if the new message is from current user // 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); this.set_ongoing(cx);
} }
// Emit the new message to the room // Emit the new message to the room
let event_to_emit = event_for_emit.clone(); let event_to_emit = event_for_emit.clone();
cx.defer_in(window, move |this, _window, cx| { 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 { } else {
// Push the new room to the front of the list // 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 // Notify the UI about the new room
cx.defer_in(window, move |_this, _window, cx| { 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::hash::{Hash, Hasher};
use std::time::Duration; use std::time::Duration;
use account::Account;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use common::display::RenderedProfile; use common::display::RenderedProfile;
use common::event::EventUtils; use common::event::EventUtils;
use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task}; use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry;
use states::{app_state, SignerKind, SEND_RETRY}; use states::{app_state, SignerKind, SEND_RETRY};
use crate::Registry;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SendOptions { pub struct SendOptions {
pub backup: bool, pub backup: bool,
@@ -119,10 +119,12 @@ pub enum RoomKind {
#[derive(Debug)] #[derive(Debug)]
pub struct Room { pub struct Room {
/// Conversation ID
pub id: u64, pub id: u64,
/// The timestamp of the last message in the room
pub created_at: Timestamp, pub created_at: Timestamp,
/// Subject of the room /// Subject of the room
pub subject: Option<String>, pub subject: Option<SharedString>,
/// All members of the room /// All members of the room
pub members: Vec<PublicKey>, pub members: Vec<PublicKey>,
/// Kind /// Kind
@@ -169,7 +171,7 @@ impl From<&Event> for Room {
let subject = val let subject = val
.tags .tags
.find(TagKind::Subject) .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 { Room {
id, id,
@@ -193,7 +195,7 @@ impl From<&UnsignedEvent> for Room {
let subject = val let subject = val
.tags .tags
.find(TagKind::Subject) .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 { Room {
id, id,
@@ -262,8 +264,11 @@ impl Room {
} }
/// Updates the subject of the room /// Updates the subject of the room
pub fn set_subject(&mut self, subject: String, cx: &mut Context<Self>) { pub fn set_subject<T>(&mut self, subject: T, cx: &mut Context<Self>)
self.subject = Some(subject); where
T: Into<SharedString>,
{
self.subject = Some(subject.into());
cx.notify(); cx.notify();
} }
@@ -279,8 +284,8 @@ impl Room {
/// Gets the display name for the room /// Gets the display name for the room
pub fn display_name(&self, cx: &App) -> SharedString { pub fn display_name(&self, cx: &App) -> SharedString {
if let Some(subject) = self.subject.clone() { if let Some(value) = self.subject.clone() {
SharedString::from(subject) value
} else { } else {
self.merged_name(cx) self.merged_name(cx)
} }
@@ -299,28 +304,29 @@ impl Room {
/// ///
/// Display member is always different from the current user. /// Display member is always different from the current user.
pub fn display_member(&self, cx: &App) -> Profile { pub fn display_member(&self, cx: &App) -> Profile {
let registry = Registry::global(cx); let persons = PersonRegistry::global(cx);
let signer_pubkey = registry.read(cx).signer_pubkey(); let account = Account::global(cx);
let public_key = account.read(cx).public_key();
let target_member = self let target_member = self
.members .members
.iter() .iter()
.find(|&member| Some(member) != signer_pubkey.as_ref()) .find(|&member| member != &public_key)
.or_else(|| self.members.first()) .or_else(|| self.members.first())
.expect("Room should have at least one member"); .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. /// Merge the names of the first two members of the room.
fn merged_name(&self, cx: &App) -> SharedString { fn merged_name(&self, cx: &App) -> SharedString {
let registry = Registry::read_global(cx); let persons = PersonRegistry::global(cx);
if self.is_group() { if self.is_group() {
let profiles: Vec<Profile> = self let profiles: Vec<Profile> = self
.members .members
.iter() .iter()
.map(|public_key| registry.get_person(public_key, cx)) .map(|public_key| persons.read(cx).get_person(public_key, cx))
.collect(); .collect();
let mut name = profiles let mut name = profiles
@@ -452,8 +458,8 @@ impl Room {
let relay_cache = state.relay_cache.read_blocking(); let relay_cache = state.relay_cache.read_blocking();
// Get current user // Get current user
let registry = Registry::global(cx); let account = Account::global(cx);
let public_key = registry.read(cx).signer_pubkey().unwrap(); let public_key = account.read(cx).public_key();
// Get room's subject // Get room's subject
let subject = self.subject.clone(); let subject = self.subject.clone();
@@ -481,9 +487,9 @@ impl Room {
} }
// Add subject tag if it's present // 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( 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, RenderOnce, SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use crate::button::{Button, ButtonVariants}; use ui::input::InputState;
use crate::input::InputState; use ui::popover::{Popover, PopoverContent};
use crate::popover::{Popover, PopoverContent}; use ui::{Icon, Sizable, Size};
use crate::{Icon, Sizable, Size};
static EMOJIS: OnceLock<Vec<SharedString>> = OnceLock::new(); static EMOJIS: OnceLock<Vec<SharedString>> = OnceLock::new();
@@ -31,27 +30,33 @@ fn get_emojis() -> &'static Vec<SharedString> {
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct EmojiPicker { pub struct EmojiPicker {
target: Option<WeakEntity<InputState>>,
icon: Option<Icon>, icon: Option<Icon>,
size: Size,
anchor: Option<Corner>, anchor: Option<Corner>,
target_input: WeakEntity<InputState>, size: Size,
} }
impl EmojiPicker { impl EmojiPicker {
pub fn new(target_input: WeakEntity<InputState>) -> Self { pub fn new() -> Self {
Self { Self {
target_input,
size: Size::default(), size: Size::default(),
target: None,
anchor: None, anchor: None,
icon: 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 { pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(icon.into()); self.icon = Some(icon.into());
self self
} }
#[allow(dead_code)]
pub fn anchor(mut self, corner: Corner) -> Self { pub fn anchor(mut self, corner: Corner) -> Self {
self.anchor = Some(corner); self.anchor = Some(corner);
self self
@@ -67,7 +72,7 @@ impl Sizable for EmojiPicker {
impl RenderOnce for EmojiPicker { impl RenderOnce for EmojiPicker {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
Popover::new("emoji-picker") Popover::new("emojis")
.map(|this| { .map(|this| {
if let Some(corner) = self.anchor { if let Some(corner) = self.anchor {
this.anchor(corner) this.anchor(corner)
@@ -76,13 +81,13 @@ impl RenderOnce for EmojiPicker {
} }
}) })
.trigger( .trigger(
Button::new("emoji-trigger") Button::new("emojis-trigger")
.when_some(self.icon, |this, icon| this.icon(icon)) .when_some(self.icon, |this, icon| this.icon(icon))
.ghost() .ghost()
.with_size(self.size), .with_size(self.size),
) )
.content(move |window, cx| { .content(move |window, cx| {
let input = self.target_input.clone(); let input = self.target.clone();
cx.new(|cx| { cx.new(|cx| {
PopoverContent::new(window, cx, move |_window, 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)) .hover(|this| this.bg(cx.theme().ghost_element_hover))
.on_click({ .on_click({
let item = e.clone(); let item = e.clone();
let input = input.upgrade(); let input = input.clone();
move |_, window, cx| { move |_, window, cx| {
if let Some(input) = input.as_ref() { if let Some(input) = input.as_ref() {
input.update(cx, |this, cx| { _ = input.update(cx, |this, cx| {
let current = this.value(); let value = this.value();
let new_text = if current.is_empty() { let new_text = if value.is_empty() {
format!("{item}") format!("{item}")
} else if current.ends_with(" ") { } else if value.ends_with(" ") {
format!("{current}{item}") format!("{value}{item}")
} else { } else {
format!("{current} {item}") format!("{value} {item}")
}; };
this.set_value(new_text, window, cx); this.set_value(new_text, window, cx);
}); });

View File

@@ -1,61 +1,58 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::time::Duration; 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::display::{RenderedProfile, RenderedTimestamp};
use common::nip96::nip96_upload; use common::nip96::nip96_upload;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ 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, ClipboardItem, Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable,
InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit,
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString, SharedUri, ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString, SharedUri,
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, Window, StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, Window,
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::{shared_t, t};
use indexset::{BTreeMap, BTreeSet}; use indexset::{BTreeMap, BTreeSet};
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::message::{Message, RenderedMessage}; use person::PersonRegistry;
use registry::room::{Room, RoomKind, RoomSignal, SendOptions, SendReport};
use registry::Registry;
use serde::Deserialize;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::fs; use smol::fs;
use states::{app_state, SignerKind}; use states::{app_state, SignerKind, QUERY_TIMEOUT};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::actions::{CopyPublicKey, OpenPublicKey};
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::context_menu::ContextMenuExt; use ui::context_menu::ContextMenuExt;
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
use ui::emoji_picker::EmojiPicker;
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::notification::Notification; use ui::notification::Notification;
use ui::popup_menu::PopupMenuExt; use ui::popup_menu::PopupMenuExt;
use ui::text::RenderedText;
use ui::{ use ui::{
h_flex, v_flex, ContextModal, Disableable, Icon, IconName, InteractiveElementExt, Sizable, h_flex, v_flex, ContextModal, Disableable, Icon, IconName, InteractiveElementExt, Sizable,
StyledExt, StyledExt,
}; };
use crate::emoji::EmojiPicker;
use crate::text::RenderedText;
mod actions;
mod emoji;
mod subject; mod subject;
mod text;
#[derive(Action, Clone, PartialEq, Eq, Deserialize)] const NIP17_WARN: &str = "has not set up Messaging Relays, they cannot receive your message.";
#[action(namespace = chat, no_json)] const EMPTY_WARN: &str = "Something is wrong. Coop cannot display this message";
pub struct SeenOn(pub EventId);
#[derive(Action, Clone, PartialEq, Eq, Deserialize)] pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
#[action(namespace = chat, no_json)] cx.new(|cx| ChatPanel::new(room, window, cx))
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 struct Chat { pub struct ChatPanel {
// Chat Room // Chat Room
room: Entity<Room>, room: Entity<Room>,
@@ -83,11 +80,11 @@ pub struct Chat {
_tasks: SmallVec<[Task<()>; 3]>, _tasks: SmallVec<[Task<()>; 3]>,
} }
impl Chat { impl ChatPanel {
pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let input = cx.new(|cx| { let input = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.placeholder(t!("chat.placeholder")) .placeholder("Message...")
.auto_grow(1, 20) .auto_grow(1, 20)
.prevent_new_line_on_enter() .prevent_new_line_on_enter()
.clean_on_escape() .clean_on_escape()
@@ -140,21 +137,23 @@ impl Chat {
// Connect and verify all members messaging relays // Connect and verify all members messaging relays
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
// Wait for 5 seconds before connecting and verifying // 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; let result = verify_connections.await;
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
match result { match result {
Ok(data) => { Ok(data) => {
let registry = Registry::global(cx); let persons = PersonRegistry::global(cx);
for (public_key, status) in data.into_iter() { for (public_key, status) in data.into_iter() {
if !status { if !status {
let profile = registry.read(cx).get_person(&public_key, cx); let profile = persons.read(cx).get_person(&public_key, cx);
let content = t!("chat.nip17_warn", u = profile.display_name()); 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 // Return if message is empty
if content.trim().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; return;
} }
@@ -322,10 +321,10 @@ impl Chat {
// Optimistically update message list // Optimistically update message list
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let delay = Duration::from_millis(100);
// Wait for the delay // 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 // Update the message list and reset the states
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
@@ -476,8 +475,8 @@ impl Chat {
} }
fn profile(&self, public_key: &PublicKey, cx: &Context<Self>) -> Profile { fn profile(&self, public_key: &PublicKey, cx: &Context<Self>) -> Profile {
let registry = Registry::read_global(cx); let persons = PersonRegistry::global(cx);
registry.get_person(public_key, cx) persons.read(cx).get_person(public_key, cx)
} }
fn signer_kind(&self, cx: &App) -> SignerKind { fn signer_kind(&self, cx: &App) -> SignerKind {
@@ -629,7 +628,9 @@ impl Chat {
.size_10() .size_10()
.text_color(cx.theme().elevated_surface_background), .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() .into_any_element()
} }
@@ -704,8 +705,8 @@ impl Chat {
let view = Box::new(OpenPublicKey(public_key)); let view = Box::new(OpenPublicKey(public_key));
let copy = Box::new(CopyPublicKey(public_key)); let copy = Box::new(CopyPublicKey(public_key));
this.menu(t!("profile.view"), view) this.menu("View Profile", view)
.menu(t!("profile.copy"), copy) .menu("Copy Public Key", copy)
}), }),
) )
}) })
@@ -806,14 +807,14 @@ impl Chat {
fn render_message_sent(&self, id: &EventId, _cx: &Context<Self>) -> impl IntoElement { fn render_message_sent(&self, id: &EventId, _cx: &Context<Self>) -> impl IntoElement {
div() div()
.id(SharedString::from(id.to_hex())) .id(SharedString::from(id.to_hex()))
.child(shared_t!("chat.sent")) .child(SharedString::from("• Sent"))
.when_some(self.sent_reports(id).cloned(), |this, reports| { .when_some(self.sent_reports(id).cloned(), |this, reports| {
this.on_click(move |_e, window, cx| { this.on_click(move |_e, window, cx| {
let reports = reports.clone(); let reports = reports.clone();
window.open_modal(cx, move |this, _window, cx| { window.open_modal(cx, move |this, _window, cx| {
this.show_close(true) this.show_close(true)
.title(shared_t!("chat.reports")) .title(SharedString::from("Sent Reports"))
.child(v_flex().pb_4().gap_4().children({ .child(v_flex().pb_4().gap_4().children({
let mut items = Vec::with_capacity(reports.len()); let mut items = Vec::with_capacity(reports.len());
@@ -836,14 +837,16 @@ impl Chat {
.text_xs() .text_xs()
.italic() .italic()
.child(Icon::new(IconName::Info).xsmall()) .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| { .when_some(self.sent_reports(id).cloned(), |this, reports| {
this.on_click(move |_e, window, cx| { this.on_click(move |_e, window, cx| {
let reports = reports.clone(); let reports = reports.clone();
window.open_modal(cx, move |this, _window, cx| { window.open_modal(cx, move |this, _window, cx| {
this.show_close(true) this.show_close(true)
.title(shared_t!("chat.reports")) .title(SharedString::from("Sent Reports"))
.child(v_flex().gap_4().pb_4().w_full().children({ .child(v_flex().gap_4().pb_4().w_full().children({
let mut items = Vec::with_capacity(reports.len()); let mut items = Vec::with_capacity(reports.len());
@@ -859,8 +862,8 @@ impl Chat {
} }
fn render_report(report: &SendReport, cx: &App) -> impl IntoElement { fn render_report(report: &SendReport, cx: &App) -> impl IntoElement {
let registry = Registry::read_global(cx); let persons = PersonRegistry::global(cx);
let profile = registry.get_person(&report.receiver, cx); let profile = persons.read(cx).get_person(&report.receiver, cx);
let name = profile.display_name(); let name = profile.display_name();
let avatar = profile.avatar(true); let avatar = profile.avatar(true);
@@ -871,7 +874,7 @@ impl Chat {
h_flex() h_flex()
.gap_2() .gap_2()
.text_sm() .text_sm()
.child(shared_t!("chat.sent_to")) .child(SharedString::from("Sent to:"))
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
@@ -897,7 +900,7 @@ impl Chat {
.flex_1() .flex_1()
.w_full() .w_full()
.text_center() .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() .flex_1()
.w_full() .w_full()
.text_center() .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_sm()
.text_color(cx.theme().secondary_foreground) .text_color(cx.theme().secondary_foreground)
.line_height(relative(1.25)) .line_height(relative(1.25))
.child(shared_t!("chat.sent_success")), .child(SharedString::from("Successfully")),
), ),
) )
} }
@@ -1035,7 +1038,7 @@ impl Chat {
.child( .child(
Button::new("reply") Button::new("reply")
.icon(IconName::Reply) .icon(IconName::Reply)
.tooltip(t!("chat.reply_button")) .tooltip("Reply")
.small() .small()
.ghost() .ghost()
.on_click({ .on_click({
@@ -1048,7 +1051,7 @@ impl Chat {
.child( .child(
Button::new("copy") Button::new("copy")
.icon(IconName::Copy) .icon(IconName::Copy)
.tooltip(t!("chat.copy_message_button")) .tooltip("Copy")
.small() .small()
.ghost() .ghost()
.on_click({ .on_click({
@@ -1066,9 +1069,7 @@ impl Chat {
.ghost() .ghost()
.popup_menu({ .popup_menu({
let id = id.to_owned(); let id = id.to_owned();
move |this, _window, _cx| { move |this, _, _| this.menu("Seen on", Box::new(SeenOn(id)))
this.menu(t!("common.seen_on"), Box::new(SeenOn(id)))
}
}), }),
) )
.group_hover("", |this| this.visible()) .group_hover("", |this| this.visible())
@@ -1123,8 +1124,8 @@ impl Chat {
fn render_reply(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement { fn render_reply(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
if let Some(text) = self.message(id) { if let Some(text) = self.message(id) {
let registry = Registry::read_global(cx); let persons = PersonRegistry::global(cx);
let profile = registry.get_person(&text.author, cx); let profile = persons.read(cx).get_person(&text.author, cx);
div() div()
.w_full() .w_full()
@@ -1143,7 +1144,7 @@ impl Chat {
.gap_1() .gap_1()
.text_xs() .text_xs()
.text_color(cx.theme().text_muted) .text_color(cx.theme().text_muted)
.child(SharedString::new(t!("chat.replying_to_label"))) .child(SharedString::from("Replying to:"))
.child( .child(
div() div()
.text_color(cx.theme().text_accent) .text_color(cx.theme().text_accent)
@@ -1201,7 +1202,7 @@ impl Chat {
Button::new("subject") Button::new("subject")
.icon(IconName::Edit) .icon(IconName::Edit)
.tooltip(t!("chat.subject_tooltip")) .tooltip("Change the subject of the conversation")
.on_click(move |_, window, cx| { .on_click(move |_, window, cx| {
let view = subject::init(subject.clone(), window, cx); let view = subject::init(subject.clone(), window, cx);
let room = room.clone(); let room = room.clone();
@@ -1212,9 +1213,9 @@ impl Chat {
let weak_view = weak_view.clone(); let weak_view = weak_view.clone();
this.confirm() this.confirm()
.title(shared_t!("chat.subject_tooltip")) .title("Change the subject of the conversation")
.child(view.clone()) .child(view.clone())
.button_props(ModalButtonProps::default().ok_text(t!("common.change"))) .button_props(ModalButtonProps::default().ok_text("Change"))
.on_ok(move |_, _window, cx| { .on_ok(move |_, _window, cx| {
if let Ok(subject) = if let Ok(subject) =
weak_view.read_with(cx, |this, cx| this.new_subject(cx)) weak_view.read_with(cx, |this, cx| this.new_subject(cx))
@@ -1236,13 +1237,12 @@ impl Chat {
Button::new("reload") Button::new("reload")
.icon(IconName::Refresh) .icon(IconName::Refresh)
.tooltip(t!("chat.reload_tooltip")) .tooltip("Reload")
.on_click(move |_, window, cx| { .on_click(move |_ev, window, cx| {
window.push_notification(t!("common.refreshed"), cx); _ = room.update(cx, |this, cx| {
room.update(cx, |this, cx| {
this.emit_refresh(cx); this.emit_refresh(cx);
}) window.push_notification("Reloaded", cx);
.ok(); });
}) })
} }
@@ -1275,8 +1275,9 @@ impl Chat {
if let Ok(urls) = task.await { if let Ok(urls) = task.await {
cx.update(|window, cx| { cx.update(|window, cx| {
window.open_modal(cx, move |this, _window, cx| { window.open_modal(cx, move |this, _window, cx| {
this.title(shared_t!("common.seen_on")).child( this.show_close(true)
v_flex().pb_4().gap_2().children({ .title(SharedString::from("Seen on"))
.child(v_flex().pb_4().gap_2().children({
let mut items = Vec::with_capacity(urls.len()); let mut items = Vec::with_capacity(urls.len());
for url in urls.clone().into_iter() { for url in urls.clone().into_iter() {
@@ -1288,13 +1289,12 @@ impl Chat {
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.font_semibold() .font_semibold()
.text_xs() .text_xs()
.child(url.to_string()), .child(SharedString::from(url.to_string())),
) )
} }
items items
}), }))
)
}); });
}) })
.ok(); .ok();
@@ -1311,7 +1311,7 @@ impl Chat {
} }
} }
impl Panel for Chat { impl Panel for ChatPanel {
fn panel_id(&self) -> SharedString { fn panel_id(&self) -> SharedString {
self.id.clone() 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 { fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone() 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 { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let kind = self.signer_kind(cx); let kind = self.signer_kind(cx);
@@ -1376,7 +1376,7 @@ impl Render for Chat {
Message::System(_timestamp) => this.render_announcement(ix, cx), Message::System(_timestamp) => this.render_announcement(ix, cx),
} }
} else { } 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( .child(
EmojiPicker::new(self.input.downgrade()) EmojiPicker::new()
.target(self.input.downgrade())
.icon(IconName::EmojiFill) .icon(IconName::EmojiFill)
.large(), .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 linkify::{LinkFinder, LinkKind};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use person::PersonRegistry;
use regex::Regex; use regex::Regex;
use registry::Registry;
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::actions::OpenPublicKey; 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) pub fn set_tooltip_builder_for_custom_ranges<F>(&mut self, f: F)
where where
F: Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView> + 'static, 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>, link_urls: &mut Vec<String>,
cx: &App, cx: &App,
) { ) {
let registry = Registry::read_global(cx); let persons = PersonRegistry::global(cx);
let profile = registry.get_person(&public_key, cx); let profile = persons.read(cx).get_person(&public_key, cx);
let display_name = format!("@{}", profile.display_name()); let display_name = format!("@{}", profile.display_name());
// Replace token with display name // Replace token with display name

View File

@@ -34,9 +34,12 @@ theme = { path = "../theme" }
common = { path = "../common" } common = { path = "../common" }
states = { path = "../states" } states = { path = "../states" }
key_store = { path = "../key_store" } key_store = { path = "../key_store" }
registry = { path = "../registry" } chat = { path = "../chat" }
chat_ui = { path = "../chat_ui" }
settings = { path = "../settings" } settings = { path = "../settings" }
auto_update = { path = "../auto_update" } auto_update = { path = "../auto_update" }
account = { path = "../account" }
person = { path = "../person" }
rust-i18n.workspace = true rust-i18n.workspace = true
i18n.workspace = true i18n.workspace = true

View File

@@ -1,10 +1,9 @@
use std::sync::Mutex; use std::sync::Mutex;
use gpui::{actions, App, AppContext}; use gpui::{actions, App};
use key_store::backend::KeyItem; use key_store::backend::KeyItem;
use key_store::KeyStore; use key_store::KeyStore;
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use registry::Registry;
use states::app_state; use states::app_state;
actions!(coop, [ReloadMetadata, DarkMode, Settings, Logout, Quit]); actions!(coop, [ReloadMetadata, DarkMode, Settings, Logout, Quit]);
@@ -48,31 +47,30 @@ pub fn load_embedded_fonts(cx: &App) {
} }
pub fn reset(cx: &mut App) { pub fn reset(cx: &mut App) {
let registry = Registry::global(cx);
let backend = KeyStore::global(cx).read(cx).backend(); let backend = KeyStore::global(cx).read(cx).backend();
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
cx.background_spawn(async move { let client = app_state().client();
let client = app_state().client();
client.unset_signer().await;
})
.await;
// Remove the signer
client.unset_signer().await;
// Delete user's credentials
backend backend
.delete_credentials(&KeyItem::User.to_string(), cx) .delete_credentials(&KeyItem::User.to_string(), cx)
.await .await
.ok(); .ok();
// Remove bunker's credentials if available
backend backend
.delete_credentials(&KeyItem::Bunker.to_string(), cx) .delete_credentials(&KeyItem::Bunker.to_string(), cx)
.await .await
.ok(); .ok();
registry cx.update(|cx| {
.update(cx, |this, cx| { cx.restart();
this.reset(cx); })
}) .ok();
.ok();
}) })
.detach(); .detach();
} }

View File

@@ -2,8 +2,11 @@ use std::borrow::Cow;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::Arc; use std::sync::Arc;
use account::Account;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use auto_update::AutoUpdater; use auto_update::AutoUpdater;
use chat::{ChatEvent, ChatRegistry};
use chat_ui::{CopyPublicKey, OpenPublicKey};
use common::display::{shorten_pubkey, RenderedProfile}; use common::display::{shorten_pubkey, RenderedProfile};
use common::event::EventUtils; use common::event::EventUtils;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
@@ -18,7 +21,7 @@ use key_store::backend::KeyItem;
use key_store::KeyStore; use key_store::KeyStore;
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::{Registry, RegistryEvent}; use person::PersonRegistry;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::{ use states::{
@@ -27,7 +30,6 @@ use states::{
}; };
use theme::{ActiveTheme, Theme, ThemeMode}; use theme::{ActiveTheme, Theme, ThemeMode};
use title_bar::TitleBar; use title_bar::TitleBar;
use ui::actions::{CopyPublicKey, OpenPublicKey};
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::dock::DockPlacement; 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::compose::compose_button;
use crate::views::setup_relay::SetupRelay; use crate::views::setup_relay::SetupRelay;
use crate::views::{ 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> { 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); ChatSpace::set_center_panel(panel, window, cx);
} }
#[derive(Debug)]
pub struct ChatSpace { pub struct ChatSpace {
/// App's Title Bar /// App's Title Bar
title_bar: Entity<TitleBar>, title_bar: Entity<TitleBar>,
@@ -84,7 +88,7 @@ pub struct ChatSpace {
impl ChatSpace { impl ChatSpace {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { 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 keystore = KeyStore::global(cx);
let title_bar = cx.new(|_| TitleBar::new()); let title_bar = cx.new(|_| TitleBar::new());
@@ -102,55 +106,49 @@ impl ChatSpace {
); );
subscriptions.push( subscriptions.push(
// Observe device changes // Observe keystore changes
cx.observe_in(&keystore, window, move |this, state, window, cx| { cx.observe_in(&keystore, window, move |_this, state, window, cx| {
if state.read(cx).initialized { if state.read(cx).initialized {
let backend = state.read(cx).backend(); let backend = state.read(cx).backend();
if state.read(cx).initialized { cx.spawn_in(window, async move |this, cx| {
if state.read(cx).is_using_file_keystore() { let result = backend
this.render_keyring_installation(window, cx); .read_credentials(&KeyItem::User.to_string(), cx)
} .await;
cx.spawn_in(window, async move |this, cx| { this.update_in(cx, |this, window, cx| {
let result = backend match result {
.read_credentials(&KeyItem::User.to_string(), cx) Ok(Some((user, secret))) => {
.await; let public_key = PublicKey::parse(&user).unwrap();
let secret = String::from_utf8(secret).unwrap();
this.update_in(cx, |this, window, cx| { this.set_account_layout(public_key, secret, window, cx);
match result { }
Ok(Some((user, secret))) => { _ => {
let public_key = PublicKey::parse(&user).unwrap(); this.set_onboarding_layout(window, cx);
let secret = String::from_utf8(secret).unwrap(); }
};
this.set_account_layout(public_key, secret, window, cx);
}
_ => {
this.set_onboarding_layout(window, cx);
}
};
})
.ok();
}) })
.detach(); .ok();
} })
.detach();
} }
}), }),
); );
subscriptions.push( subscriptions.push(
// Handle registry events // 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 { match ev {
RegistryEvent::Open(room) => { ChatEvent::OpenRoom(id) => {
if let Some(room) = room.upgrade() { if let Some(room) = chat.read(cx).room(id, cx) {
this.dock.update(cx, |this, 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); this.add_panel(Arc::new(panel), DockPlacement::Center, window, cx);
}); });
} }
} }
RegistryEvent::Close(..) => { ChatEvent::CloseRoom(..) => {
this.dock.update(cx, |this, cx| { this.dock.update(cx, |this, cx| {
this.focus_tab_panel(window, cx); this.focus_tab_panel(window, cx);
@@ -217,7 +215,8 @@ impl ChatSpace {
while let Ok(signal) = states.signal().receiver().recv_async().await { while let Ok(signal) = states.signal().receiver().recv_async().await {
view.update_in(cx, |this, window, cx| { 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); let settings = AppSettings::global(cx);
match signal { match signal {
@@ -234,8 +233,8 @@ impl ChatSpace {
this.receive_encryption(response, window, cx); this.receive_encryption(response, window, cx);
} }
SignalKind::SignerSet(public_key) => { SignalKind::SignerSet(public_key) => {
// Close all opened modals // Set the global account state
window.close_all_modals(cx); account::init(public_key, cx);
// Load user's settings // Load user's settings
settings.update(cx, |this, cx| { settings.update(cx, |this, cx| {
@@ -243,11 +242,13 @@ impl ChatSpace {
}); });
// Load all chat rooms // Load all chat rooms
registry.update(cx, |this, cx| { chat.update(cx, |this, cx| {
this.set_signer_pubkey(public_key, cx);
this.load_rooms(window, cx); this.load_rooms(window, cx);
}); });
// Close all opened modals
window.close_all_modals(cx);
// Setup the default layout for current workspace // Setup the default layout for current workspace
this.set_default_layout(window, cx); this.set_default_layout(window, cx);
} }
@@ -271,7 +272,7 @@ impl ChatSpace {
if matches!(s, UnwrappingStatus::Processing | UnwrappingStatus::Complete) { if matches!(s, UnwrappingStatus::Processing | UnwrappingStatus::Complete) {
let all_panels = this.get_all_panel_ids(cx); 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.load_rooms(window, cx);
this.refresh_rooms(all_panels, cx); this.refresh_rooms(all_panels, cx);
@@ -282,13 +283,13 @@ impl ChatSpace {
} }
} }
SignalKind::NewProfile(profile) => { SignalKind::NewProfile(profile) => {
registry.update(cx, |this, cx| { persons.update(cx, |this, cx| {
this.insert_or_update_person(profile, cx); this.insert_or_update_person(profile, cx);
}); });
} }
SignalKind::NewMessage((gift_wrap_id, event)) => { SignalKind::NewMessage(msg) => {
registry.update(cx, |this, cx| { chat.update(cx, |this, cx| {
this.event_to_message(gift_wrap_id, event, window, cx); this.new_message(msg, window, cx);
}); });
} }
SignalKind::GossipRelaysNotFound => { SignalKind::GossipRelaysNotFound => {
@@ -621,7 +622,7 @@ impl ChatSpace {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, 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); let center = DockItem::panel(panel);
self.dock.update(cx, |this, cx| { 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>) { fn render_request(&mut self, ann: Announcement, window: &mut Window, cx: &mut Context<Self>) {
let client_name = SharedString::from(ann.client().to_string()); let client_name = SharedString::from(ann.client().to_string());
let target = ann.public_key(); let target = ann.public_key();
@@ -907,8 +882,25 @@ impl ChatSpace {
), ),
) )
.on_cancel(move |_ev, window, cx| { .on_cancel(move |_ev, window, cx| {
_ = view.update(cx, |this, cx| { _ = view.update(cx, |_, cx| {
this.render_reset(window, 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 to keep modal open
false 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) { fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) {
let relays = default_nip65_relays(); let relays = default_nip65_relays();
@@ -1139,13 +1110,39 @@ impl ChatSpace {
}) })
} }
fn render_titlebar_left_side( fn render_keyring_warning(window: &mut Window, cx: &mut App) {
&mut self, window.open_modal(cx, move |this, _window, cx| {
_window: &mut Window, this.overlay_closable(false)
cx: &Context<Self>, .show_close(false)
) -> impl IntoElement { .keyboard(false)
let registry = Registry::global(cx); .alert()
let status = registry.read(cx).loading; .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() h_flex()
.gap_2() .gap_2()
@@ -1166,12 +1163,8 @@ impl ChatSpace {
}) })
} }
fn render_titlebar_right_side( fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
&mut self, let file_keystore = KeyStore::global(cx).read(cx).is_using_file_keystore();
profile: &Profile,
_window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let proxy = AppSettings::get_proxy_user_avatars(cx); let proxy = AppSettings::get_proxy_user_avatars(cx);
let updating = AutoUpdater::read_global(cx).status.is_updating(); let updating = AutoUpdater::read_global(cx).status.is_updating();
let updated = AutoUpdater::read_global(cx).status.is_updated(); let updated = AutoUpdater::read_global(cx).status.is_updated();
@@ -1179,6 +1172,19 @@ impl ChatSpace {
h_flex() h_flex()
.gap_1() .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| { .when(updating, |this| {
this.child( this.child(
h_flex() h_flex()
@@ -1236,7 +1242,7 @@ impl ChatSpace {
Button::new("setup-relays-button") Button::new("setup-relays-button")
.icon(IconName::Info) .icon(IconName::Info)
.label(t!("messaging.button")) .label(t!("messaging.button"))
.warning() .ghost()
.xsmall() .xsmall()
.rounded() .rounded()
.on_click(move |_ev, window, cx| { .on_click(move |_ev, window, cx| {
@@ -1244,22 +1250,29 @@ impl ChatSpace {
}), }),
) )
}) })
.child( .when(Account::has_global(cx), |this| {
Button::new("user") let persons = PersonRegistry::global(cx);
.small() let account = Account::global(cx);
.reverse() let public_key = account.read(cx).public_key();
.transparent() let profile = persons.read(cx).get_person(&public_key, cx);
.icon(IconName::CaretDown)
.child(Avatar::new(profile.avatar(proxy)).size(rems(1.49))) this.child(
.popup_menu(|this, _window, _cx| { Button::new("user")
this.menu(t!("user.dark_mode"), Box::new(DarkMode)) .small()
.menu(t!("user.settings"), Box::new(Settings)) .reverse()
.separator() .transparent()
.menu(t!("user.reload_metadata"), Box::new(ReloadMetadata)) .icon(IconName::CaretDown)
.separator() .child(Avatar::new(profile.avatar(proxy)).size(rems(1.49)))
.menu(t!("user.sign_out"), Box::new(Logout)) .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 { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let modal_layer = Root::render_modal_layer(window, cx); let modal_layer = Root::render_modal_layer(window, cx);
let notification_layer = Root::render_notification_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 let left = self.titlebar_left(window, cx).into_any_element();
if let Some(public_key) = registry.signer_pubkey() { let right = self.titlebar_right(window, cx).into_any_element();
let profile = registry.get_person(&public_key, cx);
let left_side = self // Update title bar children
.render_titlebar_left_side(window, cx) self.title_bar.update(cx, |this, _cx| {
.into_any_element(); this.set_children(vec![left, right]);
});
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]);
})
}
div() div()
.id(SharedString::from("chatspace")) .id(SharedString::from("chatspace"))

View File

@@ -83,9 +83,12 @@ fn main() {
ui::init(cx); ui::init(cx);
// Initialize app registry // 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); key_store::init(cx);
// Initialize settings // Initialize settings

View File

@@ -12,7 +12,7 @@ use i18n::{shared_t, t};
use key_store::backend::KeyItem; use key_store::backend::KeyItem;
use key_store::KeyStore; use key_store::KeyStore;
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use registry::Registry; use person::PersonRegistry;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::{app_state, BUNKER_TIMEOUT}; use states::{app_state, BUNKER_TIMEOUT};
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -207,8 +207,8 @@ impl Focusable for Account {
impl Render for Account { impl Render for Account {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
let registry = Registry::global(cx); let persons = PersonRegistry::global(cx);
let profile = registry.read(cx).get_person(&self.public_key, cx); let profile = persons.read(cx).get_person(&self.public_key, cx);
let bunker = self.secret.starts_with("bunker://"); let bunker = self.secret.starts_with("bunker://");
v_flex() 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 std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use chat::room::Room;
use chat::ChatRegistry;
use common::display::{RenderedProfile, TextUtils}; use common::display::{RenderedProfile, TextUtils};
use common::nip05::nip05_profile; use common::nip05::nip05_profile;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
@@ -13,8 +15,7 @@ use gpui::{
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::room::Room; use person::PersonRegistry;
use registry::Registry;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::{app_state, BOOTSTRAP_RELAYS}; use states::{app_state, BOOTSTRAP_RELAYS};
@@ -311,7 +312,7 @@ impl Compose {
} }
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) { 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 receivers: Vec<PublicKey> = self.selected(cx);
let subject_input = self.title_input.read(cx).value(); let subject_input = self.title_input.read(cx).value();
let subject = (!subject_input.is_empty()).then(|| subject_input.to_string()); let subject = (!subject_input.is_empty()).then(|| subject_input.to_string());
@@ -327,10 +328,9 @@ impl Compose {
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
match result { match result {
Ok(room) => { Ok(room) => {
registry.update(cx, |this, cx| { chat.update(cx, |this, cx| {
this.push_room(cx.new(|_| room), cx); this.push_room(cx.new(|_| room), cx);
}); });
window.close_modal(cx); window.close_modal(cx);
} }
Err(e) => { Err(e) => {
@@ -372,7 +372,7 @@ impl Compose {
fn list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> { fn list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
let proxy = AppSettings::get_proxy_user_avatars(cx); 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()); let mut items = Vec::with_capacity(self.contacts.read(cx).len());
for ix in range { for ix in range {
@@ -381,7 +381,7 @@ impl Compose {
}; };
let public_key = contact.public_key; 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( items.push(
h_flex() h_flex()

View File

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

View File

@@ -1,12 +1,12 @@
use account::Account;
use common::display::RenderedProfile; use common::display::RenderedProfile;
use gpui::http_client::Url; use gpui::http_client::Url;
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, px, relative, rems, App, AppContext, Context, Entity, InteractiveElement, IntoElement, div, px, relative, rems, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window,
}; };
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use registry::Registry; use person::PersonRegistry;
use settings::AppSettings; use settings::AppSettings;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
@@ -53,7 +53,7 @@ impl Preferences {
.on_ok(move |_, window, cx| { .on_ok(move |_, window, cx| {
weak_view weak_view
.update(cx, |this, cx| { .update(cx, |this, cx| {
let registry = Registry::global(cx); let persons = PersonRegistry::global(cx);
let set_metadata = this.set_metadata(cx); let set_metadata = this.set_metadata(cx);
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
@@ -62,7 +62,7 @@ impl Preferences {
this.update_in(cx, |_, window, cx| { this.update_in(cx, |_, window, cx| {
match result { match result {
Ok(profile) => { Ok(profile) => {
registry.update(cx, |this, cx| { persons.update(cx, |this, cx| {
this.insert_or_update_person(profile, 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 proxy = AppSettings::get_proxy_user_avatars(cx);
let hide = AppSettings::get_hide_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(); let input_state = self.media_input.downgrade();
v_flex() v_flex()
@@ -130,54 +134,48 @@ impl Render for Preferences {
.font_semibold() .font_semibold()
.child(shared_t!("preferences.account_header")), .child(shared_t!("preferences.account_header")),
) )
.when_some(registry.signer_pubkey(), |this, public_key| { .child(
let profile = registry.get_person(&public_key, cx); h_flex()
.w_full()
this.child( .justify_between()
h_flex() .child(
.w_full() h_flex()
.justify_between() .id("user")
.child( .gap_2()
h_flex() .child(Avatar::new(profile.avatar(proxy)).size(rems(2.4)))
.id("user") .child(
.gap_2() div()
.child(Avatar::new(profile.avatar(proxy)).size(rems(2.4))) .flex_1()
.child( .text_sm()
div() .child(
.flex_1() div()
.text_sm() .font_semibold()
.child( .line_height(relative(1.3))
div() .child(profile.display_name()),
.font_semibold() )
.line_height(relative(1.3)) .child(
.child(profile.display_name()), div()
) .text_xs()
.child( .text_color(cx.theme().text_muted)
div() .line_height(relative(1.3))
.text_xs() .child(shared_t!("preferences.account_btn")),
.text_color(cx.theme().text_muted) ),
.line_height(relative(1.3)) )
.child(shared_t!( .on_click(cx.listener(move |this, _e, window, cx| {
"preferences.account_btn" this.open_edit_profile(window, cx);
)), })),
), )
) .child(
.on_click(cx.listener(move |this, _e, window, cx| { Button::new("relays")
this.open_edit_profile(window, cx); .label("Messaging Relays")
})), .xsmall()
) .ghost_alt()
.child( .rounded()
Button::new("relays") .on_click(cx.listener(move |this, _e, window, cx| {
.label("Messaging Relays") this.open_relays(window, cx);
.xsmall() })),
.ghost_alt() ),
.rounded() ),
.on_click(cx.listener(move |this, _e, window, cx| {
this.open_relays(window, cx);
})),
),
)
}),
) )
.child( .child(
v_flex() v_flex()

View File

@@ -10,7 +10,7 @@ use gpui::{
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::Registry; use person::PersonRegistry;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::{app_state, BOOTSTRAP_RELAYS}; use states::{app_state, BOOTSTRAP_RELAYS};
@@ -35,8 +35,8 @@ pub struct Screening {
impl Screening { impl Screening {
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
let registry = Registry::read_global(cx); let persons = PersonRegistry::global(cx);
let profile = registry.get_person(&public_key, cx); let profile = persons.read(cx).get_person(&public_key, cx);
let mut tasks = smallvec![]; let mut tasks = smallvec![];

View File

@@ -1,5 +1,8 @@
use std::rc::Rc; use std::rc::Rc;
use chat::room::RoomKind;
use chat::ChatRegistry;
use chat_ui::{CopyPublicKey, OpenPublicKey};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, rems, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce, div, rems, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce,
@@ -7,11 +10,8 @@ use gpui::{
}; };
use i18n::t; use i18n::t;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::room::RoomKind;
use registry::Registry;
use settings::AppSettings; use settings::AppSettings;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::actions::{CopyPublicKey, OpenPublicKey};
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::context_menu::ContextMenuExt; use ui::context_menu::ContextMenuExt;
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
@@ -187,7 +187,7 @@ impl RenderOnce for RoomListItem {
.ok_text(t!("screening.response")), .ok_text(t!("screening.response")),
) )
.on_cancel(move |_event, _window, cx| { .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); this.close_room(room_id, cx);
}); });
// false to prevent closing the modal // false to prevent closing the modal

View File

@@ -3,6 +3,8 @@ use std::ops::Range;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use chat::room::{Room, RoomKind};
use chat::{ChatEvent, ChatRegistry};
use common::debounced_delay::DebouncedDelay; use common::debounced_delay::DebouncedDelay;
use common::display::{RenderedProfile, RenderedTimestamp, TextUtils}; use common::display::{RenderedProfile, RenderedTimestamp, TextUtils};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
@@ -16,8 +18,6 @@ use i18n::{shared_t, t};
use itertools::Itertools; use itertools::Itertools;
use list_item::RoomListItem; use list_item::RoomListItem;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::room::{Room, RoomKind};
use registry::{Registry, RegistryEvent};
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::{app_state, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; use states::{app_state, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
@@ -73,7 +73,7 @@ impl Sidebar {
let find_input = let find_input =
cx.new(|cx| InputState::new(window, cx).placeholder(t!("sidebar.search_label"))); 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![]; let mut subscriptions = smallvec![];
subscriptions.push( subscriptions.push(
@@ -87,8 +87,8 @@ impl Sidebar {
subscriptions.push( subscriptions.push(
// Subscribe for registry new events // Subscribe for registry new events
cx.subscribe_in(&registry, window, move |this, _, event, _window, cx| { cx.subscribe_in(&chat, window, move |this, _, event, _window, cx| {
if let RegistryEvent::NewRequest(kind) = event { if let ChatEvent::NewChatRequest(kind) = event {
this.indicator.update(cx, |this, cx| { this.indicator.update(cx, |this, cx| {
*this = Some(kind.to_owned()); *this = Some(kind.to_owned());
cx.notify(); cx.notify();
@@ -326,8 +326,8 @@ impl Sidebar {
Ok(room) => { Ok(room) => {
cx.update(|window, cx| { cx.update(|window, cx| {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
let registry = Registry::read_global(cx); let chat = ChatRegistry::global(cx);
let result = registry.search_by_public_key(public_key, cx); let result = chat.read(cx).search_by_public_key(public_key, cx);
if !result.is_empty() { if !result.is_empty() {
this.results(result, false, window, cx); 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 // 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() { if !local_results.is_empty() {
// Try to update with local results first // 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>) { 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 room
} else { } else {
let Some(result) = self.global_result.read(cx).as_ref() else { let Some(result) = self.global_result.read(cx).as_ref() else {
@@ -514,13 +514,13 @@ impl Sidebar {
room room
}; };
Registry::global(cx).update(cx, |this, cx| { ChatRegistry::global(cx).update(cx, |this, cx| {
this.push_room(room, cx); this.push_room(room, cx);
}); });
} }
fn on_reload(&mut self, _ev: &Reload, window: &mut Window, cx: &mut Context<Self>) { 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); this.load_rooms(window, cx);
}); });
window.push_notification(t!("common.refreshed"), cx); window.push_notification(t!("common.refreshed"), cx);
@@ -661,8 +661,8 @@ impl Focusable for Sidebar {
impl Render for Sidebar { impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let registry = Registry::read_global(cx); let chat = ChatRegistry::global(cx);
let loading = registry.loading; let loading = chat.read(cx).loading;
// Get rooms from either search results or the chat registry // Get rooms from either search results or the chat registry
let rooms = if let Some(results) = self.local_result.read(cx).as_ref() { let rooms = if let Some(results) = self.local_result.read(cx).as_ref() {
@@ -672,9 +672,9 @@ impl Render for Sidebar {
} else { } else {
#[allow(clippy::collapsible_else_if)] #[allow(clippy::collapsible_else_if)]
if self.active_filter.read(cx) == &RoomKind::Ongoing { if self.active_filter.read(cx) == &RoomKind::Ongoing {
registry.ongoing_rooms(cx) chat.read(cx).ongoing_rooms(cx)
} else { } 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")) .tooltip(t!("sidebar.all_conversations_tooltip"))
.when_some(self.indicator.read(cx).as_ref(), |this, kind| { .when_some(self.indicator.read(cx).as_ref(), |this, kind| {
this.when(kind == &RoomKind::Ongoing, |this| { this.when(kind == &RoomKind::Ongoing, |this| {
this.child(deferred( this.child(
div().size_1().rounded_full().bg(cx.theme().cursor), div().size_1().rounded_full().bg(cx.theme().cursor),
)) )
}) })
}) })
.small() .small()
@@ -759,9 +759,9 @@ impl Render for Sidebar {
.tooltip(t!("sidebar.requests_tooltip")) .tooltip(t!("sidebar.requests_tooltip"))
.when_some(self.indicator.read(cx).as_ref(), |this, kind| { .when_some(self.indicator.read(cx).as_ref(), |this, kind| {
this.when(kind != &RoomKind::Ongoing, |this| { this.when(kind != &RoomKind::Ongoing, |this| {
this.child(deferred( this.child(
div().size_1().rounded_full().bg(cx.theme().cursor), div().size_1().rounded_full().bg(cx.theme().cursor),
)) )
}) })
}) })
.small() .small()

View File

@@ -10,7 +10,7 @@ use gpui::{
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::Registry; use person::PersonRegistry;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::app_state; use states::app_state;
@@ -33,8 +33,8 @@ pub struct UserProfile {
impl UserProfile { impl UserProfile {
pub fn new(target: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(target: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
let registry = Registry::read_global(cx); let persons = PersonRegistry::global(cx);
let profile = registry.get_person(&target, cx); let profile = persons.read(cx).get_person(&target, cx);
let mut tasks = smallvec![]; let mut tasks = smallvec![];

View File

@@ -5,21 +5,14 @@ edition.workspace = true
publish.workspace = true publish.workspace = true
[dependencies] [dependencies]
common = { path = "../common" }
states = { path = "../states" } states = { path = "../states" }
ui = { path = "../ui" }
theme = { path = "../theme" }
settings = { path = "../settings" }
rust-i18n.workspace = true
i18n.workspace = true
gpui.workspace = true gpui.workspace = true
nostr.workspace = true nostr.workspace = true
nostr-sdk.workspace = true nostr-sdk.workspace = true
anyhow.workspace = true anyhow.workspace = true
itertools.workspace = true
smallvec.workspace = true smallvec.workspace = true
smol.workspace = true smol.workspace = true
log.workspace = true log.workspace = true

View File

@@ -54,7 +54,6 @@ impl KeyStore {
// Only used for testing keyring availability on the user's system // Only used for testing keyring availability on the user's system
let read_credential = cx.read_credentials("Coop"); let read_credential = cx.read_credentials("Coop");
let mut tasks = smallvec![]; let mut tasks = smallvec![];
tasks.push( 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 { match event.created_at >= self.initialized_at {
// New message: send a signal to notify the UI // New message: send a signal to notify the UI
true => { 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 // Old message: Coop is probably processing the user's messages during initial load
false => { false => {

View File

@@ -1,6 +1,18 @@
use flume::{Receiver, Sender}; use flume::{Receiver, Sender};
use nostr_sdk::prelude::*; 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct AuthRequest { pub struct AuthRequest {
pub url: RelayUrl, pub url: RelayUrl,
@@ -111,7 +123,7 @@ pub enum SignalKind {
NewProfile(Profile), NewProfile(Profile),
/// A signal to notify UI that a new gift wrap event has been received /// 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 /// A signal to notify UI that no messaging relays for current user was found
MessagingRelaysNotFound, MessagingRelaysNotFound,

View File

@@ -89,11 +89,11 @@ impl Render for TitleBar {
.h(height) .h(height)
.map(|this| { .map(|this| {
if window.is_fullscreen() { if window.is_fullscreen() {
this.pl_2() this.px_2()
} else if cx.theme().platform_kind.is_mac() { } 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 { } else {
this.pl_2() this.px_2()
} }
}) })
.map(|this| match decorations { .map(|this| match decorations {

View File

@@ -7,11 +7,7 @@ publish.workspace = true
[dependencies] [dependencies]
common = { path = "../common" } common = { path = "../common" }
theme = { path = "../theme" } theme = { path = "../theme" }
registry = { path = "../registry" }
rust-i18n.workspace = true
i18n.workspace = true
nostr-sdk.workspace = true
gpui.workspace = true gpui.workspace = true
smol.workspace = true smol.workspace = true
serde.workspace = true serde.workspace = true
@@ -20,14 +16,11 @@ smallvec.workspace = true
anyhow.workspace = true anyhow.workspace = true
itertools.workspace = true itertools.workspace = true
log.workspace = true log.workspace = true
emojis.workspace = true
regex = "1"
unicode-segmentation = "1.12.0" unicode-segmentation = "1.12.0"
uuid = "1.10" uuid = "1.10"
once_cell = "1.19.0" regex = "1"
image = "0.25.1" image = "0.25.1"
linkify = "0.10.0"
lsp-types = "0.97.0" lsp-types = "0.97.0"
rope = { git = "https://github.com/zed-industries/zed" } rope = { git = "https://github.com/zed-industries/zed" }
sum_tree = { 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 gpui::{actions, Action};
use nostr_sdk::prelude::PublicKey;
use serde::Deserialize; 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 /// Define a custom confirm action
#[derive(Clone, Action, PartialEq, Eq, Deserialize)] #[derive(Clone, Action, PartialEq, Eq, Deserialize)]
#[action(namespace = list, no_json)] #[action(namespace = list, no_json)]

View File

@@ -359,6 +359,7 @@ impl RenderOnce for Button {
.justify_center() .justify_center()
.cursor_default() .cursor_default()
.overflow_hidden() .overflow_hidden()
.refine_style(&self.style)
.map(|this| match self.rounded { .map(|this| match self.rounded {
false => this.rounded(cx.theme().radius), false => this.rounded(cx.theme().radius),
true => this.rounded_full(), 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, _| { .on_mouse_down(gpui::MouseButton::Left, |_, window, _| {
// Avoid focus on mouse down. // Avoid focus on mouse down.
window.prevent_default(); window.prevent_default();
@@ -505,6 +483,28 @@ impl RenderOnce for Button {
}) })
.children(self.children) .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| { .when(self.loading && !self.disabled, |this| {
this.bg(normal_style.bg.opacity(0.8)) this.bg(normal_style.bg.opacity(0.8))
.text_color(normal_style.fg.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 mut bounds = bounds;
let (display_text, text_color) = if is_empty { 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 { } 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 { } else {
(text.clone(), cx.theme().text) (text.clone(), cx.theme().text)
}; };

View File

@@ -328,7 +328,7 @@ impl InputState {
Self { Self {
focus_handle: focus_handle.clone(), focus_handle: focus_handle.clone(),
text: "".into(), text: Rope::default(),
text_wrapper: TextWrapper::new( text_wrapper: TextWrapper::new(
text_style.font(), text_style.font(),
text_style.font_size.to_pixels(window.rem_size()), text_style.font_size.to_pixels(window.rem_size()),
@@ -718,7 +718,7 @@ impl InputState {
/// Set the default value of the input field. /// Set the default value of the input field.
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self { pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
let text: SharedString = value.into(); 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.text_wrapper.set_default_text(&self.text);
self self
} }
@@ -2099,7 +2099,9 @@ impl EntityInputHandler for InputState {
.unwrap_or(self.selected_range.into()); .unwrap_or(self.selected_range.into());
let old_text = self.text.clone(); 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()); 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() { if !self.mask_pattern.is_none() {
let mask_text = self.mask_pattern.mask(&pending_text); 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 = let new_text_len =
(new_text.len() + mask_text.len()).saturating_sub(pending_text.len()); (new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
new_offset = (range.start + new_text_len).min(mask_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.push_history(&old_text, &range, new_text);
self.text_wrapper self.text_wrapper.update(
.update(&self.text, &range, &Rope::from(new_text), false, cx); &self.text,
&range,
&Rope::from_str_small(new_text),
false,
cx,
);
self.selected_range = (new_offset..new_offset).into(); self.selected_range = (new_offset..new_offset).into();
self.ime_marked_range.take(); self.ime_marked_range.take();
self.update_preferred_column(); self.update_preferred_column();
@@ -2154,7 +2161,9 @@ impl EntityInputHandler for InputState {
.unwrap_or(self.selected_range.into()); .unwrap_or(self.selected_range.into());
let old_text = self.text.clone(); 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() { if self.mode.is_single_line() {
let pending_text = self.text.to_string(); let pending_text = self.text.to_string();
@@ -2165,8 +2174,13 @@ impl EntityInputHandler for InputState {
} }
self.push_history(&old_text, &range, new_text); self.push_history(&old_text, &range, new_text);
self.text_wrapper self.text_wrapper.update(
.update(&self.text, &range, &Rope::from(new_text), false, cx); &self.text,
&range,
&Rope::from_str_small(new_text),
false,
cx,
);
if new_text.is_empty() { if new_text.is_empty() {
// Cancel selection, when cancel IME input. // Cancel selection, when cancel IME input.
self.selected_range = (range.start..range.start).into(); self.selected_range = (range.start..range.start).into();

View File

@@ -17,7 +17,6 @@ pub mod checkbox;
pub mod divider; pub mod divider;
pub mod dock_area; pub mod dock_area;
pub mod dropdown; pub mod dropdown;
pub mod emoji_picker;
pub mod history; pub mod history;
pub mod indicator; pub mod indicator;
pub mod input; pub mod input;
@@ -31,7 +30,6 @@ pub mod scroll;
pub mod skeleton; pub mod skeleton;
pub mod switch; pub mod switch;
pub mod tab; pub mod tab;
pub mod text;
pub mod tooltip; pub mod tooltip;
mod event; mod event;
@@ -42,8 +40,6 @@ mod root;
mod styled; mod styled;
mod window_border; mod window_border;
i18n::init!();
/// Initialize the UI module. /// Initialize the UI module.
/// ///
/// This must be called before using any of the UI components. /// 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"; const CONTEXT: &str = "Popover";
actions!( actions!(popover, [Escape]);
popover,
[
/// Action when user presses escape button
Escape
]
);
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
cx.bind_keys([KeyBinding::new("escape", Escape, Some(CONTEXT))]) cx.bind_keys([KeyBinding::new("escape", Escape, Some(CONTEXT))])

View File

@@ -236,16 +236,6 @@ manage_relays:
time: time:
en: "Last activity: %{t}" 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: screening:
ignore: ignore:
en: "Ignore" en: "Ignore"
@@ -378,40 +368,6 @@ compose:
subject_label: subject_label:
en: "Subject:" 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: sidebar:
reload_menu: reload_menu:
en: "Reload" en: "Reload"