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