From 0e756fb6c3c2f3d668c6f4660c1173a4921bef7e Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 4 Feb 2026 08:44:30 +0700 Subject: [PATCH 01/11] . --- Cargo.lock | 166 +++++++++++++++++++++++++++++------------------------ 1 file changed, 92 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8777f3d..f9f00bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -536,12 +536,6 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" -[[package]] -name = "atomic-destructor" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" - [[package]] name = "atomic-waker" version = "1.1.2" @@ -894,9 +888,9 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] @@ -926,9 +920,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calloop" @@ -984,9 +978,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.54" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "jobserver", @@ -1270,7 +1264,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1715,7 +1709,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "proc-macro2", "quote", @@ -2155,9 +2149,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flatbuffers" @@ -2171,9 +2165,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -2638,7 +2632,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2740,7 +2734,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2751,7 +2745,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "anyhow", "gpui", @@ -2973,7 +2967,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "anyhow", "async-compression", @@ -2998,7 +2992,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3067,14 +3061,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -3084,7 +3077,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", - "system-configuration", + "system-configuration 0.7.0", "tokio", "tower-service", "tracing", @@ -3238,7 +3231,7 @@ dependencies = [ "rgb", "tiff", "zune-core 0.5.1", - "zune-jpeg 0.5.11", + "zune-jpeg 0.5.12", ] [[package]] @@ -3749,7 +3742,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "anyhow", "bindgen", @@ -3989,7 +3982,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#07daf26c737a712b74c8edb9a9697929f4de8b21" +source = "git+https://github.com/rust-nostr/nostr#c25727493b52af561ed6b6aca43cf866ef7aeb16" dependencies = [ "aes", "base64", @@ -4014,9 +4007,10 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#07daf26c737a712b74c8edb9a9697929f4de8b21" +source = "git+https://github.com/rust-nostr/nostr#c25727493b52af561ed6b6aca43cf866ef7aeb16" dependencies = [ "async-utility", + "futures-core", "nostr", "nostr-sdk", "tokio", @@ -4026,7 +4020,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#07daf26c737a712b74c8edb9a9697929f4de8b21" +source = "git+https://github.com/rust-nostr/nostr#c25727493b52af561ed6b6aca43cf866ef7aeb16" dependencies = [ "btreecap", "flatbuffers", @@ -4038,7 +4032,7 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#07daf26c737a712b74c8edb9a9697929f4de8b21" +source = "git+https://github.com/rust-nostr/nostr#c25727493b52af561ed6b6aca43cf866ef7aeb16" dependencies = [ "nostr", ] @@ -4046,7 +4040,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#07daf26c737a712b74c8edb9a9697929f4de8b21" +source = "git+https://github.com/rust-nostr/nostr#c25727493b52af561ed6b6aca43cf866ef7aeb16" dependencies = [ "async-utility", "flume", @@ -4060,11 +4054,10 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#07daf26c737a712b74c8edb9a9697929f4de8b21" +source = "git+https://github.com/rust-nostr/nostr#c25727493b52af561ed6b6aca43cf866ef7aeb16" dependencies = [ "async-utility", "async-wsocket", - "atomic-destructor", "futures", "hex", "lru", @@ -4073,6 +4066,7 @@ dependencies = [ "nostr-database", "nostr-gossip", "tokio", + "tokio-stream", "tracing", ] @@ -4594,7 +4588,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "collections", "serde", @@ -5243,16 +5237,16 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "derive_refineable", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -5262,9 +5256,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -5273,9 +5267,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "relay_auth" @@ -5342,7 +5336,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "anyhow", "bytes", @@ -5353,6 +5347,7 @@ dependencies = [ "regex", "serde", "tokio", + "util", "zed-reqwest", ] @@ -5396,7 +5391,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "arrayvec", "log", @@ -5675,7 +5670,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "async-task", "backtrace", @@ -5688,9 +5683,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "indexmap", @@ -5702,9 +5697,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" dependencies = [ "proc-macro2", "quote", @@ -6064,9 +6059,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slotmap" @@ -6278,7 +6273,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "arrayvec", "log", @@ -6477,6 +6472,17 @@ dependencies = [ "system-configuration-sys", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + [[package]] name = "system-configuration-sys" version = "0.6.0" @@ -6760,6 +6766,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-tungstenite" version = "0.26.2" @@ -7240,7 +7258,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "anyhow", "async-fs", @@ -7278,7 +7296,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "perf", "quote", @@ -7641,14 +7659,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.5", + "webpki-root-certs 1.0.6", ] [[package]] name = "webpki-root-certs" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" dependencies = [ "rustls-pki-types", ] @@ -7659,14 +7677,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -8602,7 +8620,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", + "system-configuration 0.6.1", "tokio", "tokio-rustls", "tokio-socks", @@ -8659,18 +8677,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.35" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572" +checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.35" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22" +checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" dependencies = [ "proc-macro2", "quote", @@ -8754,7 +8772,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "anyhow", "chrono", @@ -8764,14 +8782,14 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" dependencies = [ "tracing", "tracing-subscriber", @@ -8782,7 +8800,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99" +source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" [[package]] name = "zune-core" @@ -8816,9 +8834,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2959ca473aae96a14ecedf501d20b3608d2825ba280d5adb57d651721885b0c2" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" dependencies = [ "zune-core 0.5.1", ] -- 2.49.1 From fce4c1bbcd13938781ffda2dd95ef27427f49c1d Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 5 Feb 2026 09:29:47 +0700 Subject: [PATCH 02/11] wip: update nostr sdk --- Cargo.lock | 63 +++--- Cargo.toml | 1 + crates/auto_update/src/lib.rs | 12 +- crates/chat/src/lib.rs | 10 +- crates/chat/src/room.rs | 18 +- crates/common/src/nip96.rs | 9 +- crates/coop/src/dialogs/screening.rs | 20 +- crates/coop/src/panels/messaging_relays.rs | 13 +- crates/coop/src/panels/relay_list.rs | 10 +- crates/device/src/lib.rs | 106 ++++++---- crates/person/src/lib.rs | 20 +- crates/relay_auth/src/lib.rs | 24 +-- crates/settings/src/lib.rs | 20 +- crates/state/Cargo.toml | 1 + crates/state/src/identity.rs | 4 +- crates/state/src/lib.rs | 221 ++++++++++++--------- crates/state/src/signer.rs | 88 ++++++++ crates/ui/src/menu/context_menu.rs | 3 +- 18 files changed, 391 insertions(+), 252 deletions(-) create mode 100644 crates/state/src/signer.rs diff --git a/Cargo.lock b/Cargo.lock index f9f00bf..eb12681 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1264,7 +1264,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1709,7 +1709,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "proc-macro2", "quote", @@ -2632,7 +2632,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2734,7 +2734,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2745,7 +2745,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "anyhow", "gpui", @@ -2967,7 +2967,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "anyhow", "async-compression", @@ -2992,7 +2992,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3742,7 +3742,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "anyhow", "bindgen", @@ -3982,7 +3982,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#c25727493b52af561ed6b6aca43cf866ef7aeb16" +source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" dependencies = [ "aes", "base64", @@ -4007,7 +4007,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#c25727493b52af561ed6b6aca43cf866ef7aeb16" +source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" dependencies = [ "async-utility", "futures-core", @@ -4020,7 +4020,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#c25727493b52af561ed6b6aca43cf866ef7aeb16" +source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" dependencies = [ "btreecap", "flatbuffers", @@ -4032,15 +4032,27 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#c25727493b52af561ed6b6aca43cf866ef7aeb16" +source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" dependencies = [ "nostr", ] +[[package]] +name = "nostr-gossip-memory" +version = "0.44.0" +source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +dependencies = [ + "indexmap", + "lru", + "nostr", + "nostr-gossip", + "tokio", +] + [[package]] name = "nostr-lmdb" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#c25727493b52af561ed6b6aca43cf866ef7aeb16" +source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" dependencies = [ "async-utility", "flume", @@ -4054,7 +4066,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#c25727493b52af561ed6b6aca43cf866ef7aeb16" +source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" dependencies = [ "async-utility", "async-wsocket", @@ -4588,7 +4600,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "collections", "serde", @@ -5237,7 +5249,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "derive_refineable", ] @@ -5336,7 +5348,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "anyhow", "bytes", @@ -5391,7 +5403,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "arrayvec", "log", @@ -5670,7 +5682,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "async-task", "backtrace", @@ -6189,6 +6201,7 @@ dependencies = [ "gpui_tokio", "log", "nostr-connect", + "nostr-gossip-memory", "nostr-lmdb", "nostr-sdk", "petname", @@ -6273,7 +6286,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "arrayvec", "log", @@ -7258,7 +7271,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "anyhow", "async-fs", @@ -7296,7 +7309,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "perf", "quote", @@ -8772,7 +8785,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "anyhow", "chrono", @@ -8789,7 +8802,7 @@ checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" dependencies = [ "tracing", "tracing-subscriber", @@ -8800,7 +8813,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#555c002499a4676bac4c2e4aae2fa11f0a2bbddd" +source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index 8ecb3fe..5d2a2da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" } nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" } nostr-connect = { git = "https://github.com/rust-nostr/nostr" } nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] } +nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" } # Others anyhow = "1.0.44" diff --git a/crates/auto_update/src/lib.rs b/crates/auto_update/src/lib.rs index de57b71..a525f40 100644 --- a/crates/auto_update/src/lib.rs +++ b/crates/auto_update/src/lib.rs @@ -243,12 +243,7 @@ impl AutoUpdater { .author(app_pubkey) .limit(1); - if let Err(e) = client - .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) - .await - { - log::error!("Failed to subscribe to updates: {e}"); - }; + // TODO }) } @@ -285,10 +280,7 @@ impl AutoUpdater { .author(app_pubkey) .ids(ids.clone()); - // Get all files for this release - client - .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) - .await?; + // TODO Ok(ids) } else { diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index ea02060..788547a 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -195,8 +195,8 @@ impl ChatRegistry { let mut notifications = client.notifications(); let mut processed_events = HashSet::new(); - while let Ok(notification) = notifications.recv().await { - let RelayPoolNotification::Message { message, .. } = notification else { + while let Some(notification) = notifications.next().await { + let ClientNotification::Message { message, .. } = notification else { // Skip non-message notifications continue; }; @@ -432,7 +432,7 @@ impl ChatRegistry { let client = nostr.read(cx).client(); cx.background_spawn(async move { - let signer = client.signer().await?; + let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; let contacts = client.database().contacts_public_keys(public_key).await?; @@ -597,8 +597,8 @@ impl ChatRegistry { }; // Try with the user's signer - let user_signer = client.signer().await?; - let unwrapped = UnwrappedGift::from_gift_wrap(&user_signer, gift_wrap).await?; + let user_signer = client.signer().context("Signer not found")?; + let unwrapped = UnwrappedGift::from_gift_wrap(user_signer, gift_wrap).await?; Ok(unwrapped) } diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index e92d4be..ec55efc 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use std::hash::{Hash, Hasher}; use std::time::Duration; -use anyhow::Error; +use anyhow::{Context as AnyhowContext, Error}; use common::EventUtils; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task}; use itertools::Itertools; @@ -325,7 +325,7 @@ impl Room { let id = SubscriptionId::new(format!("room-{}", self.id)); cx.background_spawn(async move { - let signer = client.signer().await?; + let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; // Subscription options @@ -343,7 +343,9 @@ impl Room { // Subscribe to get member's gossip relays client - .subscribe_with_id(id.clone(), filter, Some(opts)) + .subscribe(filter) + .close_on(opts) + .with_id(id.clone()) .await?; } @@ -458,7 +460,7 @@ impl Room { let task = self.members_with_relays(cx); cx.background_spawn(async move { - let signer = client.signer().await?; + let signer = client.signer().context("Signer not found")?; let current_user_relays = current_user_relays.await; let mut members = task.await; @@ -484,10 +486,10 @@ impl Room { // Construct the gift wrap event let event = - EventBuilder::gift_wrap(&signer, &receiver, rumor.clone(), vec![]).await?; + EventBuilder::gift_wrap(signer, &receiver, rumor.clone(), vec![]).await?; // Send the gift wrap event to the messaging relays - match client.send_event_to(relays, &event).await { + match client.send_event(&event).to(&relays).await { Ok(output) => { let id = output.id().to_owned(); let auth = output.failed.iter().any(|(_, s)| s.starts_with("auth-")); @@ -525,7 +527,7 @@ impl Room { // Construct the gift-wrapped event let event = - EventBuilder::gift_wrap(&signer, ¤t_user, rumor.clone(), vec![]).await?; + EventBuilder::gift_wrap(signer, ¤t_user, rumor.clone(), vec![]).await?; // Only send a backup message to current user if sent successfully to others if reports.iter().all(|r| r.is_sent_success()) { @@ -580,7 +582,7 @@ impl Room { if let Some(event) = client.database().event_by_id(id).await? { for url in urls.into_iter() { - let relay = client.pool().relay(url).await?; + let relay = client.relay(url).await?.context("Relay not found")?; let id = relay.send_event(&event).await?; let resent: Output = Output { diff --git a/crates/common/src/nip96.rs b/crates/common/src/nip96.rs index 10d4e6f..c40cc89 100644 --- a/crates/common/src/nip96.rs +++ b/crates/common/src/nip96.rs @@ -72,11 +72,10 @@ pub async fn nip96_upload( let json: Value = res.json().await?; let config = nip96::ServerConfig::from_json(json.to_string())?; - let signer = if client.has_signer().await { - client.signer().await? - } else { - Keys::generate().into_nostr_signer() - }; + let signer = client + .signer() + .cloned() + .unwrap_or(Keys::generate().into_nostr_signer()); let url = upload(&signer, &config, file, None).await?; diff --git a/crates/coop/src/dialogs/screening.rs b/crates/coop/src/dialogs/screening.rs index 31b7629..4ca6ccf 100644 --- a/crates/coop/src/dialogs/screening.rs +++ b/crates/coop/src/dialogs/screening.rs @@ -1,6 +1,7 @@ +use std::collections::HashMap; use std::time::Duration; -use anyhow::Error; +use anyhow::{Context as AnyhowContext, Error}; use common::{shorten_pubkey, RenderedProfile, RenderedTimestamp, BOOTSTRAP_RELAYS}; use gpui::prelude::FluentBuilder; use gpui::{ @@ -45,7 +46,7 @@ impl Screening { let contact_check: Task), Error>> = cx.background_spawn({ let client = nostr.read(cx).client(); async move { - let signer = client.signer().await?; + let signer = client.signer().context("Signer not found")?; let signer_pubkey = signer.get_public_key().await?; // Check if user is in contact list @@ -74,8 +75,15 @@ impl Screening { let filter = Filter::new().author(public_key).limit(1); let mut activity: Option = None; + // Construct target for subscription + let target = BOOTSTRAP_RELAYS + .into_iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + if let Ok(mut stream) = client - .stream_events_from(BOOTSTRAP_RELAYS, filter, Duration::from_secs(2)) + .stream_events(target) + .timeout(Duration::from_secs(2)) .await { while let Some((_url, event)) = stream.next().await { @@ -162,12 +170,12 @@ impl Screening { let public_key = self.profile.public_key(); let task: Task> = cx.background_spawn(async move { - let signer = client.signer().await?; let tag = Tag::public_key_report(public_key, Report::Impersonation); - let event = EventBuilder::report(vec![tag], "").sign(&signer).await?; + let builder = EventBuilder::report(vec![tag], ""); + let event = client.sign_event_builder(builder).await?; // Send the report to the public relays - client.send_event_to(BOOTSTRAP_RELAYS, &event).await?; + client.send_event(&event).to(BOOTSTRAP_RELAYS).await?; Ok(()) }); diff --git a/crates/coop/src/panels/messaging_relays.rs b/crates/coop/src/panels/messaging_relays.rs index d1a5f4a..6388d7c 100644 --- a/crates/coop/src/panels/messaging_relays.rs +++ b/crates/coop/src/panels/messaging_relays.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::time::Duration; -use anyhow::{anyhow, Error}; +use anyhow::{anyhow, Context as AnyhowContext, Error}; use dock::panel::{Panel, PanelEvent}; use gpui::prelude::FluentBuilder; use gpui::{ @@ -89,7 +89,7 @@ impl MessagingRelayPanel { } async fn load(client: &Client) -> Result, Error> { - let signer = client.signer().await?; + let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; let filter = Filter::new() @@ -162,20 +162,17 @@ impl MessagingRelayPanel { let task: Task> = cx.background_spawn(async move { let urls = write_relays.await; - let signer = client.signer().await?; let tags: Vec = relays .iter() .map(|relay| Tag::relay(relay.clone())) .collect(); - let event = EventBuilder::new(Kind::InboxRelays, "") - .tags(tags) - .sign(&signer) - .await?; + let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags); + let event = client.sign_event_builder(builder).await?; // Set messaging relays - client.send_event_to(urls, &event).await?; + client.send_event(&event).to(urls).await?; // Connect to messaging relays for relay in relays.iter() { diff --git a/crates/coop/src/panels/relay_list.rs b/crates/coop/src/panels/relay_list.rs index f7aef9c..262556b 100644 --- a/crates/coop/src/panels/relay_list.rs +++ b/crates/coop/src/panels/relay_list.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::time::Duration; -use anyhow::{anyhow, Error}; +use anyhow::{anyhow, Context as AnyhowContext, Error}; use common::BOOTSTRAP_RELAYS; use dock::panel::{Panel, PanelEvent}; use gpui::prelude::FluentBuilder; @@ -96,7 +96,7 @@ impl RelayListPanel { } async fn load(client: &Client) -> Result)>, Error> { - let signer = client.signer().await?; + let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; let filter = Filter::new() @@ -167,11 +167,11 @@ impl RelayListPanel { let relays = self.relays.clone(); let task: Task> = cx.background_spawn(async move { - let signer = client.signer().await?; - let event = EventBuilder::relay_list(relays).sign(&signer).await?; + let builder = EventBuilder::relay_list(relays); + let event = client.sign_event_builder(builder).await?; // Set relay list for current user - client.send_event_to(BOOTSTRAP_RELAYS, &event).await?; + client.send_event(&event).to(BOOTSTRAP_RELAYS).await?; Ok(()) }); diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index f7ac1ee..e4d2ce8 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::Duration; @@ -130,8 +130,8 @@ impl DeviceRegistry { let mut notifications = client.notifications(); let mut processed_events = HashSet::new(); - while let Ok(notification) = notifications.recv().await { - if let RelayPoolNotification::Message { + while let Some(notification) = notifications.next().await { + if let ClientNotification::Message { message: RelayMessage::Event { event, .. }, .. } = notification @@ -162,7 +162,7 @@ impl DeviceRegistry { /// Verify the author of an event async fn verify_author(client: &Client, event: &Event) -> bool { - if let Ok(signer) = client.signer().await { + if let Some(signer) = client.signer() { if let Ok(public_key) = signer.get_public_key().await { return public_key == event.pubkey; } @@ -172,7 +172,7 @@ impl DeviceRegistry { /// Encrypt and store device keys in the local database. async fn set_keys(client: &Client, secret: &str) -> Result<(), Error> { - let signer = client.signer().await?; + let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; // Encrypt the value @@ -193,7 +193,7 @@ impl DeviceRegistry { /// Get device keys from the local database. async fn get_keys(client: &Client) -> Result { - let signer = client.signer().await?; + let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; let filter = Filter::new() @@ -278,10 +278,13 @@ impl DeviceRegistry { let filter = Filter::new().kind(Kind::GiftWrap).pubkey(pkey); let id = SubscriptionId::new(DEVICE_GIFTWRAP); - if let Err(e) = client - .subscribe_with_id_to(&urls, id, vec![filter], None) - .await - { + // Construct target for subscription + let target = urls + .iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + + if let Err(e) = client.subscribe(target).with_id(id).await { log::error!("Failed to subscribe to gift wrap events: {e}"); } } @@ -291,10 +294,13 @@ impl DeviceRegistry { let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let id = SubscriptionId::new(USER_GIFTWRAP); - if let Err(e) = client - .subscribe_with_id_to(urls, id, vec![filter], None) - .await - { + // Construct target for subscription + let target = urls + .iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + + if let Err(e) = client.subscribe(target).with_id(id).await { log::error!("Failed to subscribe to gift wrap events: {e}"); } }) @@ -318,8 +324,15 @@ impl DeviceRegistry { .author(public_key) .limit(1); + // Construct target for subscription + let target = urls + .iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + let mut stream = client - .stream_events_from(&urls, vec![filter], Duration::from_secs(TIMEOUT)) + .stream_events(target) + .timeout(Duration::from_secs(TIMEOUT)) .await?; while let Some((_url, res)) = stream.next().await { @@ -368,20 +381,17 @@ impl DeviceRegistry { let n = keys.public_key(); let task: Task> = cx.background_spawn(async move { - let signer = client.signer().await?; let urls = write_relays.await; // Construct an announcement event - let event = EventBuilder::new(Kind::Custom(10044), "") - .tags(vec![ - Tag::custom(TagKind::custom("n"), vec![n]), - Tag::client(app_name()), - ]) - .sign(&signer) - .await?; + let builder = EventBuilder::new(Kind::Custom(10044), "").tags(vec![ + Tag::custom(TagKind::custom("n"), vec![n]), + Tag::client(app_name()), + ]); + let event = client.sign_event_builder(builder).await?; // Publish announcement - client.send_event_to(&urls, &event).await?; + client.send_event(&event).to(urls).await?; // Save device keys to the database Self::set_keys(&client, &secret).await?; @@ -461,8 +471,14 @@ impl DeviceRegistry { .author(public_key) .since(Timestamp::now()); + // Construct target for subscription + let target = urls + .iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + // Subscribe to the device key requests on user's write relays - client.subscribe_to(&urls, vec![filter], None).await?; + client.subscribe(target).await?; Ok(()) }); @@ -487,8 +503,14 @@ impl DeviceRegistry { .author(public_key) .since(Timestamp::now()); + // Construct target for subscription + let target = urls + .iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + // Subscribe to the device key requests on user's write relays - client.subscribe_to(&urls, vec![filter], None).await?; + client.subscribe(target).await?; Ok(()) }); @@ -508,7 +530,7 @@ impl DeviceRegistry { let app_pubkey = app_keys.public_key(); let task: Task, Error>> = cx.background_spawn(async move { - let signer = client.signer().await?; + let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; let filter = Filter::new() @@ -538,16 +560,14 @@ impl DeviceRegistry { let urls = write_relays.await; // Construct an event for device key request - let event = EventBuilder::new(Kind::Custom(4454), "") - .tags(vec![ - Tag::client(app_name()), - Tag::custom(TagKind::custom("P"), vec![app_pubkey]), - ]) - .sign(&signer) - .await?; + let builder = EventBuilder::new(Kind::Custom(4454), "").tags(vec![ + Tag::client(app_name()), + Tag::custom(TagKind::custom("P"), vec![app_pubkey]), + ]); + let event = client.sign_event_builder(builder).await?; // Send the event to write relays - client.send_event_to(&urls, &event).await?; + client.send_event(&event).to(urls).await?; Ok(None) } @@ -625,7 +645,7 @@ impl DeviceRegistry { let task: Task> = cx.background_spawn(async move { let urls = write_relays.await; - let signer = client.signer().await?; + let signer = client.signer().context("Signer not found")?; // Get device keys let keys = Self::get_keys(&client).await?; @@ -646,16 +666,14 @@ impl DeviceRegistry { // // P tag: the current device's public key // p tag: the requester's public key - let event = EventBuilder::new(Kind::Custom(4455), payload) - .tags(vec![ - Tag::custom(TagKind::custom("P"), vec![keys.public_key()]), - Tag::public_key(target), - ]) - .sign(&signer) - .await?; + let builder = EventBuilder::new(Kind::Custom(4455), payload).tags(vec![ + Tag::custom(TagKind::custom("P"), vec![keys.public_key()]), + Tag::public_key(target), + ]); + let event = client.sign_event_builder(builder).await?; // Send the response event to the user's relay list - client.send_event_to(&urls, &event).await?; + client.send_event(&event).to(urls).await?; Ok(()) }); diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs index ab20aec..3e1ef3e 100644 --- a/crates/person/src/lib.rs +++ b/crates/person/src/lib.rs @@ -139,20 +139,14 @@ impl PersonRegistry { /// Handle nostr notifications async fn handle_notifications(client: &Client, tx: &flume::Sender) { let mut notifications = client.notifications(); - let mut processed_events = HashSet::new(); - while let Ok(notification) = notifications.recv().await { - let RelayPoolNotification::Message { message, .. } = notification else { + while let Some(notification) = notifications.next().await { + let ClientNotification::Message { message, .. } = notification else { // Skip if the notification is not a message continue; }; if let RelayMessage::Event { event, .. } = message { - if !processed_events.insert(event.id) { - // Skip if the event has already been processed - continue; - } - match event.kind { Kind::Metadata => { let metadata = Metadata::from_json(&event.content).unwrap_or_default(); @@ -230,9 +224,13 @@ impl PersonRegistry { .authors(authors) .limit(limit); - client - .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) - .await?; + // Construct target for subscription + let target = BOOTSTRAP_RELAYS + .into_iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + + client.subscribe(target).close_on(opts).await?; Ok(()) } diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index 99aa2f1..6a88e0c 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -4,7 +4,7 @@ use std::collections::HashSet; use std::hash::{Hash, Hasher}; use std::rc::Rc; -use anyhow::{anyhow, Error}; +use anyhow::{anyhow, Context as AnyhowContext, Error}; use gpui::{ App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled, Subscription, Task, Window, @@ -137,8 +137,8 @@ impl RelayAuth { async fn handle_notifications(client: &Client, tx: &flume::Sender) { let mut notifications = client.notifications(); - while let Ok(notification) = notifications.recv().await { - if let RelayPoolNotification::Message { + while let Some(notification) = notifications.next().await { + if let ClientNotification::Message { message: RelayMessage::Auth { challenge }, relay_url, } = notification @@ -184,34 +184,33 @@ impl RelayAuth { let url_clone = url.clone(); let task: Task> = cx.background_spawn(async move { - let signer = client.signer().await?; - // Construct event - let event: Event = EventBuilder::auth(challenge_clone, url_clone.clone()) - .sign(&signer) - .await?; + let builder = EventBuilder::auth(challenge_clone, url_clone.clone()); + let event = client.sign_event_builder(builder).await?; // Get the event ID let id = event.id; // Get the relay - let relay = client.pool().relay(url_clone).await?; + let relay = client.relay(url_clone).await?.context("Relay not found")?; let relay_url = relay.url(); // Subscribe to notifications let mut notifications = relay.notifications(); // Send the AUTH message - relay.send_msg(ClientMessage::Auth(Cow::Borrowed(&event)))?; + relay + .send_msg(ClientMessage::Auth(Cow::Borrowed(&event))) + .await?; - while let Ok(notification) = notifications.recv().await { + while let Some(notification) = notifications.next().await { match notification { RelayNotification::Message { message: RelayMessage::Ok { event_id, .. }, } => { if id == event_id { // Re-subscribe to previous subscription - relay.resubscribe().await?; + // relay.resubscribe().await?; // Get all pending events that need to be resent let mut tracker = tracker().write().await; @@ -228,7 +227,6 @@ impl RelayAuth { } } RelayNotification::AuthenticationFailed => break, - RelayNotification::Shutdown => break, _ => {} } } diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 013880c..9781b8d 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -136,7 +136,7 @@ impl AppSettings { } fn new(cx: &mut Context) -> Self { - let load_settings = Self::get_from_database(false, cx); + let load_settings = Self::get_from_database(cx); let mut tasks = smallvec![]; let mut subscriptions = smallvec![]; @@ -169,12 +169,10 @@ impl AppSettings { } /// Get settings from the database - /// - /// If `current_user` is true, the settings will be retrieved for current user. - /// Otherwise, Coop will load the latest settings from the database. - fn get_from_database(current_user: bool, cx: &App) -> Task> { + fn get_from_database(cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let current_user = nostr.read(cx).identity().read(cx).public_key; cx.background_spawn(async move { // Construct a filter to get the latest settings @@ -183,10 +181,7 @@ impl AppSettings { .identifier(SETTINGS_IDENTIFIER) .limit(1); - if current_user { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - + if let Some(public_key) = current_user { // Push author to the filter filter = filter.author(public_key); } @@ -201,7 +196,7 @@ impl AppSettings { /// Load settings pub fn load(&mut self, cx: &mut Context) { - let task = Self::get_from_database(true, cx); + let task = Self::get_from_database(cx); self._tasks.push( // Run task in the background @@ -221,18 +216,17 @@ impl AppSettings { pub fn save(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let public_key = nostr.read(cx).identity().read(cx).public_key(); if let Ok(content) = serde_json::to_string(&self.values) { let task: Task> = cx.background_spawn(async move { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - let event = EventBuilder::new(Kind::ApplicationSpecificData, content) .tag(Tag::identifier(SETTINGS_IDENTIFIER)) .build(public_key) .sign(&Keys::generate()) .await?; + // Save event to the local database without sending to relays client.database().save_event(&event).await?; Ok(()) diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index abeea36..2d04fc2 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -10,6 +10,7 @@ common = { path = "../common" } nostr-sdk.workspace = true nostr-lmdb.workspace = true nostr-connect.workspace = true +nostr-gossip-memory.workspace = true gpui.workspace = true gpui_tokio.workspace = true diff --git a/crates/state/src/identity.rs b/crates/state/src/identity.rs index 81a652b..31bb690 100644 --- a/crates/state/src/identity.rs +++ b/crates/state/src/identity.rs @@ -15,9 +15,9 @@ impl RelayState { } /// Identity -#[derive(Debug, Clone, Default)] +#[derive(Debug, Default)] pub struct Identity { - /// The public key of the account + /// Signer's public key pub public_key: Option, /// Whether the identity is owned by the user diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 6e34fe4..5fb7df6 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,5 +1,6 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::os::unix::fs::PermissionsExt; +use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, Error}; @@ -14,12 +15,14 @@ mod event; mod gossip; mod identity; mod nip05; +mod signer; pub use device::*; pub use event::*; pub use gossip::*; pub use identity::*; pub use nip05::*; +pub use signer::*; use crate::identity::Identity; @@ -68,6 +71,9 @@ pub struct NostrRegistry { /// Nostr client client: Client, + /// Nostr signer + signer: Arc, + /// App keys /// /// Used for Nostr Connect and NIP-4e operations @@ -108,14 +114,6 @@ impl NostrRegistry { .install_default() .ok(); - // Construct the nostr client options - let opts = ClientOptions::new() - .automatic_authentication(false) - .verify_subscriptions(false) - .sleep_when_idle(SleepWhenIdle::Enabled { - timeout: Duration::from_secs(600), - }); - // Construct the lmdb let lmdb = cx.foreground_executor().block_on(async move { NostrLmdb::open(config_dir().join("nostr")) @@ -123,9 +121,23 @@ impl NostrRegistry { .expect("Failed to initialize database") }); + // Construct the nostr signer + let keys = Keys::generate(); + let signer = Arc::new(CoopSigner::new(keys)); + // Construct the nostr client - let client = ClientBuilder::default().database(lmdb).opts(opts).build(); - let _ = tracker(); + let client = ClientBuilder::default() + .signer(signer.clone()) + .database(lmdb) + .automatic_authentication(false) + .verify_subscriptions(false) + .sleep_when_idle(SleepWhenIdle::Enabled { + timeout: Duration::from_secs(600), + }) + .build(); + + // Construct the event tracker + let _tracker = tracker(); // Get the app keys let app_keys = Self::create_or_init_app_keys().unwrap(); @@ -135,7 +147,7 @@ impl NostrRegistry { let async_gossip = gossip.downgrade(); // Construct the identity entity - let identity = cx.new(|_| Identity::default()); + let identity = cx.new(|_| Identity::new()); // Channel for communication between nostr and gpui let (tx, rx) = flume::bounded::(2048); @@ -207,6 +219,7 @@ impl NostrRegistry { Self { client, + signer, app_keys, identity, gossip, @@ -239,8 +252,8 @@ impl NostrRegistry { let mut notifications = client.notifications(); let mut processed_events = HashSet::new(); - while let Ok(notification) = notifications.recv().await { - if let RelayPoolNotification::Message { message, relay_url } = notification { + while let Some(notification) = notifications.next().await { + if let ClientNotification::Message { message, relay_url } = notification { match message { RelayMessage::Event { event, @@ -325,9 +338,13 @@ impl NostrRegistry { .author(event.pubkey) .limit(1); - client - .subscribe_to(write_relays, vec![inbox, announcement], Some(opts)) - .await?; + // Construct target for subscription + let target = write_relays + .into_iter() + .map(|relay| (relay, vec![inbox.clone(), announcement.clone()])) + .collect::>(); + + client.subscribe(target).close_on(opts).await?; Ok(()) } @@ -433,21 +450,21 @@ impl NostrRegistry { } /// Set the signer for the nostr client and verify the public key - pub fn set_signer(&mut self, signer: T, owned: bool, cx: &mut Context) + pub fn set_signer(&mut self, new: T, owned: bool, cx: &mut Context) where T: NostrSigner + 'static, { - let client = self.client(); let identity = self.identity.downgrade(); + let signer = self.signer.clone(); // Create a task to update the signer and verify the public key let task: Task> = cx.background_spawn(async move { // Update signer - client.set_signer(signer).await; + signer.switch(new).await; // Verify signer - let signer = client.signer().await?; let public_key = signer.get_public_key().await?; + log::info!("test: {public_key:?}"); Ok(public_key) }); @@ -471,31 +488,6 @@ impl NostrRegistry { })); } - /// Unset the current signer - pub fn unset_signer(&mut self, cx: &mut Context) { - let client = self.client(); - let async_identity = self.identity.downgrade(); - - self.tasks.push(cx.spawn(async move |_this, cx| { - // Unset the signer from nostr client - cx.background_executor() - .await_on_background(async move { - client.unset_signer().await; - }) - .await; - - // Unset the current identity - async_identity - .update(cx, |this, cx| { - this.unset_public_key(); - cx.notify(); - }) - .ok(); - - Ok(()) - })); - } - // Get relay list for current user fn get_relay_list(&mut self, cx: &mut Context) { let client = self.client(); @@ -508,8 +500,15 @@ impl NostrRegistry { .author(public_key) .limit(1); + // Construct targets for subscription + let target = BOOTSTRAP_RELAYS + .into_iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + let mut stream = client - .stream_events_from(BOOTSTRAP_RELAYS, vec![filter], Duration::from_secs(TIMEOUT)) + .stream_events(target) + .timeout(Duration::from_secs(TIMEOUT)) .await?; while let Some((_url, res)) = stream.next().await { @@ -523,10 +522,14 @@ impl NostrRegistry { .author(public_key) .since(Timestamp::now()); + // Construct targets for subscription + let target = BOOTSTRAP_RELAYS + .into_iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + // Subscribe to the relay list events - client - .subscribe_to(BOOTSTRAP_RELAYS, vec![filter], None) - .await?; + client.subscribe(target).await?; return Ok(RelayState::Set); } @@ -565,8 +568,7 @@ impl NostrRegistry { let write_relays = self.write_relays(&public_key, cx); let task: Task> = cx.background_spawn(async move { - let mut urls = vec![]; - urls.extend(write_relays.await); + let mut urls = write_relays.await; urls.extend( BOOTSTRAP_RELAYS .iter() @@ -590,9 +592,13 @@ impl NostrRegistry { .limit(1) .author(public_key); - client - .subscribe_to(urls, vec![metadata, contact_list], Some(opts)) - .await?; + // Construct targets for subscription + let target = urls + .into_iter() + .map(|relay| (relay, vec![metadata.clone(), contact_list.clone()])) + .collect::>(); + + client.subscribe(target).close_on(opts).await?; Ok(()) }); @@ -616,9 +622,16 @@ impl NostrRegistry { .author(public_key) .limit(1); + // Construct targets for subscription + let target = urls + .iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + // Stream events from the write relays let mut stream = client - .stream_events_from(&urls, vec![filter], Duration::from_secs(TIMEOUT)) + .stream_events(target) + .timeout(Duration::from_secs(TIMEOUT)) .await?; while let Some((_url, res)) = stream.next().await { @@ -632,8 +645,14 @@ impl NostrRegistry { .author(public_key) .since(Timestamp::now()); + // Construct targets for subscription + let target = urls + .iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + // Subscribe to the relay list events - client.subscribe_to(&urls, vec![filter], None).await?; + client.subscribe(target).await?; return Ok(RelayState::Set); } @@ -687,13 +706,13 @@ impl NostrRegistry { cx.background_spawn(async move { let urls = write_relays.await; - let signer = client.signer().await?; - // Sign the new metadata event - let event = EventBuilder::metadata(&metadata).sign(&signer).await?; + // Build and sign the metadata event + let builder = EventBuilder::metadata(&metadata); + let event = client.sign_event_builder(builder).await?; // Send event to user's write relayss - client.send_event_to(urls, &event).await?; + client.send_event(&event).to(urls).await?; Ok(()) }) @@ -728,9 +747,6 @@ impl NostrRegistry { /// Create a new identity fn create_identity(&mut self, cx: &mut Context) { - let client = self.client(); - - // Generate new keys let keys = Keys::generate(); // Get write credential task @@ -743,23 +759,25 @@ impl NostrRegistry { // Update the signer self.set_signer(keys, false, cx); - // Spawn a task to set metadata and write the credentials + // Generate a unique name and avatar for the identity + let name = petname::petname(2, "-").unwrap_or("Cooper".to_string()); + let avatar = Url::parse(DEFAULT_AVATAR).unwrap(); + + // Construct metadata for the identity + let metadata = Metadata::new() + .display_name(&name) + .name(&name) + .picture(avatar); + + // Update user's metadata + let task = self.set_metadata(&metadata, cx); + + // Spawn a task to write the credentials cx.background_spawn(async move { - let name = petname::petname(2, "-").unwrap_or("Cooper".to_string()); - let avatar = Url::parse(DEFAULT_AVATAR).unwrap(); - - // Construct metadata for the identity - let metadata = Metadata::new() - .display_name(&name) - .name(&name) - .picture(avatar); - - // Set metadata for the identity - if let Err(e) = client.set_metadata(&metadata).await { - log::error!("Failed to set metadata: {}", e); + if let Err(e) = task.await { + log::error!("Failed to update metadata: {}", e); } - // Write the credentials if let Err(e) = write_credential.await { log::error!("Failed to write credentials: {}", e); } @@ -874,10 +892,13 @@ impl NostrRegistry { .author(public_key) .limit(1); - // Subscribe to bootstrap relays - client - .subscribe_to(BOOTSTRAP_RELAYS, vec![filter], Some(opts)) - .await?; + // Construct target for subscription + let target = BOOTSTRAP_RELAYS + .into_iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + + client.subscribe(target).close_on(opts).await?; Ok(public_key) }) @@ -897,9 +918,16 @@ impl NostrRegistry { .kind(Kind::Metadata) .limit(FIND_LIMIT); + // Construct target for subscription + let target = SEARCH_RELAYS + .into_iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + // Stream events from the search relays let mut stream = client - .stream_events_from(SEARCH_RELAYS, vec![filter], Duration::from_secs(3)) + .stream_events(target) + .timeout(Duration::from_secs(TIMEOUT)) .await?; // Collect the results @@ -923,28 +951,31 @@ impl NostrRegistry { let query = query.to_string(); cx.background_spawn(async move { - let signer = client.signer().await?; - // Construct a vertex request event - let event = EventBuilder::new(Kind::Custom(5315), "") - .tags(vec![ - Tag::custom(TagKind::custom("param"), vec!["search", &query]), - Tag::custom(TagKind::custom("param"), vec!["limit", "10"]), - ]) - .sign(&signer) - .await?; + let builder = EventBuilder::new(Kind::Custom(5315), "").tags(vec![ + Tag::custom(TagKind::custom("param"), vec!["search", &query]), + Tag::custom(TagKind::custom("param"), vec!["limit", "10"]), + ]); + let event = client.sign_event_builder(builder).await?; // Send the event to vertex relays - let output = client.send_event_to(WOT_RELAYS, &event).await?; + let output = client.send_event(&event).to(WOT_RELAYS).await?; // Construct a filter to get the response or error from vertex let filter = Filter::new() .kinds(vec![Kind::Custom(6315), Kind::Custom(7000)]) .event(output.id().to_owned()); - // Stream events from the search relays + // Construct target for subscription + let target = WOT_RELAYS + .into_iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + + // Stream events from the wot relays let mut stream = client - .stream_events_from(WOT_RELAYS, vec![filter], Duration::from_secs(3)) + .stream_events(target) + .timeout(Duration::from_secs(TIMEOUT)) .await?; while let Some((_url, res)) = stream.next().await { diff --git a/crates/state/src/signer.rs b/crates/state/src/signer.rs new file mode 100644 index 0000000..618df95 --- /dev/null +++ b/crates/state/src/signer.rs @@ -0,0 +1,88 @@ +use std::borrow::Cow; +use std::sync::Arc; + +use nostr_sdk::prelude::*; +use smol::lock::RwLock; + +#[derive(Debug)] +pub struct CoopSigner { + signer: RwLock>, +} + +impl CoopSigner { + pub fn new(signer: T) -> Self + where + T: IntoNostrSigner, + { + Self { + signer: RwLock::new(signer.into_nostr_signer()), + } + } + + async fn get(&self) -> Arc { + self.signer.read().await.clone() + } + + pub async fn switch(&self, new: T) + where + T: IntoNostrSigner, + { + let mut signer = self.signer.write().await; + *signer = new.into_nostr_signer(); + } +} + +impl NostrSigner for CoopSigner { + fn backend(&self) -> SignerBackend { + SignerBackend::Custom(Cow::Borrowed("custom")) + } + + fn get_public_key(&self) -> BoxedFuture> { + Box::pin(async move { Ok(self.get().await.get_public_key().await?) }) + } + + fn sign_event( + &self, + unsigned: UnsignedEvent, + ) -> BoxedFuture> { + Box::pin(async move { Ok(self.get().await.sign_event(unsigned).await?) }) + } + + fn nip04_encrypt<'a>( + &'a self, + public_key: &'a PublicKey, + content: &'a str, + ) -> BoxedFuture<'a, std::result::Result> { + Box::pin(async move { Ok(self.get().await.nip04_encrypt(public_key, content).await?) }) + } + + fn nip04_decrypt<'a>( + &'a self, + public_key: &'a PublicKey, + encrypted_content: &'a str, + ) -> BoxedFuture<'a, std::result::Result> { + Box::pin(async move { + Ok(self + .get() + .await + .nip04_decrypt(public_key, encrypted_content) + .await?) + }) + } + + fn nip44_encrypt<'a>( + &'a self, + public_key: &'a PublicKey, + content: &'a str, + ) -> BoxedFuture<'a, std::result::Result> { + Box::pin(async move { Ok(self.get().await.nip44_encrypt(public_key, content).await?) }) + } + + fn nip44_decrypt<'a>( + &'a self, + public_key: &'a PublicKey, + payload: &'a str, + ) -> BoxedFuture<'a, std::result::Result> { + Box::pin(async move { Ok(self.get().await.nip44_decrypt(public_key, payload).await?) }) + } +} diff --git a/crates/ui/src/menu/context_menu.rs b/crates/ui/src/menu/context_menu.rs index a371e84..2984adc 100644 --- a/crates/ui/src/menu/context_menu.rs +++ b/crates/ui/src/menu/context_menu.rs @@ -246,8 +246,7 @@ impl Element for ContextMenu { let menu = PopupMenu::build(window, cx, |menu, window, cx| { (builder)(menu, window, cx) - }) - .into_element(); + }); let _subscription = window.subscribe(&menu, cx, { let shared_state = shared_state.clone(); -- 2.49.1 From 253d04f9881c86bc59ba1c4d7fb6845f32ecf5cb Mon Sep 17 00:00:00 2001 From: reya Date: Fri, 6 Feb 2026 13:25:34 +0700 Subject: [PATCH 03/11] . --- Cargo.lock | 406 +++++++++++- Cargo.toml | 2 +- crates/chat/src/lib.rs | 12 +- crates/chat/src/room.rs | 184 ++---- crates/chat_ui/src/lib.rs | 101 +-- crates/coop/src/command_bar.rs | 26 +- crates/coop/src/panels/greeter.rs | 83 +-- crates/coop/src/panels/messaging_relays.rs | 25 +- crates/coop/src/panels/profile.rs | 26 +- crates/coop/src/workspace.rs | 58 +- crates/device/src/lib.rs | 133 ++-- crates/relay_auth/src/lib.rs | 36 +- crates/settings/src/lib.rs | 22 +- crates/state/Cargo.toml | 2 +- crates/state/src/identity.rs | 101 --- crates/state/src/lib.rs | 699 ++++++++++----------- crates/state/src/signer.rs | 37 +- 17 files changed, 1081 insertions(+), 872 deletions(-) delete mode 100644 crates/state/src/identity.rs diff --git a/Cargo.lock b/Cargo.lock index eb12681..c4afe4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,12 @@ dependencies = [ "equator", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -530,6 +536,15 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic" version = "0.5.3" @@ -1349,6 +1364,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-random" version = "0.1.18" @@ -1599,6 +1620,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -1693,6 +1729,17 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -1741,6 +1788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -1845,6 +1893,12 @@ dependencies = [ "ui", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -1904,6 +1958,9 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "embed-resource" @@ -2008,7 +2065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2021,6 +2078,17 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "euclid" version = "0.22.13" @@ -2409,6 +2477,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -2811,6 +2890,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -2820,6 +2901,15 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.4.1" @@ -3538,6 +3628,17 @@ dependencies = [ "redox_syscall 0.7.0", ] +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linicon" version = "2.3.0" @@ -3982,7 +4083,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ "aes", "base64", @@ -4007,7 +4108,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ "async-utility", "futures-core", @@ -4020,7 +4121,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ "btreecap", "flatbuffers", @@ -4032,27 +4133,26 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ "nostr", ] [[package]] -name = "nostr-gossip-memory" +name = "nostr-gossip-sqlite" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ - "indexmap", - "lru", "nostr", "nostr-gossip", + "sqlx", "tokio", ] [[package]] name = "nostr-lmdb" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ "async-utility", "flume", @@ -4066,7 +4166,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ "async-utility", "async-wsocket", @@ -4097,7 +4197,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4591,6 +4691,15 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -4727,6 +4836,27 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -5007,7 +5137,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -5421,6 +5551,26 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rust-embed" version = "8.11.0" @@ -5516,7 +5666,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6029,6 +6179,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -6089,6 +6249,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smol" @@ -6150,6 +6313,204 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener 5.4.1", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.114", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.114", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags 2.10.0", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags 2.10.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -6201,7 +6562,7 @@ dependencies = [ "gpui_tokio", "log", "nostr-connect", - "nostr-gossip-memory", + "nostr-gossip-sqlite", "nostr-lmdb", "nostr-sdk", "petname", @@ -6228,6 +6589,17 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -6546,7 +6918,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7753,7 +8125,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5d2a2da..251dbae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" } nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" } nostr-connect = { git = "https://github.com/rust-nostr/nostr" } nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] } -nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" } +nostr-gossip-sqlite = { git = "https://github.com/rust-nostr/nostr" } # Others anyhow = "1.0.44" diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 788547a..a08e146 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -98,7 +98,7 @@ impl ChatRegistry { /// Create a new chat registry instance fn new(cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); - let identity = nostr.read(cx).identity(); + let nip17_state = nostr.read(cx).nip17_state(); let device = DeviceRegistry::global(cx); let device_signer = device.read(cx).device_signer.clone(); @@ -114,8 +114,8 @@ impl ChatRegistry { subscriptions.push( // Observe the identity - cx.observe(&identity, |this, state, cx| { - if state.read(cx).messaging_relays_state() == RelayState::Set { + cx.observe(&nip17_state, |this, state, cx| { + if state.read(cx) == &RelayState::Configured { // Handle nostr notifications this.handle_notifications(cx); // Track unwrapping progress @@ -536,9 +536,9 @@ impl ChatRegistry { } // Set this room is ongoing if the new message is from current user - if author == nostr.read(cx).identity().read(cx).public_key() { - this.set_ongoing(cx); - } + // if author == nostr.read(cx).identity().read(cx).public_key() { + // this.set_ongoing(cx); + // } // Emit the new message to the room this.emit_message(message, cx); diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index ec55efc..bdea83e 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -216,28 +216,6 @@ impl Room { self.members.clone() } - /// Returns the members of the room with their messaging relays - pub fn members_with_relays(&self, cx: &App) -> Task)>> { - let nostr = NostrRegistry::global(cx); - let mut tasks = vec![]; - - for member in self.members.iter() { - let task = nostr.read(cx).messaging_relays(member, cx); - tasks.push((*member, task)); - } - - cx.background_spawn(async move { - let mut results = vec![]; - - for (public_key, task) in tasks.into_iter() { - let urls = task.await; - results.push((public_key, urls)); - } - - results - }) - } - /// Checks if the room has more than two members (group) pub fn is_group(&self) -> bool { self.members.len() > 2 @@ -266,17 +244,7 @@ impl Room { /// Display member is always different from the current user. pub fn display_member(&self, cx: &App) -> Person { let persons = PersonRegistry::global(cx); - let nostr = NostrRegistry::global(cx); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - - let target_member = self - .members - .iter() - .find(|&member| member != &public_key) - .or_else(|| self.members.first()) - .expect("Room should have at least one member"); - - persons.read(cx).get(target_member, cx) + persons.read(cx).get(&self.members[0], cx) } /// Merge the names of the first two members of the room. @@ -377,68 +345,79 @@ impl Room { }) } - /// Create a new message event (unsigned) - pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent { + /// Create a new unsigned message event + pub fn create_message( + &self, + content: &str, + replies: Vec, + cx: &App, + ) -> Task> { let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); - // Get current user - let public_key = nostr.read(cx).identity().read(cx).public_key(); - - // Get room's subject let subject = self.subject.clone(); + let content = content.to_string(); - let mut tags = vec![]; + let mut member_and_relay_hints = HashMap::new(); - // Add receivers - // - // NOTE: current user will be removed from the list of receivers + // Populate the hashmap with member and relay hint tasks for member in self.members.iter() { - // Get relay hint if available - let relay_url = nostr.read(cx).relay_hint(member, cx); - - // Construct a public key tag with relay hint - let tag = TagStandard::PublicKey { - public_key: member.to_owned(), - relay_url, - alias: None, - uppercase: false, - }; - - tags.push(Tag::from_standardized_without_cell(tag)); + let hint = nostr.read(cx).relay_hint(member, cx); + member_and_relay_hints.insert(member.to_owned(), hint); } - // Add subject tag if it's present - if let Some(value) = subject { - tags.push(Tag::from_standardized_without_cell(TagStandard::Subject( - value.to_string(), - ))); - } + cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; - // Add reply/quote tag - if replies.len() == 1 { - tags.push(Tag::event(replies[0])) - } else { - for id in replies { - let tag = TagStandard::Quote { - event_id: id.to_owned(), - relay_url: None, - public_key: None, + // List of event tags for each receiver + let mut tags = vec![]; + + for (member, task) in member_and_relay_hints.into_iter() { + // Skip current user + if member == public_key { + continue; + } + + // Get relay hint if available + let relay_url = task.await; + + // Construct a public key tag with relay hint + let tag = TagStandard::PublicKey { + public_key: member, + relay_url, + alias: None, + uppercase: false, }; - tags.push(Tag::from_standardized_without_cell(tag)) + + tags.push(Tag::from_standardized_without_cell(tag)); } - } - // Construct a direct message event - // - // WARNING: never sign and send this event to relays - let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content) - .tags(tags) - .build(public_key); + // Add subject tag if present + if let Some(value) = subject { + tags.push(Tag::from_standardized_without_cell(TagStandard::Subject( + value.to_string(), + ))); + } - // Ensure the event id has been generated - event.ensure_id(); + // Add all reply tags + for id in replies { + tags.push(Tag::event(id)) + } - event + // Construct a direct message event + // + // WARNING: never sign and send this event to relays + // TODO + let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content) + .tags(tags) + .build(Keys::generate().public_key()); + + // Ensure the event ID has been generated + event.ensure_id(); + + Ok(event) + }) } /// Create a task to send a message to all room members @@ -450,46 +429,27 @@ impl Room { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - // Get current user's public key and relays - let current_user = nostr.read(cx).identity().read(cx).public_key(); - let current_user_relays = nostr.read(cx).messaging_relays(¤t_user, cx); - + let mut members = self.members(); let rumor = rumor.to_owned(); - // Get all members and their messaging relays - let task = self.members_with_relays(cx); - cx.background_spawn(async move { let signer = client.signer().context("Signer not found")?; - let current_user_relays = current_user_relays.await; - let mut members = task.await; + let current_user = signer.get_public_key().await?; // Remove the current user's public key from the list of receivers // the current user will be handled separately - members.retain(|(this, _)| this != ¤t_user); + members.retain(|this| this != ¤t_user); // Collect the send reports let mut reports: Vec = vec![]; - for (receiver, relays) in members.into_iter() { - // Check if there are any relays to send the message to - if relays.is_empty() { - reports.push(SendReport::new(receiver).relays_not_found()); - continue; - } - - // Ensure relay connection - for url in relays.iter() { - client.add_relay(url).await?; - client.connect_relay(url).await?; - } - + for receiver in members.into_iter() { // Construct the gift wrap event let event = EventBuilder::gift_wrap(signer, &receiver, rumor.clone(), vec![]).await?; // Send the gift wrap event to the messaging relays - match client.send_event(&event).to(&relays).await { + match client.send_event(&event).to_nip17().await { Ok(output) => { let id = output.id().to_owned(); let auth = output.failed.iter().any(|(_, s)| s.starts_with("auth-")); @@ -531,20 +491,8 @@ impl Room { // Only send a backup message to current user if sent successfully to others if reports.iter().all(|r| r.is_sent_success()) { - // Check if there are any relays to send the event to - if current_user_relays.is_empty() { - reports.push(SendReport::new(current_user).relays_not_found()); - return Ok(reports); - } - - // Ensure relay connection - for url in current_user_relays.iter() { - client.add_relay(url).await?; - client.connect_relay(url).await?; - } - // Send the event to the messaging relays - match client.send_event_to(current_user_relays, &event).await { + match client.send_event(&event).to_nip17().await { Ok(output) => { reports.push(SendReport::new(current_user).status(output)); } diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 791beee..a29ec1c 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; -use std::time::Duration; pub use actions::*; +use anyhow::Error; use chat::{Message, RenderedMessage, Room, RoomEvent, RoomKind, SendReport}; use common::{nip96_upload, RenderedTimestamp}; use dock::panel::{Panel, PanelEvent}; @@ -244,27 +244,21 @@ impl ChatPanel { return; } - // Get the current room entity - let Some(room) = self.room.upgrade().map(|this| this.read(cx)) else { - return; - }; - // Get replies_to if it's present let replies: Vec = self.replies_to.read(cx).iter().copied().collect(); - // Create a temporary message for optimistic update - let rumor = room.create_message(&content, replies.as_ref(), cx); - let rumor_id = rumor.id.unwrap(); - - // Create a task for sending the message in the background - let send_message = room.send_message(&rumor, cx); + // Get a task to create temporary message for optimistic update + let Ok(get_rumor) = self + .room + .read_with(cx, |this, cx| this.create_message(&content, replies, cx)) + else { + return; + }; // Optimistically update message list - cx.spawn_in(window, async move |this, cx| { - // Wait for the delay - cx.background_executor() - .timer(Duration::from_millis(100)) - .await; + let task: Task> = cx.spawn_in(window, async move |this, cx| { + let mut rumor = get_rumor.await?; + let rumor_id = rumor.id(); // Update the message list and reset the states this.update_in(cx, |this, window, cx| { @@ -280,43 +274,50 @@ impl ChatPanel { // Update the message list this.insert_message(&rumor, true, cx); - }) - .ok(); - }) - .detach(); - self.tasks.push(cx.spawn_in(window, async move |this, cx| { - let result = send_message.await; + if let Ok(task) = this + .room + .read_with(cx, |this, cx| this.send_message(&rumor, cx)) + { + this.tasks.push(cx.spawn_in(window, async move |this, cx| { + let result = task.await; - this.update_in(cx, |this, window, cx| { - match result { - Ok(reports) => { - // Update room's status - this.room - .update(cx, |this, cx| { - if this.kind != RoomKind::Ongoing { - // Update the room kind to ongoing, - // but keep the room kind if send failed - if reports.iter().all(|r| !r.is_sent_success()) { - this.kind = RoomKind::Ongoing; - cx.notify(); - } + this.update_in(cx, |this, window, cx| { + match result { + Ok(reports) => { + // Update room's status + this.room + .update(cx, |this, cx| { + if this.kind != RoomKind::Ongoing { + // Update the room kind to ongoing, + // but keep the room kind if send failed + if reports.iter().all(|r| !r.is_sent_success()) { + this.kind = RoomKind::Ongoing; + cx.notify(); + } + } + }) + .ok(); + + // Insert the sent reports + this.reports_by_id.insert(rumor_id, reports); + + cx.notify(); } - }) - .ok(); - - // Insert the sent reports - this.reports_by_id.insert(rumor_id, reports); - - cx.notify(); - } - Err(e) => { - window.push_notification(e.to_string(), cx); - } + Err(e) => { + window.push_notification(e.to_string(), cx); + } + } + }) + .ok(); + })) } - }) - .ok(); - })); + })?; + + Ok(()) + }); + + task.detach(); } /// Insert a message into the chat panel diff --git a/crates/coop/src/command_bar.rs b/crates/coop/src/command_bar.rs index dbf4532..6cea72f 100644 --- a/crates/coop/src/command_bar.rs +++ b/crates/coop/src/command_bar.rs @@ -169,7 +169,6 @@ impl CommandBar { fn search(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let identity = nostr.read(cx).identity(); let query = self.find_input.read(cx).value(); // Return if the query is empty @@ -191,7 +190,7 @@ impl CommandBar { // Block the input until the search completes self.set_finding(true, window, cx); - let find_users = if identity.read(cx).owned { + let find_users = if nostr.read(cx).owned_signer() { nostr.read(cx).wot_search(&query, cx) } else { nostr.read(cx).search(&query, cx) @@ -245,17 +244,28 @@ impl CommandBar { fn create(&mut self, window: &mut Window, cx: &mut Context) { let chat = ChatRegistry::global(cx); - let nostr = NostrRegistry::global(cx); - let public_key = nostr.read(cx).identity().read(cx).public_key(); + let async_chat = chat.downgrade(); + let nostr = NostrRegistry::global(cx); + let signer_pkey = nostr.read(cx).signer_pkey(cx); + + // Get all selected public keys let receivers = self.selected(cx); - chat.update(cx, |this, cx| { - let room = cx.new(|_| Room::new(public_key, receivers)); - this.emit_room(room.downgrade(), cx); + let task: Task> = cx.spawn_in(window, async move |_this, cx| { + let public_key = signer_pkey.await?; + + async_chat.update_in(cx, |this, window, cx| { + let room = cx.new(|_| Room::new(public_key, receivers)); + this.emit_room(room.downgrade(), cx); + + window.close_modal(cx); + })?; + + Ok(()) }); - window.close_modal(cx); + task.detach(); } fn select(&mut self, pkey: PublicKey, cx: &mut Context) { diff --git a/crates/coop/src/panels/greeter.rs b/crates/coop/src/panels/greeter.rs index 04541c9..9b58edd 100644 --- a/crates/coop/src/panels/greeter.rs +++ b/crates/coop/src/panels/greeter.rs @@ -29,6 +29,26 @@ impl GreeterPanel { focus_handle: cx.focus_handle(), } } + + fn add_profile_panel(&mut self, window: &mut Window, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let signer_pkey = nostr.read(cx).signer_pkey(cx); + + cx.spawn_in(window, async move |_this, cx| { + if let Ok(public_key) = signer_pkey.await { + cx.update(|window, cx| { + Workspace::add_panel( + profile::init(public_key, window, cx), + DockPlacement::Center, + window, + cx, + ); + }) + .ok(); + } + }) + .detach(); + } } impl Panel for GreeterPanel { @@ -62,12 +82,11 @@ impl Render for GreeterPanel { const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr."; let nostr = NostrRegistry::global(cx); - let identity = nostr.read(cx).identity(); + let nip65_state = nostr.read(cx).nip65_state(); + let nip17_state = nostr.read(cx).nip17_state(); - let relay_list_state = identity.read(cx).relay_list_state(); - let messaging_relay_state = identity.read(cx).messaging_relays_state(); - let required_actions = - relay_list_state == RelayState::NotSet || messaging_relay_state == RelayState::NotSet; + let required_actions = nip65_state.read(cx) == &RelayState::NotConfigured + || nip17_state.read(cx) == &RelayState::NotConfigured; h_flex() .size_full() @@ -128,7 +147,7 @@ impl Render for GreeterPanel { v_flex() .gap_2() .w_full() - .when(relay_list_state == RelayState::NotSet, |this| { + .when(nip65_state.read(cx).not_configured(), |this| { this.child( Button::new("relaylist") .icon(Icon::new(IconName::Relay)) @@ -146,31 +165,28 @@ impl Render for GreeterPanel { }), ) }) - .when( - messaging_relay_state == RelayState::NotSet, - |this| { - this.child( - Button::new("import") - .icon(Icon::new(IconName::Relay)) - .label("Set up messaging relays") - .ghost() - .small() - .no_center() - .on_click(move |_ev, window, cx| { - Workspace::add_panel( - messaging_relays::init(window, cx), - DockPlacement::Center, - window, - cx, - ); - }), - ) - }, - ), + .when(nip17_state.read(cx).not_configured(), |this| { + this.child( + Button::new("import") + .icon(Icon::new(IconName::Relay)) + .label("Set up messaging relays") + .ghost() + .small() + .no_center() + .on_click(move |_ev, window, cx| { + Workspace::add_panel( + messaging_relays::init(window, cx), + DockPlacement::Center, + window, + cx, + ); + }), + ) + }), ), ) }) - .when(!identity.read(cx).owned, |this| { + .when(!nostr.read(cx).owned_signer(), |this| { this.child( v_flex() .gap_2() @@ -257,14 +273,9 @@ impl Render for GreeterPanel { .ghost() .small() .no_center() - .on_click(move |_ev, window, cx| { - Workspace::add_panel( - profile::init(window, cx), - DockPlacement::Center, - window, - cx, - ); - }), + .on_click(cx.listener(move |this, _ev, window, cx| { + this.add_profile_panel(window, cx) + })), ) .child( Button::new("invite") diff --git a/crates/coop/src/panels/messaging_relays.rs b/crates/coop/src/panels/messaging_relays.rs index 6388d7c..0625dd0 100644 --- a/crates/coop/src/panels/messaging_relays.rs +++ b/crates/coop/src/panels/messaging_relays.rs @@ -156,29 +156,20 @@ impl MessagingRelayPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - let relays = self.relays.clone(); + + let tags: Vec = self + .relays + .iter() + .map(|relay| Tag::relay(relay.clone())) + .collect(); let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; - - let tags: Vec = relays - .iter() - .map(|relay| Tag::relay(relay.clone())) - .collect(); - + // Construct nip17 event builder let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags); let event = client.sign_event_builder(builder).await?; // Set messaging relays - client.send_event(&event).to(urls).await?; - - // Connect to messaging relays - for relay in relays.iter() { - client.add_relay(relay).await.ok(); - client.connect_relay(relay).await.ok(); - } + client.send_event(&event).to_nip65().await?; Ok(()) }); diff --git a/crates/coop/src/panels/profile.rs b/crates/coop/src/panels/profile.rs index 8d48095..ad806db 100644 --- a/crates/coop/src/panels/profile.rs +++ b/crates/coop/src/panels/profile.rs @@ -22,8 +22,8 @@ use ui::input::{InputState, TextInput}; use ui::notification::Notification; use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension}; -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| ProfilePanel::new(window, cx)) +pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| ProfilePanel::new(public_key, window, cx)) } #[derive(Debug)] @@ -31,6 +31,9 @@ pub struct ProfilePanel { name: SharedString, focus_handle: FocusHandle, + /// User's public key + public_key: PublicKey, + /// User's name text input name_input: Entity, @@ -51,13 +54,10 @@ pub struct ProfilePanel { } impl ProfilePanel { - fn new(window: &mut Window, cx: &mut Context) -> Self { + fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context) -> Self { let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice")); let website_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me")); - - // Hidden input for avatar url let avatar_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me/a.jpg")); - // Use multi-line input for bio let bio_input = cx.new(|cx| { InputState::new(window, cx) @@ -66,13 +66,10 @@ impl ProfilePanel { .placeholder("A short introduce about you.") }); + // Get user's profile and update inputs cx.defer_in(window, move |this, window, cx| { - let nostr = NostrRegistry::global(cx); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let persons = PersonRegistry::global(cx); let profile = persons.read(cx).get(&public_key, cx); - // Set all input's values with current profile this.set_profile(profile, window, cx); }); @@ -80,6 +77,7 @@ impl ProfilePanel { Self { name: "Update Profile".into(), focus_handle: cx.focus_handle(), + public_key, name_input, avatar_input, bio_input, @@ -209,7 +207,7 @@ impl ProfilePanel { fn set_metadata(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let public_key = nostr.read(cx).identity().read(cx).public_key(); + let public_key = self.public_key; // Get the old metadata let persons = PersonRegistry::global(cx); @@ -289,9 +287,7 @@ impl Focusable for ProfilePanel { impl Render for ProfilePanel { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { - let nostr = NostrRegistry::global(cx); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let shorten_pkey = SharedString::from(shorten_pubkey(public_key, 8)); + let shorten_pkey = SharedString::from(shorten_pubkey(self.public_key, 8)); // Get the avatar let avatar_input = self.avatar_input.read(cx).value(); @@ -390,7 +386,7 @@ impl Render for ProfilePanel { .ghost() .on_click(cx.listener(move |this, _ev, window, cx| { this.copy( - public_key.to_bech32().unwrap(), + this.public_key.to_bech32().unwrap(), window, cx, ); diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index 2d3cc0b..4d32e2b 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -9,10 +9,11 @@ use gpui::{ div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window, }; +use nostr_sdk::prelude::*; use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; use state::NostrRegistry; -use theme::{ActiveTheme, Theme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT}; +use theme::{ActiveTheme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT}; use titlebar::TitleBar; use ui::avatar::Avatar; use ui::{h_flex, v_flex, Icon, IconName, Root, Sizable, WindowExtension}; @@ -35,6 +36,9 @@ pub struct Workspace { /// App's Command Bar command_bar: Entity, + /// Current User + current_user: Entity>, + /// Event subscriptions _subscriptions: SmallVec<[Subscription; 3]>, } @@ -42,20 +46,23 @@ pub struct Workspace { impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); + let current_user = cx.new(|_| None); + + let nostr = NostrRegistry::global(cx); + let nip65_state = nostr.read(cx).nip65_state(); + + // Titlebar let titlebar = cx.new(|_| TitleBar::new()); + + // Command bar let command_bar = cx.new(|cx| CommandBar::new(window, cx)); + + // Dock let dock = cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar)); let mut subscriptions = smallvec![]; - subscriptions.push( - // Automatically sync theme with system appearance - window.observe_window_appearance(|window, cx| { - Theme::sync_system_appearance(Some(window), cx); - }), - ); - subscriptions.push( // Observe all events emitted by the chat registry cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| { @@ -100,6 +107,15 @@ impl Workspace { }), ); + subscriptions.push( + // Observe the NIP-65 state + cx.observe(&nip65_state, move |this, state, cx| { + if state.read(cx).idle() { + this.get_current_user(cx); + } + }), + ); + // Set the default layout for app's dock cx.defer_in(window, |this, window, cx| { this.set_layout(window, cx); @@ -109,6 +125,7 @@ impl Workspace { titlebar, dock, command_bar, + current_user, _subscriptions: subscriptions, } } @@ -173,18 +190,35 @@ impl Workspace { }); } - fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { + fn get_current_user(&self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let identity = nostr.read(cx).identity(); + let client = nostr.read(cx).client(); + let current_user = self.current_user.downgrade(); + cx.spawn(async move |_this, cx| { + if let Some(signer) = client.signer() { + if let Ok(public_key) = signer.get_public_key().await { + current_user + .update(cx, |this, cx| { + *this = Some(public_key); + cx.notify(); + }) + .ok(); + } + } + }) + .detach(); + } + + fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { h_flex() .h(TITLEBAR_HEIGHT) .flex_1() .justify_between() .gap_2() - .when_some(identity.read(cx).public_key, |this, public_key| { + .when_some(self.current_user.read(cx).as_ref(), |this, public_key| { let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&public_key, cx); + let profile = persons.read(cx).get(public_key, cx); this.child( h_flex() diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index e4d2ce8..d94ac41 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -40,7 +40,7 @@ pub struct DeviceRegistry { tasks: Vec>>, /// Subscriptions - _subscriptions: SmallVec<[Subscription; 1]>, + _subscriptions: SmallVec<[Subscription; 2]>, } impl DeviceRegistry { @@ -58,7 +58,8 @@ impl DeviceRegistry { fn new(cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let identity = nostr.read(cx).identity(); + let nip65_state = nostr.read(cx).nip65_state(); + let nip17_state = nostr.read(cx).nip17_state(); let device_signer = cx.new(|_| None); let requests = cx.new(|_| HashSet::default()); @@ -70,21 +71,26 @@ impl DeviceRegistry { let mut tasks = vec![]; subscriptions.push( - // Observe the identity entity - cx.observe(&identity, |this, state, cx| { - match state.read(cx).relay_list_state() { - RelayState::Initial => { + // Observe the NIP-65 state + cx.observe(&nip65_state, |this, state, cx| { + match state.read(cx) { + RelayState::Idle => { this.reset(cx); } - RelayState::Set => { + RelayState::Configured => { this.get_announcement(cx); - - if state.read(cx).messaging_relays_state() == RelayState::Set { - this.get_messages(cx); - } } _ => {} - } + }; + }), + ); + + subscriptions.push( + // Observe the NIP-17 state + cx.observe(&nip17_state, |this, state, cx| { + if state.read(cx) == &RelayState::Configured { + this.get_messages(cx); + }; }), ); @@ -265,29 +271,26 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let device_signer = self.device_signer.read(cx).clone(); + let messaging_relays = nostr.read(cx).messaging_relays(cx); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let messaging_relays = nostr.read(cx).messaging_relays(&public_key, cx); - - cx.background_spawn(async move { + let task: Task> = cx.background_spawn(async move { let urls = messaging_relays.await; + let user_signer = client.signer().context("Signer not found")?; + let public_key = user_signer.get_public_key().await?; // Get messages with dekey if let Some(signer) = device_signer.as_ref() { - if let Ok(pkey) = signer.get_public_key().await { - let filter = Filter::new().kind(Kind::GiftWrap).pubkey(pkey); - let id = SubscriptionId::new(DEVICE_GIFTWRAP); + let device_pkey = signer.get_public_key().await?; + let filter = Filter::new().kind(Kind::GiftWrap).pubkey(device_pkey); + let id = SubscriptionId::new(DEVICE_GIFTWRAP); - // Construct target for subscription - let target = urls - .iter() - .map(|relay| (relay, vec![filter.clone()])) - .collect::>(); + // Construct target for subscription + let target = urls + .iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); - if let Err(e) = client.subscribe(target).with_id(id).await { - log::error!("Failed to subscribe to gift wrap events: {e}"); - } - } + client.subscribe(target).with_id(id).await?; } // Get messages with user key @@ -300,11 +303,12 @@ impl DeviceRegistry { .map(|relay| (relay, vec![filter.clone()])) .collect::>(); - if let Err(e) = client.subscribe(target).with_id(id).await { - log::error!("Failed to subscribe to gift wrap events: {e}"); - } - }) - .detach(); + client.subscribe(target).with_id(id).await?; + + Ok(()) + }); + + task.detach(); } /// Get device announcement for current user @@ -312,11 +316,9 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; // Construct the filter for the device announcement event let filter = Filter::new() @@ -324,14 +326,8 @@ impl DeviceRegistry { .author(public_key) .limit(1); - // Construct target for subscription - let target = urls - .iter() - .map(|relay| (relay, vec![filter.clone()])) - .collect::>(); - let mut stream = client - .stream_events(target) + .stream_events(filter) .timeout(Duration::from_secs(TIMEOUT)) .await?; @@ -373,16 +369,12 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - + // Generate a new device keys let keys = Keys::generate(); let secret = keys.secret_key().to_secret_hex(); let n = keys.public_key(); let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; - // Construct an announcement event let builder = EventBuilder::new(Kind::Custom(10044), "").tags(vec![ Tag::custom(TagKind::custom("n"), vec![n]), @@ -391,7 +383,7 @@ impl DeviceRegistry { let event = client.sign_event_builder(builder).await?; // Publish announcement - client.send_event(&event).to(urls).await?; + client.send_event(&event).to_nip65().await?; // Save device keys to the database Self::set_keys(&client, &secret).await?; @@ -459,11 +451,9 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; // Construct a filter for device key requests let filter = Filter::new() @@ -471,14 +461,8 @@ impl DeviceRegistry { .author(public_key) .since(Timestamp::now()); - // Construct target for subscription - let target = urls - .iter() - .map(|relay| (relay, vec![filter.clone()])) - .collect::>(); - // Subscribe to the device key requests on user's write relays - client.subscribe(target).await?; + client.subscribe(filter).await?; Ok(()) }); @@ -491,11 +475,9 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; // Construct a filter for device key requests let filter = Filter::new() @@ -503,14 +485,8 @@ impl DeviceRegistry { .author(public_key) .since(Timestamp::now()); - // Construct target for subscription - let target = urls - .iter() - .map(|relay| (relay, vec![filter.clone()])) - .collect::>(); - // Subscribe to the device key requests on user's write relays - client.subscribe(target).await?; + client.subscribe(filter).await?; Ok(()) }); @@ -523,9 +499,6 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - let app_keys = nostr.read(cx).app_keys().clone(); let app_pubkey = app_keys.public_key(); @@ -557,8 +530,6 @@ impl DeviceRegistry { Ok(Some(keys)) } None => { - let urls = write_relays.await; - // Construct an event for device key request let builder = EventBuilder::new(Kind::Custom(4454), "").tags(vec![ Tag::client(app_name()), @@ -567,7 +538,7 @@ impl DeviceRegistry { let event = client.sign_event_builder(builder).await?; // Send the event to write relays - client.send_event(&event).to(urls).await?; + client.send_event(&event).to_nip65().await?; Ok(None) } @@ -640,11 +611,7 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; let signer = client.signer().context("Signer not found")?; // Get device keys @@ -673,7 +640,7 @@ impl DeviceRegistry { let event = client.sign_event_builder(builder).await?; // Send the response event to the user's relay list - client.send_event(&event).to(urls).await?; + client.send_event(&event).to_nip65().await?; Ok(()) }); diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index 6a88e0c..1f182ea 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -138,16 +138,36 @@ impl RelayAuth { let mut notifications = client.notifications(); while let Some(notification) = notifications.next().await { - if let ClientNotification::Message { - message: RelayMessage::Auth { challenge }, - relay_url, - } = notification - { - let request = AuthRequest::new(challenge, relay_url); + match notification { + ClientNotification::Message { relay_url, message } => { + match message { + RelayMessage::Auth { challenge } => { + let request = AuthRequest::new(challenge, relay_url); - if let Err(e) = tx.send_async(request).await { - log::error!("Failed to send auth request: {}", e); + if let Err(e) = tx.send_async(request).await { + log::error!("Failed to send auth request: {}", e); + } + } + RelayMessage::Ok { + event_id, message, .. + } => { + let msg = MachineReadablePrefix::parse(&message); + let mut tracker = tracker().write().await; + + // Handle authentication messages + if let Some(MachineReadablePrefix::AuthRequired) = msg { + // Keep track of events that need to be resent after authentication + tracker.add_to_pending(event_id, relay_url); + } else { + // Keep track of events sent by Coop + tracker.sent(event_id) + } + } + _ => {} + } } + ClientNotification::Shutdown => break, + _ => {} } } } diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 9781b8d..42645c5 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use anyhow::{anyhow, Error}; +use anyhow::{anyhow, Context as AnyhowContext, Error}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; @@ -121,7 +121,7 @@ pub struct AppSettings { _subscriptions: SmallVec<[Subscription; 1]>, /// Background tasks - _tasks: SmallVec<[Task<()>; 1]>, + tasks: SmallVec<[Task<()>; 1]>, } impl AppSettings { @@ -163,8 +163,8 @@ impl AppSettings { Self { values: Settings::default(), + tasks, _subscriptions: subscriptions, - _tasks: tasks, } } @@ -172,7 +172,6 @@ impl AppSettings { fn get_from_database(cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let current_user = nostr.read(cx).identity().read(cx).public_key; cx.background_spawn(async move { // Construct a filter to get the latest settings @@ -181,9 +180,12 @@ impl AppSettings { .identifier(SETTINGS_IDENTIFIER) .limit(1); - if let Some(public_key) = current_user { - // Push author to the filter - filter = filter.author(public_key); + // If the signer is available, get settings belonging to the current user + if let Some(signer) = client.signer() { + if let Ok(public_key) = signer.get_public_key().await { + // Push author to the filter + filter = filter.author(public_key); + } } if let Some(event) = client.database().query(filter).await?.first_owned() { @@ -198,7 +200,7 @@ impl AppSettings { pub fn load(&mut self, cx: &mut Context) { let task = Self::get_from_database(cx); - self._tasks.push( + self.tasks.push( // Run task in the background cx.spawn(async move |this, cx| { if let Ok(settings) = task.await { @@ -216,10 +218,12 @@ impl AppSettings { pub fn save(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); if let Ok(content) = serde_json::to_string(&self.values) { let task: Task> = cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; + let event = EventBuilder::new(Kind::ApplicationSpecificData, content) .tag(Tag::identifier(SETTINGS_IDENTIFIER)) .build(public_key) diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index 2d04fc2..bc3b846 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -10,7 +10,7 @@ common = { path = "../common" } nostr-sdk.workspace = true nostr-lmdb.workspace = true nostr-connect.workspace = true -nostr-gossip-memory.workspace = true +nostr-gossip-sqlite.workspace = true gpui.workspace = true gpui_tokio.workspace = true diff --git a/crates/state/src/identity.rs b/crates/state/src/identity.rs deleted file mode 100644 index 31bb690..0000000 --- a/crates/state/src/identity.rs +++ /dev/null @@ -1,101 +0,0 @@ -use nostr_sdk::prelude::*; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum RelayState { - #[default] - Initial, - NotSet, - Set, -} - -impl RelayState { - pub fn is_initial(&self) -> bool { - matches!(self, RelayState::Initial) - } -} - -/// Identity -#[derive(Debug, Default)] -pub struct Identity { - /// Signer's public key - pub public_key: Option, - - /// Whether the identity is owned by the user - pub owned: bool, - - /// Status of the current user NIP-65 relays - relay_list: RelayState, - - /// Status of the current user NIP-17 relays - messaging_relays: RelayState, -} - -impl AsRef for Identity { - fn as_ref(&self) -> &Identity { - self - } -} - -impl Identity { - pub fn new() -> Self { - Self { - public_key: None, - owned: true, - relay_list: RelayState::default(), - messaging_relays: RelayState::default(), - } - } - - /// Resets the relay states to their default values. - pub fn reset_relay_state(&mut self) { - self.relay_list = RelayState::default(); - self.messaging_relays = RelayState::default(); - } - - /// Sets the state of the NIP-65 relays. - pub fn set_relay_list_state(&mut self, state: RelayState) { - self.relay_list = state; - } - - /// Returns the state of the NIP-65 relays. - pub fn relay_list_state(&self) -> RelayState { - self.relay_list - } - - /// Sets the state of the NIP-17 relays. - pub fn set_messaging_relays_state(&mut self, state: RelayState) { - self.messaging_relays = state; - } - - /// Returns the state of the NIP-17 relays. - pub fn messaging_relays_state(&self) -> RelayState { - self.messaging_relays - } - - /// Force getting the public key of the identity. - /// - /// Panics if the public key is not set. - pub fn public_key(&self) -> PublicKey { - self.public_key.unwrap() - } - - /// Returns true if the identity has a public key. - pub fn has_public_key(&self) -> bool { - self.public_key.is_some() - } - - /// Sets the public key of the identity. - pub fn set_public_key(&mut self, public_key: PublicKey) { - self.public_key = Some(public_key); - } - - /// Unsets the public key of the identity. - pub fn unset_public_key(&mut self) { - self.public_key = None; - } - - /// Sets whether the identity is owned by the user. - pub fn set_owned(&mut self, owned: bool) { - self.owned = owned; - } -} diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 5fb7df6..cd8830d 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,31 +1,29 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::os::unix::fs::PermissionsExt; use std::sync::Arc; use std::time::Duration; -use anyhow::{anyhow, Error}; +use anyhow::{anyhow, Context as AnyhowContext, Error}; use common::{config_dir, CLIENT_NAME}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; +use gpui_tokio::Tokio; use nostr_connect::prelude::*; +use nostr_gossip_sqlite::prelude::*; use nostr_lmdb::NostrLmdb; use nostr_sdk::prelude::*; mod device; mod event; mod gossip; -mod identity; mod nip05; mod signer; pub use device::*; pub use event::*; pub use gossip::*; -pub use identity::*; pub use nip05::*; pub use signer::*; -use crate::identity::Identity; - /// Default timeout for subscription pub const TIMEOUT: u64 = 3; /// Default delay for searching @@ -55,9 +53,19 @@ pub const BOOTSTRAP_RELAYS: [&str; 4] = [ ]; pub fn init(cx: &mut App) { + // rustls uses the `aws_lc_rs` provider by default + // This only errors if the default provider has already + // been installed. We can ignore this `Result`. + rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .ok(); + // Initialize the tokio runtime gpui_tokio::init(cx); + // Initialize the event tracker + let _tracker = tracker(); + NostrRegistry::set_global(cx.new(NostrRegistry::new), cx); } @@ -74,23 +82,26 @@ pub struct NostrRegistry { /// Nostr signer signer: Arc, + /// By default, Coop generates a new signer for new users. + /// + /// This flag indicates whether the signer is user-owned or Coop-generated. + owned_signer: bool, + + /// NIP-65 relay state + nip65: Entity, + + /// NIP-17 relay state + nip17: Entity, + /// App keys /// /// Used for Nostr Connect and NIP-4e operations app_keys: Keys, - /// Current identity (user's public key) - /// - /// Set by the current Nostr signer - identity: Entity, - - /// Gossip implementation - gossip: Entity, - /// Tasks for asynchronous operations tasks: Vec>>, - /// Subscriptions + /// Event subscriptions _subscriptions: Vec, } @@ -107,27 +118,39 @@ impl NostrRegistry { /// Create a new nostr instance fn new(cx: &mut Context) -> Self { - // rustls uses the `aws_lc_rs` provider by default - // This only errors if the default provider has already - // been installed. We can ignore this `Result`. - rustls::crypto::aws_lc_rs::default_provider() - .install_default() - .ok(); - - // Construct the lmdb + // Construct the nostr lmdb instance let lmdb = cx.foreground_executor().block_on(async move { NostrLmdb::open(config_dir().join("nostr")) .await .expect("Failed to initialize database") }); + // Use tokio to spawn a task to build the gossip instance + let build_gossip_sqlite = Tokio::spawn(cx, async move { + NostrGossipSqlite::open(config_dir().join("gossip")) + .await + .expect("Failed to initialize gossip") + }); + + // Initialize the nostr gossip instance + let gossip = cx.foreground_executor().block_on(async move { + build_gossip_sqlite + .await + .expect("Failed to initialize gossip") + }); + // Construct the nostr signer - let keys = Keys::generate(); - let signer = Arc::new(CoopSigner::new(keys)); + let app_keys = Self::create_or_init_app_keys().unwrap_or(Keys::generate()); + let signer = Arc::new(CoopSigner::new(app_keys.clone())); + + // Construct the relay states entity + let nip65 = cx.new(|_| RelayState::default()); + let nip17 = cx.new(|_| RelayState::default()); // Construct the nostr client let client = ClientBuilder::default() .signer(signer.clone()) + .gossip(gossip) .database(lmdb) .automatic_authentication(false) .verify_subscriptions(false) @@ -136,83 +159,23 @@ impl NostrRegistry { }) .build(); - // Construct the event tracker - let _tracker = tracker(); - - // Get the app keys - let app_keys = Self::create_or_init_app_keys().unwrap(); - - // Construct the gossip entity - let gossip = cx.new(|_| Gossip::default()); - let async_gossip = gossip.downgrade(); - - // Construct the identity entity - let identity = cx.new(|_| Identity::new()); - - // Channel for communication between nostr and gpui - let (tx, rx) = flume::bounded::(2048); - let mut subscriptions = vec![]; - let mut tasks = vec![]; subscriptions.push( - // Observe the identity entity - cx.observe(&identity, |this, state, cx| { - if state.read(cx).has_public_key() { - match state.read(cx).relay_list_state() { - RelayState::Initial => { - this.get_relay_list(cx); - } - RelayState::Set => { - if state.read(cx).messaging_relays_state() == RelayState::Initial { - this.get_profile(cx); - this.get_messaging_relays(cx); - }; - } - _ => {} - } + // Observe the NIP-65 state + cx.observe(&nip65, |this, state, cx| { + if state.read(cx).configured() { + this.get_profile(cx); + this.get_messaging_relays(cx); } }), ); - tasks.push( - // Handle nostr notifications - cx.background_spawn({ - let client = client.clone(); - - async move { Self::handle_notifications(&client, &tx).await } - }), - ); - - tasks.push( - // Update GPUI states - cx.spawn(async move |_this, cx| { - while let Ok(event) = rx.recv_async().await { - match event.kind { - Kind::RelayList => { - async_gossip.update(cx, |this, cx| { - this.insert_relays(&event); - cx.notify(); - })?; - } - Kind::InboxRelays => { - async_gossip.update(cx, |this, cx| { - this.insert_messaging_relays(&event); - cx.notify(); - })?; - } - _ => {} - } - } - - Ok(()) - }), - ); - cx.defer(|cx| { let nostr = NostrRegistry::global(cx); nostr.update(cx, |this, cx| { + this.connect(cx); this.get_identity(cx); }); }); @@ -220,89 +183,188 @@ impl NostrRegistry { Self { client, signer, + owned_signer: false, + nip65, + nip17, app_keys, - identity, - gossip, + tasks: vec![], _subscriptions: subscriptions, - tasks, } } - /// Handle nostr notifications - async fn handle_notifications(client: &Client, tx: &flume::Sender) -> Result<(), Error> { - // Add bootstrap relay to the relay pool - for url in BOOTSTRAP_RELAYS.into_iter() { - client.add_relay(url).await?; - } + /// Connect to all bootstrap relays + fn connect(&mut self, cx: &mut Context) { + let client = self.client(); - // Add search relay to the relay pool - for url in SEARCH_RELAYS.into_iter() { - client.add_relay(url).await?; - } + self.tasks.push(cx.background_spawn(async move { + // Add bootstrap relay to the relay pool + for url in BOOTSTRAP_RELAYS.into_iter() { + client.add_relay(url).await?; + } - // Add wot relay to the relay pool - for url in WOT_RELAYS.into_iter() { - client.add_relay(url).await?; - } + // Add search relay to the relay pool + for url in SEARCH_RELAYS.into_iter() { + client.add_relay(url).await?; + } - // Connect to all added relays - client.connect().await; + // Add wot relay to the relay pool + for url in WOT_RELAYS.into_iter() { + client.add_relay(url).await?; + } - // Handle nostr notifications - let mut notifications = client.notifications(); - let mut processed_events = HashSet::new(); + // Connect to all added relays + client.connect().await; - while let Some(notification) = notifications.next().await { - if let ClientNotification::Message { message, relay_url } = notification { - match message { - RelayMessage::Event { - event, - subscription_id, - } => { - if !processed_events.insert(event.id) { - // Skip if the event has already been processed - continue; - } + Ok(()) + })); + } - match event.kind { - Kind::RelayList => { - // Automatically get messaging relays for each member when the user opens a room - if subscription_id.as_str().starts_with("room-") { - Self::get_adv_events_by(client, event.as_ref()).await?; - } + /// Get the nostr client + pub fn client(&self) -> Client { + self.client.clone() + } - tx.send_async(event.into_owned()).await?; - } - Kind::InboxRelays => { - tx.send_async(event.into_owned()).await?; - } - _ => {} - } - } - RelayMessage::Ok { - event_id, message, .. - } => { - let msg = MachineReadablePrefix::parse(&message); - let mut tracker = tracker().write().await; + /// Get the app keys + pub fn app_keys(&self) -> &Keys { + &self.app_keys + } - // Handle authentication messages - if let Some(MachineReadablePrefix::AuthRequired) = msg { - // Keep track of events that need to be resent after authentication - tracker.add_to_pending(event_id, relay_url); - } else { - // Keep track of events sent by Coop - tracker.sent(event_id) - } - } - _ => {} + /// Returns whether the current signer is owned by user + pub fn owned_signer(&self) -> bool { + self.owned_signer + } + + /// Set whether the current signer is owned by user + pub fn set_owned_signer(&mut self, owned: bool, cx: &mut Context) { + self.owned_signer = owned; + cx.notify(); + } + + /// Get the NIP-65 state + pub fn nip65_state(&self) -> Entity { + self.nip65.clone() + } + + /// Get the NIP-17 state + pub fn nip17_state(&self) -> Entity { + self.nip17.clone() + } + + /// Get current signer's public key + pub fn signer_pkey(&self, cx: &App) -> Task> { + let client = self.client(); + + cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; + + Ok(public_key) + }) + } + + /// Get a relay hint (messaging relay) for a given public key + /// + /// Used for building chat messages + pub fn relay_hint(&self, public_key: &PublicKey, cx: &App) -> Task> { + let client = self.client(); + let public_key = public_key.to_owned(); + + cx.background_spawn(async move { + let filter = Filter::new() + .author(public_key) + .kind(Kind::InboxRelays) + .limit(1); + + if let Ok(events) = client.database().query(filter).await { + if let Some(event) = events.first_owned() { + let relays: Vec = nip17::extract_owned_relay_list(event).collect(); + return relays.first().cloned(); } } - } - Ok(()) + None + }) } - /// Automatically get messaging relays and encryption announcement from a received relay list + /// Get a list of messaging relays with current signer's public key + pub fn messaging_relays(&self, cx: &App) -> Task> { + let client = self.client(); + + cx.background_spawn(async move { + let Ok(signer) = client.signer().context("Signer not found") else { + return vec![]; + }; + + let Ok(public_key) = signer.get_public_key().await else { + return vec![]; + }; + + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); + + client + .database() + .query(filter) + .await + .ok() + .and_then(|events| events.first_owned()) + .map(|event| nip17::extract_owned_relay_list(event).collect()) + .unwrap_or_default() + }) + } + + /// Reset all relay states + pub fn reset_relay_states(&mut self, cx: &mut Context) { + self.nip65.update(cx, |this, cx| { + *this = RelayState::default(); + cx.notify(); + }); + self.nip17.update(cx, |this, cx| { + *this = RelayState::default(); + cx.notify(); + }); + } + + /// Set the signer for the nostr client and verify the public key + pub fn set_signer(&mut self, new: T, owned: bool, cx: &mut Context) + where + T: NostrSigner + 'static, + { + let client = self.client(); + let signer = self.signer.clone(); + + // Create a task to update the signer and verify the public key + let task: Task> = cx.background_spawn(async move { + // Update signer + signer.switch(new).await; + + // Verify signer + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; + + log::info!("Signer's public key: {public_key}"); + + Ok(()) + }); + + self.tasks.push(cx.spawn(async move |this, cx| { + // set signer + task.await?; + + // Update states + this.update(cx, |this, cx| { + this.reset_relay_states(cx); + this.get_relay_list(cx); + this.set_owned_signer(owned, cx); + })?; + + Ok(()) + })); + } + + /* async fn get_adv_events_by(client: &Client, event: &Event) -> Result<(), Error> { // Subscription options let opts = SubscribeAutoCloseOptions::default() @@ -348,6 +410,7 @@ impl NostrRegistry { Ok(()) } + */ /// Get or create a new app keys fn create_or_init_app_keys() -> Result { @@ -377,124 +440,15 @@ impl NostrRegistry { Ok(keys) } - /// Get the nostr client - pub fn client(&self) -> Client { - self.client.clone() - } - - /// Get the app keys - pub fn app_keys(&self) -> &Keys { - &self.app_keys - } - - /// Get current identity - pub fn identity(&self) -> Entity { - self.identity.clone() - } - - /// Get a relay hint (messaging relay) for a given public key - pub fn relay_hint(&self, public_key: &PublicKey, cx: &App) -> Option { - self.gossip - .read(cx) - .messaging_relays(public_key) - .first() - .cloned() - } - - /// Get a list of write relays for a given public key - pub fn write_relays(&self, public_key: &PublicKey, cx: &App) -> Task> { - let client = self.client(); - let relays = self.gossip.read(cx).write_relays(public_key); - - cx.background_spawn(async move { - // Ensure relay connections - for url in relays.iter() { - client.add_relay(url).await.ok(); - client.connect_relay(url).await.ok(); - } - - relays - }) - } - - /// Get a list of read relays for a given public key - pub fn read_relays(&self, public_key: &PublicKey, cx: &App) -> Task> { - let client = self.client(); - let relays = self.gossip.read(cx).read_relays(public_key); - - cx.background_spawn(async move { - // Ensure relay connections - for url in relays.iter() { - client.add_relay(url).await.ok(); - client.connect_relay(url).await.ok(); - } - - relays - }) - } - - /// Get a list of messaging relays for a given public key - pub fn messaging_relays(&self, public_key: &PublicKey, cx: &App) -> Task> { - let client = self.client(); - let relays = self.gossip.read(cx).messaging_relays(public_key); - - cx.background_spawn(async move { - // Ensure relay connections - for url in relays.iter() { - client.add_relay(url).await.ok(); - client.connect_relay(url).await.ok(); - } - - relays - }) - } - - /// Set the signer for the nostr client and verify the public key - pub fn set_signer(&mut self, new: T, owned: bool, cx: &mut Context) - where - T: NostrSigner + 'static, - { - let identity = self.identity.downgrade(); - let signer = self.signer.clone(); - - // Create a task to update the signer and verify the public key - let task: Task> = cx.background_spawn(async move { - // Update signer - signer.switch(new).await; - - // Verify signer - let public_key = signer.get_public_key().await?; - log::info!("test: {public_key:?}"); - - Ok(public_key) - }); - - self.tasks.push(cx.spawn(async move |_this, cx| { - match task.await { - Ok(public_key) => { - identity.update(cx, |this, cx| { - this.set_public_key(public_key); - this.reset_relay_state(); - this.set_owned(owned); - cx.notify(); - })?; - } - Err(e) => { - log::error!("Failed to set signer: {e}"); - } - }; - - Ok(()) - })); - } - // Get relay list for current user fn get_relay_list(&mut self, cx: &mut Context) { let client = self.client(); - let async_identity = self.identity.downgrade(); - let public_key = self.identity().read(cx).public_key(); + let nip65 = self.nip65.downgrade(); let task: Task> = cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; + let filter = Filter::new() .kind(Kind::RelayList) .author(public_key) @@ -531,7 +485,7 @@ impl NostrRegistry { // Subscribe to the relay list events client.subscribe(target).await?; - return Ok(RelayState::Set); + return Ok(RelayState::Configured); } Err(e) => { log::error!("Failed to receive relay list event: {e}"); @@ -539,15 +493,15 @@ impl NostrRegistry { } } - Ok(RelayState::NotSet) + Ok(RelayState::NotConfigured) }); self.tasks.push(cx.spawn(async move |_this, cx| { match task.await { - Ok(state) => { - async_identity + Ok(new_state) => { + nip65 .update(cx, |this, cx| { - this.set_relay_list_state(state); + *this = new_state; cx.notify(); }) .ok(); @@ -561,19 +515,78 @@ impl NostrRegistry { })); } + /// Get messaging relays for current user + fn get_messaging_relays(&mut self, cx: &mut Context) { + let client = self.client(); + let nip17 = self.nip17.downgrade(); + + let task: Task> = cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; + + // Construct the filter for inbox relays + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); + + // Stream events from the write relays + let mut stream = client + .stream_events(filter) + .timeout(Duration::from_secs(TIMEOUT)) + .await?; + + while let Some((_url, res)) = stream.next().await { + match res { + Ok(event) => { + log::info!("Received messaging relays event: {event:?}"); + + // Construct a filter to continuously receive relay list events + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .since(Timestamp::now()); + + // Subscribe to the relay list events + client.subscribe(filter).await?; + + return Ok(RelayState::Configured); + } + Err(e) => { + log::error!("Failed to get messaging relays: {e}"); + } + } + } + + Ok(RelayState::NotConfigured) + }); + + self.tasks.push(cx.spawn(async move |_this, cx| { + match task.await { + Ok(new_state) => { + nip17 + .update(cx, |this, cx| { + *this = new_state; + cx.notify(); + }) + .ok(); + } + Err(e) => { + log::error!("Failed to get messaging relays: {e}"); + } + } + + Ok(()) + })); + } + /// Get profile and contact list for current user fn get_profile(&mut self, cx: &mut Context) { let client = self.client(); - let public_key = self.identity().read(cx).public_key(); - let write_relays = self.write_relays(&public_key, cx); let task: Task> = cx.background_spawn(async move { - let mut urls = write_relays.await; - urls.extend( - BOOTSTRAP_RELAYS - .iter() - .filter_map(|url| RelayUrl::parse(url).ok()), - ); + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; // Construct subscription options let opts = SubscribeAutoCloseOptions::default() @@ -592,13 +605,10 @@ impl NostrRegistry { .limit(1) .author(public_key); - // Construct targets for subscription - let target = urls - .into_iter() - .map(|relay| (relay, vec![metadata.clone(), contact_list.clone()])) - .collect::>(); - - client.subscribe(target).close_on(opts).await?; + client + .subscribe(vec![metadata, contact_list]) + .close_on(opts) + .await?; Ok(()) }); @@ -606,90 +616,14 @@ impl NostrRegistry { task.detach(); } - /// Get messaging relays for current user - fn get_messaging_relays(&mut self, cx: &mut Context) { - let client = self.client(); - let async_identity = self.identity.downgrade(); - let public_key = self.identity().read(cx).public_key(); - let write_relays = self.write_relays(&public_key, cx); - - let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; - - // Construct the filter for inbox relays - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); - - // Construct targets for subscription - let target = urls - .iter() - .map(|relay| (relay, vec![filter.clone()])) - .collect::>(); - - // Stream events from the write relays - let mut stream = client - .stream_events(target) - .timeout(Duration::from_secs(TIMEOUT)) - .await?; - - while let Some((_url, res)) = stream.next().await { - match res { - Ok(event) => { - log::info!("Received messaging relays event: {event:?}"); - - // Construct a filter to continuously receive relay list events - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .since(Timestamp::now()); - - // Construct targets for subscription - let target = urls - .iter() - .map(|relay| (relay, vec![filter.clone()])) - .collect::>(); - - // Subscribe to the relay list events - client.subscribe(target).await?; - - return Ok(RelayState::Set); - } - Err(e) => { - log::error!("Failed to get messaging relays: {e}"); - } - } - } - - Ok(RelayState::NotSet) - }); - - self.tasks.push(cx.spawn(async move |_this, cx| { - match task.await { - Ok(state) => { - async_identity - .update(cx, |this, cx| { - this.set_messaging_relays_state(state); - cx.notify(); - }) - .ok(); - } - Err(e) => { - log::error!("Failed to get messaging relays: {e}"); - } - } - - Ok(()) - })); - } - /// Get contact list for the current user pub fn get_contact_list(&self, cx: &App) -> Task, Error>> { let client = self.client(); - let public_key = self.identity().read(cx).public_key(); cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; + let contacts = client.database().contacts_public_keys(public_key).await?; let results = contacts.into_iter().collect(); @@ -700,19 +634,15 @@ impl NostrRegistry { /// Set the metadata for the current user pub fn set_metadata(&self, metadata: &Metadata, cx: &App) -> Task> { let client = self.client(); - let public_key = self.identity().read(cx).public_key(); - let write_relays = self.write_relays(&public_key, cx); let metadata = metadata.clone(); cx.background_spawn(async move { - let urls = write_relays.await; - // Build and sign the metadata event let builder = EventBuilder::metadata(&metadata); let event = client.sign_event_builder(builder).await?; // Send event to user's write relayss - client.send_event(&event).to(urls).await?; + client.send_event(&event).to_nip65().await?; Ok(()) }) @@ -1007,6 +937,29 @@ impl NostrRegistry { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum RelayState { + #[default] + Idle, + Checking, + NotConfigured, + Configured, +} + +impl RelayState { + pub fn idle(&self) -> bool { + matches!(self, RelayState::Idle) + } + + pub fn not_configured(&self) -> bool { + matches!(self, RelayState::NotConfigured) + } + + pub fn configured(&self) -> bool { + matches!(self, RelayState::Configured) + } +} + #[derive(Debug, Clone)] pub struct CoopAuthUrlHandler; diff --git a/crates/state/src/signer.rs b/crates/state/src/signer.rs index 618df95..afe26bf 100644 --- a/crates/state/src/signer.rs +++ b/crates/state/src/signer.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::result::Result; use std::sync::Arc; use nostr_sdk::prelude::*; @@ -19,10 +20,12 @@ impl CoopSigner { } } - async fn get(&self) -> Arc { + /// Get the current signer. + pub async fn get(&self) -> Arc { self.signer.read().await.clone() } + /// Switch the current signer to a new signer. pub async fn switch(&self, new: T) where T: IntoNostrSigner, @@ -33,40 +36,40 @@ impl CoopSigner { } impl NostrSigner for CoopSigner { + #[allow(mismatched_lifetime_syntaxes)] fn backend(&self) -> SignerBackend { SignerBackend::Custom(Cow::Borrowed("custom")) } - fn get_public_key(&self) -> BoxedFuture> { - Box::pin(async move { Ok(self.get().await.get_public_key().await?) }) + fn get_public_key<'a>(&'a self) -> BoxedFuture<'a, Result> { + Box::pin(async move { self.get().await.get_public_key().await }) } - fn sign_event( - &self, + fn sign_event<'a>( + &'a self, unsigned: UnsignedEvent, - ) -> BoxedFuture> { - Box::pin(async move { Ok(self.get().await.sign_event(unsigned).await?) }) + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { self.get().await.sign_event(unsigned).await }) } fn nip04_encrypt<'a>( &'a self, public_key: &'a PublicKey, content: &'a str, - ) -> BoxedFuture<'a, std::result::Result> { - Box::pin(async move { Ok(self.get().await.nip04_encrypt(public_key, content).await?) }) + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { self.get().await.nip04_encrypt(public_key, content).await }) } fn nip04_decrypt<'a>( &'a self, public_key: &'a PublicKey, encrypted_content: &'a str, - ) -> BoxedFuture<'a, std::result::Result> { + ) -> BoxedFuture<'a, Result> { Box::pin(async move { - Ok(self - .get() + self.get() .await .nip04_decrypt(public_key, encrypted_content) - .await?) + .await }) } @@ -74,15 +77,15 @@ impl NostrSigner for CoopSigner { &'a self, public_key: &'a PublicKey, content: &'a str, - ) -> BoxedFuture<'a, std::result::Result> { - Box::pin(async move { Ok(self.get().await.nip44_encrypt(public_key, content).await?) }) + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { self.get().await.nip44_encrypt(public_key, content).await }) } fn nip44_decrypt<'a>( &'a self, public_key: &'a PublicKey, payload: &'a str, - ) -> BoxedFuture<'a, std::result::Result> { - Box::pin(async move { Ok(self.get().await.nip44_decrypt(public_key, payload).await?) }) + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { self.get().await.nip44_decrypt(public_key, payload).await }) } } -- 2.49.1 From 031883c278f4a23b57b52c291a3b0266a135e8b6 Mon Sep 17 00:00:00 2001 From: reya Date: Sat, 7 Feb 2026 20:52:17 +0700 Subject: [PATCH 04/11] . --- Cargo.lock | 565 +++++---------------------- Cargo.toml | 2 +- crates/chat/src/lib.rs | 142 +++---- crates/chat/src/message.rs | 10 +- crates/chat/src/room.rs | 52 ++- crates/common/src/display.rs | 38 -- crates/coop/src/dialogs/screening.rs | 299 ++++++++------ crates/person/src/person.rs | 9 +- crates/state/Cargo.toml | 2 +- crates/state/src/lib.rs | 19 +- 10 files changed, 393 insertions(+), 745 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c4afe4d..90d384c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,12 +85,6 @@ dependencies = [ "equator", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -152,9 +146,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "ar_archive_writer" @@ -536,15 +530,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - [[package]] name = "atomic" version = "0.5.3" @@ -1279,7 +1264,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1364,12 +1349,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "const-random" version = "0.1.18" @@ -1590,21 +1569,22 @@ dependencies = [ [[package]] name = "cosmic-text" -version = "0.14.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" +checksum = "8c5c9868e64aa6c5410629a83450e142c80e721c727a5bc0fb18107af6c2d66b" dependencies = [ "bitflags 2.10.0", - "fontdb 0.16.2", + "fontdb", + "harfrust", + "linebender_resource_handle", "log", "rangemap", - "rustc-hash 1.1.0", - "rustybuzz 0.14.1", + "rustc-hash 2.1.1", "self_cell", + "skrifa 0.40.0", "smol_str", "swash", "sys-locale", - "ttf-parser 0.21.1", "unicode-bidi", "unicode-linebreak", "unicode-script", @@ -1620,21 +1600,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.5.0" @@ -1729,17 +1694,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - [[package]] name = "derive_more" version = "0.99.20" @@ -1756,7 +1710,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "proc-macro2", "quote", @@ -1788,7 +1742,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", "subtle", ] @@ -1893,12 +1846,6 @@ dependencies = [ "ui", ] -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "downcast-rs" version = "1.2.1" @@ -1958,9 +1905,6 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -dependencies = [ - "serde", -] [[package]] name = "embed-resource" @@ -2078,17 +2022,6 @@ dependencies = [ "svg_fmt", ] -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - [[package]] name = "euclid" version = "0.22.13" @@ -2301,6 +2234,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "font-types" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4d2d0cf79d38430cc9dc9aadec84774bff2e1ba30ae2bf6c16cfce9385a23" +dependencies = [ + "bytemuck", +] + [[package]] name = "fontconfig-parser" version = "0.5.8" @@ -2310,20 +2252,6 @@ dependencies = [ "roxmltree", ] -[[package]] -name = "fontdb" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2 0.9.9", - "slotmap", - "tinyvec", - "ttf-parser 0.20.0", -] - [[package]] name = "fontdb" version = "0.23.0" @@ -2335,7 +2263,7 @@ dependencies = [ "memmap2 0.9.9", "slotmap", "tinyvec", - "ttf-parser 0.25.1", + "ttf-parser", ] [[package]] @@ -2477,17 +2405,6 @@ dependencies = [ "futures-util", ] -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - [[package]] name = "futures-io" version = "0.3.31" @@ -2711,7 +2628,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2813,7 +2730,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2824,7 +2741,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "anyhow", "gpui", @@ -2869,6 +2786,19 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "harfrust" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9f40651a03bc0f7316bd75267ff5767e93017ef3cfffe76c6aa7252cc5a31c" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "core_maths", + "read-fonts 0.37.0", + "smallvec", +] + [[package]] name = "hashbrown" version = "0.9.1" @@ -2890,8 +2820,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash", ] @@ -2901,15 +2829,6 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "heck" version = "0.4.1" @@ -3057,7 +2976,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "anyhow", "async-compression", @@ -3082,7 +3001,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3629,15 +3548,10 @@ dependencies = [ ] [[package]] -name = "libsqlite3-sys" -version = "0.30.1" +name = "linebender_resource_handle" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" [[package]] name = "linicon" @@ -3843,7 +3757,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "anyhow", "bindgen", @@ -4139,13 +4053,14 @@ dependencies = [ ] [[package]] -name = "nostr-gossip-sqlite" +name = "nostr-gossip-memory" version = "0.44.0" source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ + "indexmap", + "lru", "nostr", "nostr-gossip", - "sqlx", "tokio", ] @@ -4691,15 +4606,6 @@ dependencies = [ "hmac", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -4709,7 +4615,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "collections", "serde", @@ -4836,27 +4742,6 @@ dependencies = [ "futures-io", ] -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - [[package]] name = "pkg-config" version = "0.3.32" @@ -5315,7 +5200,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" dependencies = [ "bytemuck", - "font-types", + "font-types 0.10.1", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.11.0", ] [[package]] @@ -5379,7 +5275,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "derive_refineable", ] @@ -5478,7 +5374,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "anyhow", "bytes", @@ -5533,7 +5429,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "arrayvec", "log", @@ -5551,26 +5447,6 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "signature", - "spki", - "subtle", - "zeroize", -] - [[package]] name = "rust-embed" version = "8.11.0" @@ -5761,23 +5637,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "rustybuzz" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" -dependencies = [ - "bitflags 2.10.0", - "bytemuck", - "libm", - "smallvec", - "ttf-parser 0.21.1", - "unicode-bidi-mirroring 0.2.0", - "unicode-ccc 0.2.0", - "unicode-properties", - "unicode-script", -] - [[package]] name = "rustybuzz" version = "0.20.1" @@ -5789,9 +5648,9 @@ dependencies = [ "core_maths", "log", "smallvec", - "ttf-parser 0.25.1", - "unicode-bidi-mirroring 0.4.0", - "unicode-ccc 0.4.0", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", "unicode-properties", "unicode-script", ] @@ -5832,7 +5691,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "async-task", "backtrace", @@ -6179,16 +6038,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - [[package]] name = "simd-adler32" version = "0.3.8" @@ -6226,7 +6075,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.35.0", +] + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts 0.37.0", ] [[package]] @@ -6249,9 +6108,6 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] [[package]] name = "smol" @@ -6272,9 +6128,9 @@ dependencies = [ [[package]] name = "smol_str" -version = "0.2.2" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +checksum = "0f7a918bd2a9951d18ee6e48f076843e8e73a9a5d22cf05bcd4b7a81bdd04e17" [[package]] name = "socket2" @@ -6313,204 +6169,6 @@ dependencies = [ "bitflags 2.10.0", ] -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sqlx" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" -dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", -] - -[[package]] -name = "sqlx-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" -dependencies = [ - "base64", - "bytes", - "crc", - "crossbeam-queue", - "either", - "event-listener 5.4.1", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.15.5", - "hashlink", - "indexmap", - "log", - "memchr", - "once_cell", - "percent-encoding", - "serde", - "serde_json", - "sha2", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tracing", - "url", -] - -[[package]] -name = "sqlx-macros" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn 2.0.114", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" -dependencies = [ - "dotenvy", - "either", - "heck 0.5.0", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn 2.0.114", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" -dependencies = [ - "atoi", - "base64", - "bitflags 2.10.0", - "byteorder", - "bytes", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand 0.8.5", - "rsa", - "serde", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror 2.0.18", - "tracing", - "whoami", -] - -[[package]] -name = "sqlx-postgres" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" -dependencies = [ - "atoi", - "base64", - "bitflags 2.10.0", - "byteorder", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "rand 0.8.5", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror 2.0.18", - "tracing", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" -dependencies = [ - "atoi", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "serde_urlencoded", - "sqlx-core", - "thiserror 2.0.18", - "tracing", - "url", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -6562,7 +6220,7 @@ dependencies = [ "gpui_tokio", "log", "nostr-connect", - "nostr-gossip-sqlite", + "nostr-gossip-memory", "nostr-lmdb", "nostr-sdk", "petname", @@ -6589,17 +6247,6 @@ dependencies = [ "float-cmp", ] -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - [[package]] name = "strsim" version = "0.11.1" @@ -6658,7 +6305,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "arrayvec", "log", @@ -6767,7 +6414,7 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47846491253e976bdd07d0f9cc24b7daf24720d11309302ccbbc6e6b6e53550a" dependencies = [ - "skrifa", + "skrifa 0.37.0", "yazi", "zeno", ] @@ -7393,18 +7040,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "ttf-parser" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" - -[[package]] -name = "ttf-parser" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" - [[package]] name = "ttf-parser" version = "0.25.1" @@ -7491,24 +7126,12 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" -[[package]] -name = "unicode-bidi-mirroring" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" - [[package]] name = "unicode-bidi-mirroring" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" -[[package]] -name = "unicode-ccc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" - [[package]] name = "unicode-ccc" version = "0.4.0" @@ -7604,13 +7227,13 @@ dependencies = [ "base64", "data-url", "flate2", - "fontdb 0.23.0", + "fontdb", "imagesize", "kurbo", "log", "pico-args", "roxmltree", - "rustybuzz 0.20.1", + "rustybuzz", "simplecss", "siphasher", "strict-num", @@ -7643,7 +7266,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "anyhow", "async-fs", @@ -7681,7 +7304,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "perf", "quote", @@ -8125,7 +7748,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.59.0", ] [[package]] @@ -9062,18 +8685,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.38" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.38" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", @@ -9157,7 +8780,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "anyhow", "chrono", @@ -9174,7 +8797,7 @@ checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" dependencies = [ "tracing", "tracing-subscriber", @@ -9185,7 +8808,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#49c777779a63a0f44abec3ba7bd490c8b1552667" +source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index 251dbae..5d2a2da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" } nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" } nostr-connect = { git = "https://github.com/rust-nostr/nostr" } nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] } -nostr-gossip-sqlite = { git = "https://github.com/rust-nostr/nostr" } +nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" } # Others anyhow = "1.0.44" diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index a08e146..001e0e6 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -146,15 +146,15 @@ impl ChatRegistry { }) .ok(); } - NostrEvent::Eose => { + NostrEvent::Unwrapping(status) => { this.update(cx, |this, cx| { + this.set_loading(status, cx); this.get_rooms(cx); }) .ok(); } - NostrEvent::Unwrapping(status) => { + NostrEvent::Eose => { this.update(cx, |this, cx| { - this.set_loading(status, cx); this.get_rooms(cx); }) .ok(); @@ -310,10 +310,24 @@ impl ChatRegistry { /// Add a new room to the start of list. pub fn add_room(&mut self, room: I, cx: &mut Context) where - I: Into, + I: Into + 'static, { - self.rooms.insert(0, cx.new(|_| room.into())); - cx.notify(); + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + self.tasks.push(cx.spawn(async move |this, cx| { + if let Some(signer) = client.signer() { + if let Ok(public_key) = signer.get_public_key().await { + this.update(cx, |this, cx| { + this.rooms + .insert(0, cx.new(|_| room.into().organize(&public_key))); + cx.emit(ChatEvent::Ping); + cx.notify(); + }) + .ok(); + } + } + })); } /// Emit an open room event. @@ -407,23 +421,20 @@ impl ChatRegistry { pub fn get_rooms(&mut self, cx: &mut Context) { let task = self.get_rooms_from_database(cx); - self.tasks.push( - // Run and finished in the background - cx.spawn(async move |this, cx| { - match task.await { - Ok(rooms) => { - this.update(cx, move |this, cx| { - this.extend_rooms(rooms, cx); - this.sort(cx); - }) - .ok(); - } - Err(e) => { - log::error!("Failed to load rooms: {e}") - } - }; - }), - ); + self.tasks.push(cx.spawn(async move |this, cx| { + match task.await { + Ok(rooms) => { + this.update(cx, move |this, cx| { + this.extend_rooms(rooms, cx); + this.sort(cx); + }) + .ok(); + } + Err(e) => { + log::error!("Failed to load rooms: {e}") + } + }; + })); } /// Create a task to load rooms from the database @@ -434,8 +445,11 @@ impl ChatRegistry { cx.background_spawn(async move { let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; + + // Get contacts let contacts = client.database().contacts_public_keys(public_key).await?; + // Construct authored filter let authored_filter = Filter::new() .kind(Kind::ApplicationSpecificData) .custom_tag(SingleLetterTag::lowercase(Alphabet::A), public_key); @@ -443,6 +457,7 @@ impl ChatRegistry { // Get all authored events let authored = client.database().query(authored_filter).await?; + // Construct addressed filter let addressed_filter = Filter::new() .kind(Kind::ApplicationSpecificData) .custom_tag(SingleLetterTag::lowercase(Alphabet::P), public_key); @@ -453,6 +468,7 @@ impl ChatRegistry { // Merge authored and addressed events let events = authored.merge(addressed); + // Collect results let mut rooms: HashSet = HashSet::new(); let mut grouped: HashMap> = HashMap::new(); @@ -468,24 +484,21 @@ impl ChatRegistry { for (_id, mut messages) in grouped.into_iter() { messages.sort_by_key(|m| Reverse(m.created_at)); + // Always use the latest message let Some(latest) = messages.first() else { continue; }; - let mut room = Room::from(latest); - - if rooms.iter().any(|r| r.id == room.id) { - continue; - } - - let mut public_keys = room.members(); - public_keys.retain(|pk| pk != &public_key); + // Construct the room from the latest message. + // + // Call `.organize` to ensure the current user is at the end of the list. + let mut room = Room::from(latest).organize(&public_key); // Check if the user has responded to the room let user_sent = messages.iter().any(|m| m.pubkey == public_key); // Check if public keys are from the user's contacts - let is_contact = public_keys.iter().any(|k| contacts.contains(k)); + let is_contact = room.members.iter().any(|k| contacts.contains(k)); // Set the room's kind based on status if user_sent || is_contact { @@ -499,6 +512,24 @@ impl ChatRegistry { }) } + /// Parse a nostr event into a message and push it to the belonging room + /// + /// If the room doesn't exist, it will be created. + /// Updates room ordering based on the most recent messages. + pub fn new_message(&mut self, message: NewMessage, cx: &mut Context) { + match self.rooms.iter().find(|e| e.read(cx).id == message.room) { + Some(room) => { + room.update(cx, |this, cx| { + this.push_message(message, cx); + }); + } + None => { + // Push the new room to the front of the list + self.add_room(message.rumor, cx); + } + } + } + /// Trigger a refresh of the opened chat rooms by their IDs pub fn refresh_rooms(&mut self, ids: Option>, cx: &mut Context) { if let Some(ids) = ids { @@ -512,53 +543,6 @@ impl ChatRegistry { } } - /// Parse a nostr event into a message and push it to the belonging room - /// - /// If the room doesn't exist, it will be created. - /// Updates room ordering based on the most recent messages. - pub fn new_message(&mut self, message: NewMessage, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - // Get the unique id - let id = message.rumor.uniq_id(); - // Get the author - let author = message.rumor.pubkey; - - match self.rooms.iter().find(|room| room.read(cx).id == id) { - Some(room) => { - let new_message = message.rumor.created_at > room.read(cx).created_at; - let created_at = message.rumor.created_at; - - // Update room - room.update(cx, |this, cx| { - // Update the last timestamp if the new message is newer - if new_message { - this.set_created_at(created_at, cx); - } - - // Set this room is ongoing if the new message is from current user - // if author == nostr.read(cx).identity().read(cx).public_key() { - // this.set_ongoing(cx); - // } - - // Emit the new message to the room - this.emit_message(message, cx); - }); - - // Resort all rooms in the registry by their created at (after updated) - if new_message { - self.sort(cx); - } - } - None => { - // Push the new room to the front of the list - self.add_room(&message.rumor, cx); - - // Notify the UI about the new room - cx.emit(ChatEvent::Ping); - } - } - } - /// Unwraps a gift-wrapped event and processes its contents. async fn extract_rumor( client: &Client, diff --git a/crates/chat/src/message.rs b/crates/chat/src/message.rs index c4cfef6..5ec33a7 100644 --- a/crates/chat/src/message.rs +++ b/crates/chat/src/message.rs @@ -1,17 +1,25 @@ use std::hash::Hash; +use common::EventUtils; use nostr_sdk::prelude::*; /// New message. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct NewMessage { pub gift_wrap: EventId, + pub room: u64, pub rumor: UnsignedEvent, } impl NewMessage { pub fn new(gift_wrap: EventId, rumor: UnsignedEvent) -> Self { - Self { gift_wrap, rumor } + let room = rumor.uniq_id(); + + Self { + gift_wrap, + room, + rumor, + } } } diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index bdea83e..638449c 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -11,7 +11,7 @@ use nostr_sdk::prelude::*; use person::{Person, PersonRegistry}; use state::{tracker, NostrRegistry}; -use crate::NewMessage; +use crate::{ChatRegistry, NewMessage}; const SEND_RETRY: usize = 10; @@ -99,16 +99,20 @@ pub enum RoomKind { Ongoing, } -#[derive(Debug)] +#[derive(Debug, Clone)] 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, + /// All members of the room - pub members: Vec, + pub(super) members: Vec, + /// Kind pub kind: RoomKind, } @@ -145,11 +149,7 @@ impl From<&UnsignedEvent> for Room { fn from(val: &UnsignedEvent) -> Self { let id = val.uniq_id(); let created_at = val.created_at; - - // Get the members from the event's tags and event's pubkey let members = val.extract_public_keys(); - - // Get subject from tags let subject = val .tags .find(TagKind::Subject) @@ -165,6 +165,12 @@ impl From<&UnsignedEvent> for Room { } } +impl From for Room { + fn from(val: UnsignedEvent) -> Self { + Room::from(&val) + } +} + impl Room { /// Constructs a new room with the given receiver and tags. pub fn new(author: PublicKey, receivers: T) -> Self @@ -172,16 +178,30 @@ impl Room { T: IntoIterator, { let tags = Tags::from_list(receivers.into_iter().map(Tag::public_key).collect()); + // Construct an unsigned event for a direct message + // + // WARNING: never sign this event let mut event = EventBuilder::new(Kind::PrivateDirectMessage, "") .tags(tags) .build(author); - // Generate event ID + // Ensure that the ID is set event.ensure_id(); Room::from(&event) } + /// Organizes the members of the room by moving the target member to the end. + /// + /// Always call this function to ensure the current user is at the end of the list. + pub fn organize(mut self, target: &PublicKey) -> Self { + if let Some(index) = self.members.iter().position(|member| member == target) { + let member = self.members.remove(index); + self.members.push(member); + } + self + } + /// Sets the kind of the room and returns the modified room pub fn kind(mut self, kind: RoomKind) -> Self { self.kind = kind; @@ -275,9 +295,21 @@ impl Room { } } - /// Emits a new message signal to the current room - pub fn emit_message(&self, message: NewMessage, cx: &mut Context) { + /// Push a new message to the current room + pub fn push_message(&mut self, message: NewMessage, cx: &mut Context) { + let created_at = message.rumor.created_at; + let new_message = created_at > self.created_at; + + // Emit the incoming message event cx.emit(RoomEvent::Incoming(message)); + + if new_message { + self.set_created_at(created_at, cx); + // Sort chats after emitting a new message + ChatRegistry::global(cx).update(cx, |this, cx| { + this.sort(cx); + }); + } } /// Emits a signal to reload the current room's messages. diff --git a/crates/common/src/display.rs b/crates/common/src/display.rs index ab7c215..45a4dfd 100644 --- a/crates/common/src/display.rs +++ b/crates/common/src/display.rs @@ -12,44 +12,6 @@ const SECONDS_IN_MINUTE: i64 = 60; const MINUTES_IN_HOUR: i64 = 60; const HOURS_IN_DAY: i64 = 24; const DAYS_IN_MONTH: i64 = 30; -const IMAGE_RESIZER: &str = "https://wsrv.nl"; - -pub trait RenderedProfile { - fn avatar(&self) -> SharedString; - fn display_name(&self) -> SharedString; -} - -impl RenderedProfile for Profile { - fn avatar(&self) -> SharedString { - self.metadata() - .picture - .as_ref() - .filter(|picture| !picture.is_empty()) - .map(|picture| { - let url = format!( - "{IMAGE_RESIZER}/?url={picture}&w=100&h=100&fit=cover&mask=circle&n=-1" - ); - url.into() - }) - .unwrap_or_else(|| "brand/avatar.png".into()) - } - - fn display_name(&self) -> SharedString { - if let Some(display_name) = self.metadata().display_name.as_ref() { - if !display_name.is_empty() { - return SharedString::from(display_name); - } - } - - if let Some(name) = self.metadata().name.as_ref() { - if !name.is_empty() { - return SharedString::from(name); - } - } - - SharedString::from(shorten_pubkey(self.public_key(), 4)) - } -} pub trait RenderedTimestamp { fn to_human_time(&self) -> SharedString; diff --git a/crates/coop/src/dialogs/screening.rs b/crates/coop/src/dialogs/screening.rs index 4ca6ccf..e27609a 100644 --- a/crates/coop/src/dialogs/screening.rs +++ b/crates/coop/src/dialogs/screening.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::time::Duration; use anyhow::{Context as AnyhowContext, Error}; -use common::{shorten_pubkey, RenderedProfile, RenderedTimestamp, BOOTSTRAP_RELAYS}; +use common::{shorten_pubkey, RenderedTimestamp, BOOTSTRAP_RELAYS}; use gpui::prelude::FluentBuilder; use gpui::{ div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity, @@ -11,7 +11,7 @@ use gpui::{ use nostr_sdk::prelude::*; use person::{Person, PersonRegistry}; use smallvec::{smallvec, SmallVec}; -use state::{NostrAddress, NostrRegistry}; +use state::{NostrAddress, NostrRegistry, TIMEOUT}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; @@ -22,56 +22,117 @@ pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity< cx.new(|cx| Screening::new(public_key, window, cx)) } +/// Screening pub struct Screening { - profile: Person, + /// Public Key of the person being screened. + public_key: PublicKey, + + /// Whether the person's address is verified. verified: bool, + + /// Whether the person is followed by current user. followed: bool, + + /// Last time the person was active. last_active: Option, - mutual_contacts: Vec, - _tasks: SmallVec<[Task<()>; 3]>, + + /// All mutual contacts of the person being screened. + mutual_contacts: Vec, + + /// Async tasks + tasks: SmallVec<[Task<()>; 3]>, } impl Screening { pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context) -> Self { - let http_client = cx.http_client(); - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&public_key, cx); - - let mut tasks = smallvec![]; - - // Check WOT - let contact_check: Task), Error>> = cx.background_spawn({ - let client = nostr.read(cx).client(); - async move { - let signer = client.signer().context("Signer not found")?; - let signer_pubkey = signer.get_public_key().await?; - - // Check if user is in contact list - let contacts = client.database().contacts_public_keys(signer_pubkey).await; - let followed = contacts.unwrap_or_default().contains(&public_key); - - // Check mutual contacts - let contact_list = Filter::new().kind(Kind::ContactList).pubkey(public_key); - let mut mutual_contacts = vec![]; - - if let Ok(events) = client.database().query(contact_list).await { - for event in events.into_iter().filter(|ev| ev.pubkey != signer_pubkey) { - if let Ok(metadata) = client.database().metadata(event.pubkey).await { - let profile = Profile::new(event.pubkey, metadata.unwrap_or_default()); - mutual_contacts.push(profile); - } - } - } - - Ok((followed, mutual_contacts)) - } + cx.defer_in(window, move |this, _window, cx| { + this.check_contact(cx); + this.check_wot(cx); + this.check_last_activity(cx); + this.verify_identifier(cx); }); - // Check the last activity - let activity_check = cx.background_spawn(async move { + Self { + public_key, + verified: false, + followed: false, + last_active: None, + mutual_contacts: vec![], + tasks: smallvec![], + } + } + + fn check_contact(&mut self, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + let public_key = self.public_key; + + let task: Task> = cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let signer_pubkey = signer.get_public_key().await?; + + // Check if user is in contact list + let contacts = client.database().contacts_public_keys(signer_pubkey).await; + let followed = contacts.unwrap_or_default().contains(&public_key); + + Ok(followed) + }); + + self.tasks.push(cx.spawn(async move |this, cx| { + let result = task.await.unwrap_or(false); + + this.update(cx, |this, cx| { + this.followed = result; + cx.notify(); + }) + .ok(); + })); + } + + fn check_wot(&mut self, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + let public_key = self.public_key; + + let task: Task, Error>> = cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let signer_pubkey = signer.get_public_key().await?; + + // Check mutual contacts + let filter = Filter::new().kind(Kind::ContactList).pubkey(public_key); + let mut mutual_contacts = vec![]; + + if let Ok(events) = client.database().query(filter).await { + for event in events.into_iter().filter(|ev| ev.pubkey != signer_pubkey) { + mutual_contacts.push(event.pubkey); + } + } + + Ok(mutual_contacts) + }); + + self.tasks.push(cx.spawn(async move |this, cx| { + match task.await { + Ok(contacts) => { + this.update(cx, |this, cx| { + this.mutual_contacts = contacts; + cx.notify(); + }) + .ok(); + } + Err(e) => { + log::error!("Failed to fetch mutual contacts: {}", e); + } + }; + })); + } + + fn check_last_activity(&mut self, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + let public_key = self.public_key; + + let task: Task> = cx.background_spawn(async move { let filter = Filter::new().author(public_key).limit(1); let mut activity: Option = None; @@ -83,7 +144,7 @@ impl Screening { if let Ok(mut stream) = client .stream_events(target) - .timeout(Duration::from_secs(2)) + .timeout(Duration::from_secs(TIMEOUT)) .await { while let Some((_url, event)) = stream.next().await { @@ -96,78 +157,61 @@ impl Screening { activity }); - // Verify the NIP05 address if available - let addr_check = profile.metadata().nip05.and_then(|address| { - Nip05Address::parse(&address).ok().map(|addr| { - cx.background_spawn(async move { addr.verify(&http_client, &public_key).await }) + self.tasks.push(cx.spawn(async move |this, cx| { + let result = task.await; + + this.update(cx, |this, cx| { + this.last_active = result; + cx.notify(); }) - }); - - tasks.push( - // Run the contact check in the background - cx.spawn_in(window, async move |this, cx| { - if let Ok((followed, mutual_contacts)) = contact_check.await { - this.update(cx, |this, cx| { - this.followed = followed; - this.mutual_contacts = mutual_contacts; - cx.notify(); - }) - .ok(); - } - }), - ); - - tasks.push( - // Run the activity check in the background - cx.spawn_in(window, async move |this, cx| { - let active = activity_check.await; - - this.update(cx, |this, cx| { - this.last_active = active; - cx.notify(); - }) - .ok(); - }), - ); - - tasks.push( - // Run the NIP-05 verification in the background - cx.spawn_in(window, async move |this, cx| { - if let Some(task) = addr_check { - if let Ok(verified) = task.await { - this.update(cx, |this, cx| { - this.verified = verified; - cx.notify(); - }) - .ok(); - } - } - }), - ); - - Self { - profile, - verified: false, - followed: false, - last_active: None, - mutual_contacts: vec![], - _tasks: tasks, - } + .ok(); + })); } - fn address(&self, _cx: &Context) -> Option { - self.profile.metadata().nip05 + fn verify_identifier(&mut self, cx: &mut Context) { + let http_client = cx.http_client(); + let public_key = self.public_key; + + // Skip if the user doesn't have a NIP-05 identifier + let Some(address) = self.address(cx) else { + return; + }; + + let task: Task> = + cx.background_spawn(async move { address.verify(&http_client, &public_key).await }); + + self.tasks.push(cx.spawn(async move |this, cx| { + let result = task.await.unwrap_or(false); + + this.update(cx, |this, cx| { + this.verified = result; + cx.notify(); + }) + .ok(); + })); } - fn open_njump(&mut self, _window: &mut Window, cx: &mut App) { - let Ok(bech32) = self.profile.public_key().to_bech32(); + fn profile(&self, cx: &Context) -> Person { + let persons = PersonRegistry::global(cx); + persons.read(cx).get(&self.public_key, cx) + } + + fn address(&self, cx: &Context) -> Option { + self.profile(cx) + .metadata() + .nip05 + .and_then(|addr| Nip05Address::parse(&addr).ok()) + } + + fn open_njump(&mut self, _window: &mut Window, cx: &mut Context) { + let Ok(bech32) = self.profile(cx).public_key().to_bech32(); cx.open_url(&format!("https://njump.me/{bech32}")); } fn report(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = self.profile.public_key(); + let public_key = self.public_key; let task: Task> = cx.background_spawn(async move { let tag = Tag::public_key_report(public_key, Report::Impersonation); @@ -180,7 +224,7 @@ impl Screening { Ok(()) }); - cx.spawn_in(window, async move |_, cx| { + self.tasks.push(cx.spawn_in(window, async move |_, cx| { if task.await.is_ok() { cx.update(|window, cx| { window.close_modal(cx); @@ -188,8 +232,7 @@ impl Screening { }) .ok(); } - }) - .detach(); + })); } fn mutual_contacts(&mut self, window: &mut Window, cx: &mut Context) { @@ -202,25 +245,27 @@ impl Screening { this.title(SharedString::from("Mutual contacts")).child( v_flex().gap_1().pb_4().child( uniform_list("contacts", total, move |range, _window, cx| { + let persons = PersonRegistry::global(cx); let mut items = Vec::with_capacity(total); for ix in range { - if let Some(contact) = contacts.get(ix) { - items.push( - h_flex() - .h_11() - .w_full() - .px_2() - .gap_1p5() - .rounded(cx.theme().radius) - .text_sm() - .hover(|this| { - this.bg(cx.theme().elevated_surface_background) - }) - .child(Avatar::new(contact.avatar()).size(rems(1.75))) - .child(contact.display_name()), - ); - } + let Some(contact) = contacts.get(ix) else { + continue; + }; + let profile = persons.read(cx).get(contact, cx); + + items.push( + h_flex() + .h_11() + .w_full() + .px_2() + .gap_1p5() + .rounded(cx.theme().radius) + .text_sm() + .hover(|this| this.bg(cx.theme().elevated_surface_background)) + .child(Avatar::new(profile.avatar()).size(rems(1.75))) + .child(profile.name()), + ); } items @@ -234,7 +279,9 @@ impl Screening { impl Render for Screening { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let shorten_pubkey = shorten_pubkey(self.profile.public_key(), 8); + let profile = self.profile(cx); + let shorten_pubkey = shorten_pubkey(self.public_key, 8); + let total_mutuals = self.mutual_contacts.len(); let last_active = self.last_active.map(|_| true); @@ -246,12 +293,12 @@ impl Render for Screening { .items_center() .justify_center() .text_center() - .child(Avatar::new(self.profile.avatar()).size(rems(4.))) + .child(Avatar::new(profile.avatar()).size(rems(4.))) .child( div() .font_semibold() .line_height(relative(1.25)) - .child(self.profile.name()), + .child(profile.name()), ), ) .child( diff --git a/crates/person/src/person.rs b/crates/person/src/person.rs index a66a2eb..cd6a2a8 100644 --- a/crates/person/src/person.rs +++ b/crates/person/src/person.rs @@ -5,6 +5,8 @@ use gpui::SharedString; use nostr_sdk::prelude::*; use state::Announcement; +const IMAGE_RESIZER: &str = "https://wsrv.nl"; + /// Person #[derive(Debug, Clone)] pub struct Person { @@ -86,7 +88,12 @@ impl Person { .picture .as_ref() .filter(|picture| !picture.is_empty()) - .map(|picture| picture.into()) + .map(|picture| { + let url = format!( + "{IMAGE_RESIZER}/?url={picture}&w=100&h=100&fit=cover&mask=circle&n=-1" + ); + url.into() + }) .unwrap_or_else(|| "brand/avatar.png".into()) } diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index bc3b846..2d04fc2 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -10,7 +10,7 @@ common = { path = "../common" } nostr-sdk.workspace = true nostr-lmdb.workspace = true nostr-connect.workspace = true -nostr-gossip-sqlite.workspace = true +nostr-gossip-memory.workspace = true gpui.workspace = true gpui_tokio.workspace = true diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index cd8830d..8b7992f 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -6,9 +6,8 @@ use std::time::Duration; use anyhow::{anyhow, Context as AnyhowContext, Error}; use common::{config_dir, CLIENT_NAME}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; -use gpui_tokio::Tokio; use nostr_connect::prelude::*; -use nostr_gossip_sqlite::prelude::*; +use nostr_gossip_memory::prelude::*; use nostr_lmdb::NostrLmdb; use nostr_sdk::prelude::*; @@ -125,20 +124,6 @@ impl NostrRegistry { .expect("Failed to initialize database") }); - // Use tokio to spawn a task to build the gossip instance - let build_gossip_sqlite = Tokio::spawn(cx, async move { - NostrGossipSqlite::open(config_dir().join("gossip")) - .await - .expect("Failed to initialize gossip") - }); - - // Initialize the nostr gossip instance - let gossip = cx.foreground_executor().block_on(async move { - build_gossip_sqlite - .await - .expect("Failed to initialize gossip") - }); - // Construct the nostr signer let app_keys = Self::create_or_init_app_keys().unwrap_or(Keys::generate()); let signer = Arc::new(CoopSigner::new(app_keys.clone())); @@ -150,7 +135,7 @@ impl NostrRegistry { // Construct the nostr client let client = ClientBuilder::default() .signer(signer.clone()) - .gossip(gossip) + .gossip(NostrGossipMemory::unbounded()) .database(lmdb) .automatic_authentication(false) .verify_subscriptions(false) -- 2.49.1 From 6cce0d8bea26a4c180f356bad2a9a6502a836965 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 9 Feb 2026 10:12:21 +0700 Subject: [PATCH 05/11] set default data for newly created identity --- Cargo.lock | 2 +- crates/auto_update/src/lib.rs | 1 - crates/chat/src/room.rs | 1 - crates/common/Cargo.toml | 1 - crates/common/src/constants.rs | 28 ------ crates/common/src/lib.rs | 55 ---------- crates/coop/src/dialogs/screening.rs | 4 +- crates/coop/src/main.rs | 2 +- crates/coop/src/panels/relay_list.rs | 3 +- crates/device/src/lib.rs | 6 +- crates/person/src/lib.rs | 4 +- crates/state/Cargo.toml | 1 + crates/state/src/constants.rs | 60 +++++++++++ crates/state/src/lib.rs | 144 ++++++++++++++++----------- 14 files changed, 158 insertions(+), 154 deletions(-) delete mode 100644 crates/common/src/constants.rs create mode 100644 crates/state/src/constants.rs diff --git a/Cargo.lock b/Cargo.lock index 90d384c..6830357 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1319,7 +1319,6 @@ dependencies = [ "reqwest", "smallvec", "smol", - "whoami", ] [[package]] @@ -6230,6 +6229,7 @@ dependencies = [ "serde_json", "smol", "webbrowser", + "whoami", ] [[package]] diff --git a/crates/auto_update/src/lib.rs b/crates/auto_update/src/lib.rs index a525f40..f680a0a 100644 --- a/crates/auto_update/src/lib.rs +++ b/crates/auto_update/src/lib.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, Context as AnyhowContext, Error}; -use common::BOOTSTRAP_RELAYS; use gpui::http_client::{AsyncBody, HttpClient}; use gpui::{ App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task, diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index 638449c..516e785 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -440,7 +440,6 @@ impl Room { // Construct a direct message event // // WARNING: never sign and send this event to relays - // TODO let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content) .tags(tags) .build(Keys::generate().public_key()); diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 857374e..3fd083a 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -19,5 +19,4 @@ log.workspace = true dirs = "5.0" qrcode = "0.14.1" -whoami = "1.6.1" nostr = { git = "https://github.com/rust-nostr/nostr" } diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs deleted file mode 100644 index 9cad0a5..0000000 --- a/crates/common/src/constants.rs +++ /dev/null @@ -1,28 +0,0 @@ -pub const CLIENT_NAME: &str = "Coop"; -pub const APP_ID: &str = "su.reya.coop"; - -/// Bootstrap Relays. -pub const BOOTSTRAP_RELAYS: [&str; 4] = [ - "wss://relay.damus.io", - "wss://relay.primal.net", - "wss://relay.nos.social", - "wss://user.kindpag.es", -]; - -/// Search Relays. -pub const SEARCH_RELAYS: [&str; 2] = ["wss://search.nos.today", "wss://relay.noswhere.com"]; - -/// Default relay for Nostr Connect -pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app"; - -/// Default retry count for fetching NIP-17 relays -pub const RELAY_RETRY: u64 = 2; - -/// Default retry count for sending messages -pub const SEND_RETRY: u64 = 10; - -/// Default timeout (in seconds) for Nostr Connect -pub const NOSTR_CONNECT_TIMEOUT: u64 = 200; - -/// Default timeout (in seconds) for Nostr Connect (Bunker) -pub const BUNKER_TIMEOUT: u64 = 30; diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 653d1dc..75511a4 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -1,66 +1,11 @@ -use std::sync::OnceLock; - -pub use constants::*; pub use debounced_delay::*; pub use display::*; pub use event::*; pub use nip96::*; -use nostr_sdk::prelude::*; pub use paths::*; -mod constants; mod debounced_delay; mod display; mod event; mod nip96; mod paths; - -static APP_NAME: OnceLock = OnceLock::new(); -static NIP65_RELAYS: OnceLock)>> = OnceLock::new(); -static NIP17_RELAYS: OnceLock> = OnceLock::new(); - -/// Get the app name -pub fn app_name() -> &'static String { - APP_NAME.get_or_init(|| { - let devicename = whoami::devicename(); - let platform = whoami::platform(); - - format!("{CLIENT_NAME} on {platform} ({devicename})") - }) -} - -/// Default NIP-65 Relays. Used for new account -pub fn default_nip65_relays() -> &'static Vec<(RelayUrl, Option)> { - NIP65_RELAYS.get_or_init(|| { - vec![ - ( - RelayUrl::parse("wss://nostr.mom").unwrap(), - Some(RelayMetadata::Read), - ), - ( - RelayUrl::parse("wss://nostr.bitcoiner.social").unwrap(), - Some(RelayMetadata::Read), - ), - ( - RelayUrl::parse("wss://nos.lol").unwrap(), - Some(RelayMetadata::Write), - ), - ( - RelayUrl::parse("wss://relay.snort.social").unwrap(), - Some(RelayMetadata::Write), - ), - (RelayUrl::parse("wss://relay.primal.net").unwrap(), None), - (RelayUrl::parse("wss://relay.damus.io").unwrap(), None), - ] - }) -} - -/// Default NIP-17 Relays. Used for new account -pub fn default_nip17_relays() -> &'static Vec { - NIP17_RELAYS.get_or_init(|| { - vec![ - RelayUrl::parse("wss://nip17.com").unwrap(), - RelayUrl::parse("wss://auth.nostr1.com").unwrap(), - ] - }) -} diff --git a/crates/coop/src/dialogs/screening.rs b/crates/coop/src/dialogs/screening.rs index e27609a..4c2e617 100644 --- a/crates/coop/src/dialogs/screening.rs +++ b/crates/coop/src/dialogs/screening.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::time::Duration; use anyhow::{Context as AnyhowContext, Error}; -use common::{shorten_pubkey, RenderedTimestamp, BOOTSTRAP_RELAYS}; +use common::{shorten_pubkey, RenderedTimestamp}; use gpui::prelude::FluentBuilder; use gpui::{ div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity, @@ -11,7 +11,7 @@ use gpui::{ use nostr_sdk::prelude::*; use person::{Person, PersonRegistry}; use smallvec::{smallvec, SmallVec}; -use state::{NostrAddress, NostrRegistry, TIMEOUT}; +use state::{NostrAddress, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index 6a40e92..060910c 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -1,12 +1,12 @@ use std::sync::{Arc, Mutex}; use assets::Assets; -use common::{APP_ID, CLIENT_NAME}; use gpui::{ point, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString, Size, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions, }; +use state::{APP_ID, CLIENT_NAME}; use ui::Root; use crate::actions::Quit; diff --git a/crates/coop/src/panels/relay_list.rs b/crates/coop/src/panels/relay_list.rs index 262556b..3c2c930 100644 --- a/crates/coop/src/panels/relay_list.rs +++ b/crates/coop/src/panels/relay_list.rs @@ -2,7 +2,6 @@ use std::collections::HashSet; use std::time::Duration; use anyhow::{anyhow, Context as AnyhowContext, Error}; -use common::BOOTSTRAP_RELAYS; use dock::panel::{Panel, PanelEvent}; use gpui::prelude::FluentBuilder; use gpui::{ @@ -12,7 +11,7 @@ use gpui::{ }; use nostr_sdk::prelude::*; use smallvec::{smallvec, SmallVec}; -use state::NostrRegistry; +use state::{NostrRegistry, BOOTSTRAP_RELAYS}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::input::{InputEvent, InputState, TextInput}; diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index d94ac41..4a02aeb 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -3,15 +3,15 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, Context as AnyhowContext, Error}; -use common::app_name; -pub use device::*; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use nostr_sdk::prelude::*; use smallvec::{smallvec, SmallVec}; -use state::{NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT, USER_GIFTWRAP}; +use state::{app_name, NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT, USER_GIFTWRAP}; mod device; +pub use device::*; + const IDENTIFIER: &str = "coop:device"; pub fn init(cx: &mut App) { diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs index 3e1ef3e..3d15c38 100644 --- a/crates/person/src/lib.rs +++ b/crates/person/src/lib.rs @@ -4,12 +4,12 @@ use std::rc::Rc; use std::time::Duration; use anyhow::{anyhow, Error}; -use common::{EventUtils, BOOTSTRAP_RELAYS}; +use common::EventUtils; use gpui::{App, AppContext, Context, Entity, Global, Task}; use nostr_sdk::prelude::*; pub use person::*; use smallvec::{smallvec, SmallVec}; -use state::{Announcement, NostrRegistry, TIMEOUT}; +use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT}; mod person; diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index 2d04fc2..009c0a3 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -25,3 +25,4 @@ serde_json.workspace = true rustls = "0.23" petname = "2.0.2" +whoami = "1.6.1" diff --git a/crates/state/src/constants.rs b/crates/state/src/constants.rs new file mode 100644 index 0000000..5e54cfb --- /dev/null +++ b/crates/state/src/constants.rs @@ -0,0 +1,60 @@ +use std::sync::OnceLock; + +/// Client name (Application name) +pub const CLIENT_NAME: &str = "Coop"; + +/// COOP's public key +pub const COOP_PUBKEY: &str = "npub126kl5fruqan90py77gf6pvfvygefl2mu2ukew6xdx5pc5uqscwgsnkgarv"; + +/// App ID +pub const APP_ID: &str = "su.reya.coop"; + +/// Keyring name +pub const KEYRING: &str = "Coop Secret Storage"; + +/// Default timeout for subscription +pub const TIMEOUT: u64 = 3; + +/// Default delay for searching +pub const FIND_DELAY: u64 = 600; + +/// Default limit for searching +pub const FIND_LIMIT: usize = 20; + +/// Default timeout for Nostr Connect +pub const NOSTR_CONNECT_TIMEOUT: u64 = 200; + +/// Default Nostr Connect relay +pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app"; + +/// Default subscription id for device gift wrap events +pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps"; + +/// Default subscription id for user gift wrap events +pub const USER_GIFTWRAP: &str = "user-gift-wraps"; + +/// Default vertex relays +pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"]; + +/// Default search relays +pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"]; + +/// Default bootstrap relays +pub const BOOTSTRAP_RELAYS: [&str; 4] = [ + "wss://relay.damus.io", + "wss://relay.primal.net", + "wss://relay.nos.social", + "wss://user.kindpag.es", +]; + +static APP_NAME: OnceLock = OnceLock::new(); + +/// Get the app name +pub fn app_name() -> &'static String { + APP_NAME.get_or_init(|| { + let devicename = whoami::devicename(); + let platform = whoami::platform(); + + format!("{CLIENT_NAME} on {platform} ({devicename})") + }) +} diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 8b7992f..66615f4 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -4,53 +4,25 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, Context as AnyhowContext, Error}; -use common::{config_dir, CLIENT_NAME}; +use common::config_dir; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use nostr_connect::prelude::*; use nostr_gossip_memory::prelude::*; use nostr_lmdb::NostrLmdb; use nostr_sdk::prelude::*; +mod constants; mod device; mod event; -mod gossip; mod nip05; mod signer; +pub use constants::*; pub use device::*; pub use event::*; -pub use gossip::*; pub use nip05::*; pub use signer::*; -/// Default timeout for subscription -pub const TIMEOUT: u64 = 3; -/// Default delay for searching -pub const FIND_DELAY: u64 = 600; -/// Default limit for searching -pub const FIND_LIMIT: usize = 20; -/// Default timeout for Nostr Connect -pub const NOSTR_CONNECT_TIMEOUT: u64 = 200; -/// Default Nostr Connect relay -pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app"; -/// Default subscription id for device gift wrap events -pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps"; -/// Default subscription id for user gift wrap events -pub const USER_GIFTWRAP: &str = "user-gift-wraps"; -/// Default avatar for new users -pub const DEFAULT_AVATAR: &str = "https://image.nostr.build/93bb6084457a42620849b6827f3f34f111ae5a4ac728638a989d4ed4b4bb3ac8.png"; -/// Default vertex relays -pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"]; -/// Default search relays -pub const SEARCH_RELAYS: [&str; 2] = ["wss://search.nos.today", "wss://relay.noswhere.com"]; -/// Default bootstrap relays -pub const BOOTSTRAP_RELAYS: [&str; 4] = [ - "wss://relay.damus.io", - "wss://relay.primal.net", - "wss://relay.nos.social", - "wss://user.kindpag.es", -]; - pub fn init(cx: &mut App) { // rustls uses the `aws_lc_rs` provider by default // This only errors if the default provider has already @@ -626,8 +598,8 @@ impl NostrRegistry { let builder = EventBuilder::metadata(&metadata); let event = client.sign_event_builder(builder).await?; - // Send event to user's write relayss - client.send_event(&event).to_nip65().await?; + // Send event to user's relays + client.send_event(&event).await?; Ok(()) }) @@ -635,7 +607,7 @@ impl NostrRegistry { /// Get local stored identity fn get_identity(&mut self, cx: &mut Context) { - let read_credential = cx.read_credentials(CLIENT_NAME); + let read_credential = cx.read_credentials(KEYRING); self.tasks.push(cx.spawn(async move |this, cx| { match read_credential.await { @@ -662,42 +634,72 @@ impl NostrRegistry { /// Create a new identity fn create_identity(&mut self, cx: &mut Context) { + let client = self.client(); let keys = Keys::generate(); + let async_keys = keys.clone(); // Get write credential task let write_credential = cx.write_credentials( - CLIENT_NAME, + KEYRING, &keys.public_key().to_hex(), &keys.secret_key().to_secret_bytes(), ); - // Update the signer - self.set_signer(keys, false, cx); + // Run async tasks in background + let task: Task> = cx.background_spawn(async move { + // Build and sign the relay list event + let relay_list = default_relay_list(); + let event = EventBuilder::relay_list(relay_list) + .sign(&async_keys) + .await?; - // Generate a unique name and avatar for the identity - let name = petname::petname(2, "-").unwrap_or("Cooper".to_string()); - let avatar = Url::parse(DEFAULT_AVATAR).unwrap(); + // Publish relay list event + client.send_event(&event).await?; - // Construct metadata for the identity - let metadata = Metadata::new() - .display_name(&name) - .name(&name) - .picture(avatar); + // Build and sign the metadata event + let name = petname::petname(2, "-").unwrap_or("Cooper".to_string()); + let avatar = Url::parse(&format!("https://avatar.vercel.sh/{name}")).unwrap(); + let metadata = Metadata::new().display_name(&name).picture(avatar); + let event = EventBuilder::metadata(&metadata).sign(&async_keys).await?; - // Update user's metadata - let task = self.set_metadata(&metadata, cx); + // Publish metadata event + client.send_event(&event).await?; - // Spawn a task to write the credentials - cx.background_spawn(async move { - if let Err(e) = task.await { - log::error!("Failed to update metadata: {}", e); - } + // Build and sign the contact list event + let contacts = vec![Contact::new(PublicKey::parse(COOP_PUBKEY).unwrap())]; + let event = EventBuilder::contact_list(contacts) + .sign(&async_keys) + .await?; - if let Err(e) = write_credential.await { - log::error!("Failed to write credentials: {}", e); - } - }) - .detach(); + // Publish contact list event + client.send_event(&event).await?; + + // Build and sign the messaging relay list event + let relays = default_messaging_relays(); + let event = EventBuilder::nip17_relay_list(relays) + .sign(&async_keys) + .await?; + + // Publish messaging relay list event + client.send_event(&event).await?; + + // Write user's credentials to the system keyring + write_credential.await?; + + Ok(()) + }); + + self.tasks.push(cx.spawn(async move |this, cx| { + // Wait for the task to complete + task.await?; + + // Update the signer + this.update(cx, |this, cx| { + this.set_signer(keys, false, cx); + })?; + + Ok(()) + })); } /// Get local stored bunker connection @@ -922,6 +924,34 @@ impl NostrRegistry { } } +fn default_relay_list() -> Vec<(RelayUrl, Option)> { + vec![ + ( + RelayUrl::parse("wss://relay.gulugulu.moe").unwrap(), + Some(RelayMetadata::Write), + ), + ( + RelayUrl::parse("wss://relay.primal.net/").unwrap(), + Some(RelayMetadata::Write), + ), + ( + RelayUrl::parse("wss://relay.primal.net/").unwrap(), + Some(RelayMetadata::Read), + ), + ( + RelayUrl::parse("wss://nos.lol/").unwrap(), + Some(RelayMetadata::Read), + ), + ] +} + +fn default_messaging_relays() -> Vec { + vec![ + RelayUrl::parse("wss://auth.nostr1.com/").unwrap(), + RelayUrl::parse("wss://nip17.com/").unwrap(), + ] +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum RelayState { #[default] -- 2.49.1 From a9d2a0a24b1462266221daf7107efd91393a81a5 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 9 Feb 2026 15:11:37 +0700 Subject: [PATCH 06/11] improve relay auth --- crates/coop/src/command_bar.rs | 6 +- crates/device/src/lib.rs | 4 +- crates/relay_auth/src/lib.rs | 147 ++++++++++++++++++--------------- crates/settings/src/lib.rs | 85 ++++++++++--------- crates/state/src/constants.rs | 4 +- crates/state/src/lib.rs | 16 ++-- 6 files changed, 142 insertions(+), 120 deletions(-) diff --git a/crates/coop/src/command_bar.rs b/crates/coop/src/command_bar.rs index 6cea72f..96ab1cc 100644 --- a/crates/coop/src/command_bar.rs +++ b/crates/coop/src/command_bar.rs @@ -190,11 +190,7 @@ impl CommandBar { // Block the input until the search completes self.set_finding(true, window, cx); - let find_users = if nostr.read(cx).owned_signer() { - nostr.read(cx).wot_search(&query, cx) - } else { - nostr.read(cx).search(&query, cx) - }; + let find_users = nostr.read(cx).search(&query, cx); // Run task in the main thread self.find_task = Some(cx.spawn_in(window, async move |this, cx| { diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 4a02aeb..3aefe87 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -250,6 +250,8 @@ impl DeviceRegistry { *this = Some(Arc::new(signer)); cx.notify(); }); + + log::info!("Device Signer set"); } /// Set the device state @@ -308,7 +310,7 @@ impl DeviceRegistry { Ok(()) }); - task.detach(); + task.detach_and_log_err(cx); } /// Get device announcement for current user diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index 1f182ea..6314811 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -3,6 +3,7 @@ use std::cell::Cell; use std::collections::HashSet; use std::hash::{Hash, Hasher}; use std::rc::Rc; +use std::sync::Arc; use anyhow::{anyhow, Context as AnyhowContext, Error}; use gpui::{ @@ -28,8 +29,8 @@ pub fn init(window: &mut Window, cx: &mut App) { /// Authentication request #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct AuthRequest { - pub url: RelayUrl, - pub challenge: String, + url: RelayUrl, + challenge: String, } impl Hash for AuthRequest { @@ -45,6 +46,14 @@ impl AuthRequest { url, } } + + pub fn url(&self) -> &RelayUrl { + &self.url + } + + pub fn challenge(&self) -> &str { + &self.challenge + } } struct GlobalRelayAuth(Entity); @@ -55,7 +64,7 @@ impl Global for GlobalRelayAuth {} #[derive(Debug)] pub struct RelayAuth { /// Entity for managing auth requests - requests: HashSet, + requests: HashSet>, /// Event subscriptions _subscriptions: SmallVec<[Subscription; 1]>, @@ -91,14 +100,14 @@ impl RelayAuth { subscriptions.push( // Observe the current state - cx.observe_in(&entity, window, |this, _, window, cx| { + cx.observe_in(&entity, window, |this, _state, window, cx| { let settings = AppSettings::global(cx); let mode = AppSettings::get_auth_mode(cx); - for req in this.requests.clone().into_iter() { - let is_trusted_relay = settings.read(cx).is_trusted_relay(&req.url, cx); + for req in this.requests.iter() { + let trusted_relay = settings.read(cx).trusted_relay(req.url(), cx); - if is_trusted_relay && mode == AuthMode::Auto { + if trusted_relay && mode == AuthMode::Auto { // Automatically authenticate if the relay is authenticated before this.response(req, window, cx); } else { @@ -111,7 +120,9 @@ impl RelayAuth { tasks.push( // Handle nostr notifications - cx.background_spawn(async move { Self::handle_notifications(&client, &tx).await }), + cx.background_spawn(async move { + Self::handle_notifications(&client, &tx).await; + }), ); tasks.push( @@ -136,16 +147,16 @@ impl RelayAuth { // Handle nostr notifications async fn handle_notifications(client: &Client, tx: &flume::Sender) { let mut notifications = client.notifications(); + let mut challenges: HashSet> = HashSet::default(); while let Some(notification) = notifications.next().await { match notification { ClientNotification::Message { relay_url, message } => { match message { RelayMessage::Auth { challenge } => { - let request = AuthRequest::new(challenge, relay_url); - - if let Err(e) = tx.send_async(request).await { - log::error!("Failed to send auth request: {}", e); + if challenges.insert(challenge.clone()) { + let request = AuthRequest::new(challenge, relay_url); + tx.send_async(request).await.ok(); } } RelayMessage::Ok { @@ -174,7 +185,7 @@ impl RelayAuth { /// Add a new authentication request. fn add_request(&mut self, request: AuthRequest, cx: &mut Context) { - self.requests.insert(request); + self.requests.insert(Arc::new(request)); cx.notify(); } @@ -185,35 +196,34 @@ impl RelayAuth { /// Reask for approval for all pending requests. pub fn re_ask(&mut self, window: &mut Window, cx: &mut Context) { - for request in self.requests.clone().into_iter() { + for request in self.requests.iter() { self.ask_for_approval(request, window, cx); } } /// Respond to an authentication request. - fn response(&mut self, req: AuthRequest, window: &mut Window, cx: &mut Context) { + fn response(&self, req: &Arc, window: &Window, cx: &Context) { let settings = AppSettings::global(cx); - let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let challenge = req.challenge.to_owned(); - let url = req.url.to_owned(); - - let challenge_clone = challenge.clone(); - let url_clone = url.clone(); + let req = req.clone(); + let challenge = req.challenge().to_string(); + let async_req = req.clone(); let task: Task> = cx.background_spawn(async move { // Construct event - let builder = EventBuilder::auth(challenge_clone, url_clone.clone()); + let builder = EventBuilder::auth(async_req.challenge(), async_req.url().clone()); let event = client.sign_event_builder(builder).await?; // Get the event ID let id = event.id; // Get the relay - let relay = client.relay(url_clone).await?.context("Relay not found")?; - let relay_url = relay.url(); + let relay = client + .relay(async_req.url()) + .await? + .context("Relay not found")?; // Subscribe to notifications let mut notifications = relay.notifications(); @@ -234,7 +244,7 @@ impl RelayAuth { // Get all pending events that need to be resent let mut tracker = tracker().write().await; - let ids: Vec = tracker.pending_resend(relay_url); + let ids: Vec = tracker.pending_resend(relay.url()); for id in ids.into_iter() { if let Some(event) = client.database().event_by_id(&id).await? { @@ -254,47 +264,56 @@ impl RelayAuth { Err(anyhow!("Authentication failed")) }); - self._tasks.push( - // Handle response in the background - cx.spawn_in(window, async move |this, cx| { - match task.await { + cx.spawn_in(window, async move |this, cx| { + let result = task.await; + let url = req.url(); + + this.update_in(cx, |this, window, cx| { + match result { Ok(_) => { - this.update_in(cx, |this, window, cx| { - // Clear the current notification - window.clear_notification(challenge, cx); + window.clear_notification(challenge, cx); + window.push_notification(format!("{} has been authenticated", url), cx); - // Push a new notification - window.push_notification(format!("{url} has been authenticated"), cx); + // Save the authenticated relay to automatically authenticate future requests + settings.update(cx, |this, cx| { + this.add_trusted_relay(url, cx); + }); - // Save the authenticated relay to automatically authenticate future requests - settings.update(cx, |this, cx| { - this.add_trusted_relay(url, cx); - }); - - // Remove the challenge from the list of pending authentications - this.requests.remove(&req); - cx.notify(); - }) - .expect("Entity has been released"); + // Remove the challenge from the list of pending authentications + this.requests.remove(&req); + cx.notify(); } Err(e) => { - this.update_in(cx, |_, window, cx| { - window.push_notification(Notification::error(e.to_string()), cx); - }) - .expect("Entity has been released"); + window.push_notification(Notification::error(e.to_string()), cx); } - }; - }), - ); + } + }) + .ok(); + }) + .detach(); } /// Push a popup to approve the authentication request. - fn ask_for_approval(&mut self, req: AuthRequest, window: &mut Window, cx: &mut Context) { - let url = SharedString::from(req.url.clone().to_string()); + fn ask_for_approval(&self, req: &Arc, window: &Window, cx: &Context) { + let notification = self.notification(req, cx); + + cx.spawn_in(window, async move |_this, cx| { + cx.update(|window, cx| { + window.push_notification(notification, cx); + }) + .ok(); + }) + .detach(); + } + + /// Build a notification for the authentication request. + fn notification(&self, req: &Arc, cx: &Context) -> Notification { + let req = req.clone(); + let url = SharedString::from(req.url().to_string()); let entity = cx.entity().downgrade(); let loading = Rc::new(Cell::new(false)); - let note = Notification::new() + Notification::new() .custom_id(SharedString::from(&req.challenge)) .autohide(false) .icon(IconName::Info) @@ -317,7 +336,7 @@ impl RelayAuth { .into_any_element() }) .action(move |_window, _cx| { - let entity = entity.clone(); + let view = entity.clone(); let req = req.clone(); Button::new("approve") @@ -328,24 +347,18 @@ impl RelayAuth { .disabled(loading.get()) .on_click({ let loading = Rc::clone(&loading); + move |_ev, window, cx| { // Set loading state to true loading.set(true); // Process to approve the request - entity - .update(cx, |this, cx| { - this.response(req.clone(), window, cx); - }) - .ok(); + view.update(cx, |this, cx| { + this.response(&req, window, cx); + }) + .ok(); } }) - }); - - // Push the notification to the current window - window.push_notification(note, cx); - - // Bring the window to the front - cx.activate(true); + }) } } diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 42645c5..adfdf84 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -47,8 +47,8 @@ setting_accessors! { #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum AuthMode { #[default] - Manual, Auto, + Manual, } /// Signer kind @@ -121,7 +121,7 @@ pub struct AppSettings { _subscriptions: SmallVec<[Subscription; 1]>, /// Background tasks - tasks: SmallVec<[Task<()>; 1]>, + tasks: SmallVec<[Task>; 1]>, } impl AppSettings { @@ -151,13 +151,15 @@ impl AppSettings { tasks.push( // Load the initial settings cx.spawn(async move |this, cx| { - if let Ok(settings) = load_settings.await { - this.update(cx, |this, cx| { - this.values = settings; - cx.notify(); - }) - .ok(); - } + let settings = load_settings.await.unwrap_or(Settings::default()); + log::info!("Settings: {settings:?}"); + + // Update the settings state + this.update(cx, |this, cx| { + this.set_settings(settings, cx); + })?; + + Ok(()) }), ); @@ -168,6 +170,12 @@ impl AppSettings { } } + /// Update settings + fn set_settings(&mut self, settings: Settings, cx: &mut Context) { + self.values = settings; + cx.notify(); + } + /// Get settings from the database fn get_from_database(cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); @@ -189,7 +197,7 @@ impl AppSettings { } if let Some(event) = client.database().query(filter).await?.first_owned() { - Ok(serde_json::from_str(&event.content).unwrap_or(Settings::default())) + Ok(serde_json::from_str(&event.content)?) } else { Err(anyhow!("Not found")) } @@ -203,13 +211,13 @@ impl AppSettings { self.tasks.push( // Run task in the background cx.spawn(async move |this, cx| { - if let Ok(settings) = task.await { - this.update(cx, |this, cx| { - this.values = settings; - cx.notify(); - }) - .ok(); - } + let settings = task.await?; + // Update settings + this.update(cx, |this, cx| { + this.set_settings(settings, cx); + })?; + + Ok(()) }), ); } @@ -218,36 +226,37 @@ impl AppSettings { pub fn save(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let settings = self.values.clone(); - if let Ok(content) = serde_json::to_string(&self.values) { - let task: Task> = cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; + self.tasks.push(cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; + let content = serde_json::to_string(&settings)?; - let event = EventBuilder::new(Kind::ApplicationSpecificData, content) - .tag(Tag::identifier(SETTINGS_IDENTIFIER)) - .build(public_key) - .sign(&Keys::generate()) - .await?; + let event = EventBuilder::new(Kind::ApplicationSpecificData, content) + .tag(Tag::identifier(SETTINGS_IDENTIFIER)) + .build(public_key) + .sign(&Keys::generate()) + .await?; - // Save event to the local database without sending to relays - client.database().save_event(&event).await?; + // Save event to the local database only + client.database().save_event(&event).await?; + log::info!("Settings saved successfully"); - Ok(()) - }); - - task.detach(); - } + Ok(()) + })); } - /// Check if the given relay is trusted - pub fn is_trusted_relay(&self, url: &RelayUrl, _cx: &App) -> bool { - self.values.trusted_relays.contains(url) + /// Check if the given relay is already authenticated + pub fn trusted_relay(&self, url: &RelayUrl, _cx: &App) -> bool { + self.values.trusted_relays.iter().any(|relay| { + relay.as_str_without_trailing_slash() == url.as_str_without_trailing_slash() + }) } /// Add a relay to the trusted list - pub fn add_trusted_relay(&mut self, url: RelayUrl, cx: &mut Context) { - self.values.trusted_relays.insert(url); + pub fn add_trusted_relay(&mut self, url: &RelayUrl, cx: &mut Context) { + self.values.trusted_relays.insert(url.clone()); cx.notify(); } diff --git a/crates/state/src/constants.rs b/crates/state/src/constants.rs index 5e54cfb..90a1993 100644 --- a/crates/state/src/constants.rs +++ b/crates/state/src/constants.rs @@ -10,7 +10,7 @@ pub const COOP_PUBKEY: &str = "npub126kl5fruqan90py77gf6pvfvygefl2mu2ukew6xdx5pc pub const APP_ID: &str = "su.reya.coop"; /// Keyring name -pub const KEYRING: &str = "Coop Secret Storage"; +pub const KEYRING: &str = "Coop Safe Storage"; /// Default timeout for subscription pub const TIMEOUT: u64 = 3; @@ -37,7 +37,7 @@ pub const USER_GIFTWRAP: &str = "user-gift-wraps"; pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"]; /// Default search relays -pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"]; +pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://relay.noswhere.com"]; /// Default bootstrap relays pub const BOOTSTRAP_RELAYS: [&str; 4] = [ diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 66615f4..06e1147 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -164,11 +164,6 @@ impl NostrRegistry { client.add_relay(url).await?; } - // Add wot relay to the relay pool - for url in WOT_RELAYS.into_iter() { - client.add_relay(url).await?; - } - // Connect to all added relays client.connect().await; @@ -261,14 +256,21 @@ impl NostrRegistry { .author(public_key) .limit(1); - client + let relays: Vec = client .database() .query(filter) .await .ok() .and_then(|events| events.first_owned()) .map(|event| nip17::extract_owned_relay_list(event).collect()) - .unwrap_or_default() + .unwrap_or_default(); + + for relay in relays.iter() { + client.add_relay(relay).await.ok(); + client.connect_relay(relay).await.ok(); + } + + relays }) } -- 2.49.1 From 9bee5f2a7796126a940dfa78f1f05d46e23c9bb0 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 11 Feb 2026 08:55:42 +0700 Subject: [PATCH 07/11] redesign the sidebar --- Cargo.lock | 325 ++++++-- crates/coop/src/main.rs | 1 - crates/coop/src/panels/greeter.rs | 14 +- crates/coop/src/{ => sidebar}/command_bar.rs | 4 +- crates/coop/src/sidebar/mod.rs | 658 +++++++++++++--- crates/coop/src/workspace.rs | 52 +- crates/dock/src/stack_panel.rs | 30 +- crates/settings/src/lib.rs | 1 - crates/state/src/lib.rs | 8 +- crates/ui/src/button.rs | 239 +++--- crates/ui/src/divider.rs | 4 +- crates/ui/src/menu/popup_menu.rs | 3 + crates/ui/src/popup_menu.rs | 776 ------------------- crates/ui/src/styled.rs | 14 +- 14 files changed, 962 insertions(+), 1167 deletions(-) rename crates/coop/src/{ => sidebar}/command_bar.rs (99%) delete mode 100644 crates/ui/src/popup_menu.rs diff --git a/Cargo.lock b/Cargo.lock index 6830357..d14b4c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,9 +291,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.37" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40" +checksum = "68650b7df54f0293fd061972a0fb05aaf4fc0879d3b3d21a638a182c5c543b9f" dependencies = [ "compression-codecs", "compression-core", @@ -501,10 +501,8 @@ dependencies = [ [[package]] name = "async-wsocket" version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7d8c7d34a225ba919dd9ba44d4b9106d20142da545e086be8ae21d1897e043" +source = "git+https://github.com/shadowylab/async-wsocket?rev=0fed6c9c6aec7393ee0e9cf3933d76914ab427d3#0fed6c9c6aec7393ee0e9cf3933d76914ab427d3" dependencies = [ - "async-utility", "futures", "futures-util", "js-sys", @@ -514,6 +512,7 @@ dependencies = [ "tokio-tungstenite", "url", "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", ] @@ -1264,7 +1263,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1709,7 +1708,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "proc-macro2", "quote", @@ -1914,7 +1913,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.11+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", "vswhom", "winreg", ] @@ -2008,7 +2007,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]] @@ -2535,6 +2534,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "gif" version = "0.14.1" @@ -2627,7 +2639,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2729,7 +2741,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2740,7 +2752,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "anyhow", "gpui", @@ -2975,7 +2987,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "anyhow", "async-compression", @@ -3000,7 +3012,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3197,6 +3209,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -3497,6 +3515,12 @@ dependencies = [ "leak", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lebe" version = "0.5.3" @@ -3505,15 +3529,15 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libfuzzer-sys" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" dependencies = [ "arbitrary", "cc", @@ -3756,7 +3780,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "anyhow", "bindgen", @@ -3770,9 +3794,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" @@ -3996,7 +4020,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" +source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" dependencies = [ "aes", "base64", @@ -4021,7 +4045,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" +source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" dependencies = [ "async-utility", "futures-core", @@ -4034,7 +4058,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" +source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" dependencies = [ "btreecap", "flatbuffers", @@ -4046,7 +4070,7 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" +source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" dependencies = [ "nostr", ] @@ -4054,7 +4078,7 @@ dependencies = [ [[package]] name = "nostr-gossip-memory" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" +source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" dependencies = [ "indexmap", "lru", @@ -4066,7 +4090,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" +source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" dependencies = [ "async-utility", "flume", @@ -4080,7 +4104,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" +source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" dependencies = [ "async-utility", "async-wsocket", @@ -4098,9 +4122,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ "winapi", ] @@ -4111,7 +4135,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]] @@ -4614,7 +4638,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "collections", "serde", @@ -4910,9 +4934,9 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa96cb91275ed31d6da3e983447320c4eb219ac180fa1679a0889ff32861e2d" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" dependencies = [ "ar_archive_writer", "cc", @@ -5021,7 +5045,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5274,7 +5298,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "derive_refineable", ] @@ -5373,7 +5397,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "anyhow", "bytes", @@ -5428,7 +5452,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "arrayvec", "log", @@ -5541,7 +5565,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5656,9 +5680,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa20" @@ -5690,7 +5714,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "async-task", "backtrace", @@ -6176,9 +6200,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stacker" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" dependencies = [ "cc", "cfg-if", @@ -6305,7 +6329,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "arrayvec", "log", @@ -6316,15 +6340,15 @@ dependencies = [ [[package]] name = "sval" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502b8906c4736190684646827fbab1e954357dfe541013bbd7994d033d53a1ca" +checksum = "c1aaf178a50bbdd86043fce9bf0a5867007d9b382db89d1c96ccae4601ff1ff9" [[package]] name = "sval_buffer" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4b854348b15b6c441bdd27ce9053569b016a0723eab2d015b1fd8e6abe4f708" +checksum = "f89273e48f03807ebf51c4d81c52f28d35ffa18a593edf97e041b52de143df89" dependencies = [ "sval", "sval_ref", @@ -6332,18 +6356,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bd9e8b74410ddad37c6962587c5f9801a2caadba9e11f3f916ee3f31ae4a1f" +checksum = "0430f4e18e7eba21a49d10d25a8dec3ce0e044af40b162347e99a8e3c3ced864" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe17b8deb33a9441280b4266c2d257e166bafbaea6e66b4b34ca139c91766d9" +checksum = "835f51b9d7331b9d7fc48fc716c02306fa88c4a076b1573531910c91a525882d" dependencies = [ "itoa", "ryu", @@ -6352,9 +6376,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854addb048a5bafb1f496c98e0ab5b9b581c3843f03ca07c034ae110d3b7c623" +checksum = "13cbfe3ef406ee2366e7e8ab3678426362085fa9eaedf28cb878a967159dced3" dependencies = [ "itoa", "ryu", @@ -6363,9 +6387,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf068f482108ff44ae8013477cb047a1665d5f1a635ad7cf79582c1845dce9" +checksum = "8b20358af4af787c34321a86618c3cae12eabdd0e9df22cd9dd2c6834214c518" dependencies = [ "sval", "sval_buffer", @@ -6374,18 +6398,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed02126365ffe5ab8faa0abd9be54fbe68d03d607cd623725b0a71541f8aaa6f" +checksum = "fb5e500f8eb2efa84f75e7090f7fc43f621b9f8b6cde571c635b3855f97b332a" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a263383c6aa2076c4ef6011d3bae1b356edf6ea2613e3d8e8ebaa7b57dd707d5" +checksum = "ca2032ae39b11dcc6c18d5fbc50a661ea191cac96484c59ccf49b002261ca2c1" dependencies = [ "serde_core", "sval", @@ -6557,15 +6581,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand 2.3.0", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix 1.1.3", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6812,9 +6836,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", @@ -6853,9 +6877,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", "serde_core", @@ -6912,9 +6936,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1" dependencies = [ "winnow", ] @@ -7051,9 +7075,9 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", @@ -7140,9 +7164,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-linebreak" @@ -7189,6 +7213,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.5.1" @@ -7266,7 +7296,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "anyhow", "async-fs", @@ -7304,7 +7334,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "perf", "quote", @@ -7449,6 +7479,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasite" version = "0.1.0" @@ -7514,6 +7553,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -7527,6 +7588,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "wayland-backend" version = "0.3.12" @@ -7647,9 +7720,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" +checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" dependencies = [ "core-foundation 0.10.0", "jni", @@ -7748,7 +7821,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8349,6 +8422,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.114", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -8780,7 +8935,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "anyhow", "chrono", @@ -8790,14 +8945,14 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" dependencies = [ "tracing", "tracing-subscriber", @@ -8808,7 +8963,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7cb4d75139b39efb0d1d1e90faf7480d0f1dc734" +source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" [[package]] name = "zune-core" diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index 060910c..02df297 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -12,7 +12,6 @@ use ui::Root; use crate::actions::Quit; mod actions; -mod command_bar; mod dialogs; mod panels; mod sidebar; diff --git a/crates/coop/src/panels/greeter.rs b/crates/coop/src/panels/greeter.rs index 9b58edd..7b0dce1 100644 --- a/crates/coop/src/panels/greeter.rs +++ b/crates/coop/src/panels/greeter.rs @@ -154,7 +154,7 @@ impl Render for GreeterPanel { .label("Set up relay list") .ghost() .small() - .no_center() + .justify_start() .on_click(move |_ev, window, cx| { Workspace::add_panel( relay_list::init(window, cx), @@ -172,7 +172,7 @@ impl Render for GreeterPanel { .label("Set up messaging relays") .ghost() .small() - .no_center() + .justify_start() .on_click(move |_ev, window, cx| { Workspace::add_panel( messaging_relays::init(window, cx), @@ -211,7 +211,7 @@ impl Render for GreeterPanel { .label("Connect account via Nostr Connect") .ghost() .small() - .no_center() + .justify_start() .on_click(move |_ev, window, cx| { Workspace::add_panel( connect::init(window, cx), @@ -227,7 +227,7 @@ impl Render for GreeterPanel { .label("Import a secret key or bunker") .ghost() .small() - .no_center() + .justify_start() .on_click(move |_ev, window, cx| { Workspace::add_panel( import::init(window, cx), @@ -264,7 +264,7 @@ impl Render for GreeterPanel { .label("Backup account") .ghost() .small() - .no_center(), + .justify_start(), ) .child( Button::new("profile") @@ -272,7 +272,7 @@ impl Render for GreeterPanel { .label("Update profile") .ghost() .small() - .no_center() + .justify_start() .on_click(cx.listener(move |this, _ev, window, cx| { this.add_profile_panel(window, cx) })), @@ -283,7 +283,7 @@ impl Render for GreeterPanel { .label("Invite friends") .ghost() .small() - .no_center(), + .justify_start(), ), ), ), diff --git a/crates/coop/src/command_bar.rs b/crates/coop/src/sidebar/command_bar.rs similarity index 99% rename from crates/coop/src/command_bar.rs rename to crates/coop/src/sidebar/command_bar.rs index 96ab1cc..09962d2 100644 --- a/crates/coop/src/command_bar.rs +++ b/crates/coop/src/sidebar/command_bar.rs @@ -438,9 +438,9 @@ impl Render for CommandBar { .w_full() .child( TextInput::new(&self.find_input) - .appearance(true) + .appearance(false) .bordered(false) - .xsmall() + .small() .text_xs() .when(!self.find_input.read(cx).loading, |this| { this.suffix( diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index 072b0b6..041d181 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -1,23 +1,38 @@ +use std::collections::HashSet; use std::ops::Range; +use std::time::Duration; -use chat::{ChatEvent, ChatRegistry, RoomKind}; -use common::RenderedTimestamp; +use anyhow::Error; +use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; +use common::{DebouncedDelay, RenderedTimestamp}; use dock::panel::{Panel, PanelEvent}; use gpui::prelude::FluentBuilder; use gpui::{ - deferred, div, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, - Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled, - Subscription, Window, + div, rems, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, + Focusable, InteractiveElement, IntoElement, ParentElement, Render, RetainAllImageCache, + SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, }; use list_item::RoomListItem; +use nostr_sdk::prelude::*; +use person::PersonRegistry; +use settings::AppSettings; use smallvec::{smallvec, SmallVec}; +use state::{NostrRegistry, FIND_DELAY}; use theme::{ActiveTheme, TABBAR_HEIGHT}; +use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; +use ui::divider::Divider; use ui::indicator::Indicator; -use ui::{h_flex, v_flex, IconName, Selectable, Sizable, StyledExt}; +use ui::input::{InputEvent, InputState, TextInput}; +use ui::notification::Notification; +use ui::{ + h_flex, v_flex, Disableable, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, +}; mod list_item; +const INPUT_PLACEHOLDER: &str = "Find or start a conversation"; + pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Sidebar::new(window, cx)) } @@ -30,12 +45,42 @@ pub struct Sidebar { /// Image cache image_cache: Entity, + /// Find input state + find_input: Entity, + + /// Debounced delay for find input + find_debouncer: DebouncedDelay, + + /// Whether a search is in progress + finding: bool, + + /// Whether the find input is focused + find_focused: bool, + + /// Find results + find_results: Entity>>, + + /// Async find operation + find_task: Option>>, + + /// Whether there are search results + has_search: bool, + /// Whether there are new chat requests new_requests: bool, + /// Selected public keys + selected_pkeys: Entity>, + /// Chatroom filter filter: Entity, + /// User's contacts + contact_list: Entity>>, + + /// Async tasks + tasks: SmallVec<[Task<()>; 1]>, + /// Event subscriptions _subscriptions: SmallVec<[Subscription; 1]>, } @@ -44,9 +89,49 @@ impl Sidebar { fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); let filter = cx.new(|_| RoomKind::Ongoing); + let contact_list = cx.new(|_| None); + let selected_pkeys = cx.new(|_| HashSet::new()); + let find_results = cx.new(|_| None); + let find_input = cx.new(|cx| { + InputState::new(window, cx) + .placeholder(INPUT_PLACEHOLDER) + .clean_on_escape() + }); let mut subscriptions = smallvec![]; + subscriptions.push( + // Subscribe to find input events + cx.subscribe_in(&find_input, window, |this, state, event, window, cx| { + let delay = Duration::from_millis(FIND_DELAY); + + match event { + InputEvent::PressEnter { .. } => { + this.search(window, cx); + } + InputEvent::Change => { + if state.read(cx).value().is_empty() { + // Clear results when input is empty + this.reset(window, cx); + } else { + // Run debounced search + this.find_debouncer + .fire_new(delay, window, cx, |this, window, cx| { + this.debounced_search(window, cx) + }); + } + } + InputEvent::Focus => { + this.set_input_focus(cx); + this.get_contact_list(window, cx); + } + InputEvent::Blur => { + this.set_input_focus(cx); + } + }; + }), + ); + subscriptions.push( // Subscribe for registry new events cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| { @@ -61,12 +146,193 @@ impl Sidebar { name: "Sidebar".into(), focus_handle: cx.focus_handle(), image_cache: RetainAllImageCache::new(cx), + find_input, + find_debouncer: DebouncedDelay::new(), + find_results, + find_task: None, + find_focused: false, + finding: false, + has_search: false, new_requests: false, + contact_list, + selected_pkeys, filter, + tasks: smallvec![], _subscriptions: subscriptions, } } + /// Get the contact list. + fn get_contact_list(&mut self, window: &mut Window, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let task = nostr.read(cx).get_contact_list(cx); + + self.tasks.push(cx.spawn_in(window, async move |this, cx| { + match task.await { + Ok(contacts) => { + this.update(cx, |this, cx| { + this.set_contact_list(contacts, cx); + }) + .ok(); + } + Err(e) => { + cx.update(|window, cx| { + window.push_notification(Notification::error(e.to_string()), cx); + }) + .ok(); + } + }; + })); + } + + /// Set the contact list with new contacts. + fn set_contact_list(&mut self, contacts: I, cx: &mut Context) + where + I: IntoIterator, + { + self.contact_list.update(cx, |this, cx| { + *this = Some(contacts.into_iter().collect()); + cx.notify(); + }); + } + + /// Trigger the debounced search + fn debounced_search(&self, window: &mut Window, cx: &mut Context) -> Task<()> { + cx.spawn_in(window, async move |this, cx| { + this.update_in(cx, |this, window, cx| { + this.search(window, cx); + }) + .ok(); + }) + } + + /// Search + fn search(&mut self, window: &mut Window, cx: &mut Context) { + // Return if a search is already in progress + if self.finding { + if self.find_task.is_none() { + window.push_notification("There is another search in progress", cx); + return; + } else { + // Cancel the ongoing search request + self.find_task = None; + } + } + + // Get query + let query = self.find_input.read(cx).value(); + + // Return if the query is empty + if query.is_empty() { + return; + } + + // Block the input until the search completes + self.set_finding(true, window, cx); + + let nostr = NostrRegistry::global(cx); + let find_users = nostr.read(cx).search(&query, cx); + + // Run task in the main thread + self.find_task = Some(cx.spawn_in(window, async move |this, cx| { + let rooms = find_users.await?; + // Update the UI with the search results + this.update_in(cx, |this, window, cx| { + this.set_results(rooms, cx); + this.set_finding(false, window, cx); + })?; + + Ok(()) + })); + } + + fn set_results(&mut self, results: Vec, cx: &mut Context) { + self.find_results.update(cx, |this, cx| { + *this = Some(results); + cx.notify(); + }); + } + + fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context) { + // Disable the input to prevent duplicate requests + self.find_input.update(cx, |this, cx| { + this.set_disabled(status, cx); + this.set_loading(status, cx); + }); + // Set the search status + self.finding = status; + cx.notify(); + } + + fn set_input_focus(&mut self, cx: &mut Context) { + self.find_focused = !self.find_focused; + cx.notify(); + } + + fn reset(&mut self, window: &mut Window, cx: &mut Context) { + // Clear all search results + self.find_results.update(cx, |this, cx| { + *this = None; + cx.notify(); + }); + + // Reset the search status + self.set_finding(false, window, cx); + + // Cancel the current search task + self.find_task = None; + cx.notify(); + } + + /// Select a public key in the sidebar. + fn select(&mut self, pkey: PublicKey, cx: &mut Context) { + self.selected_pkeys.update(cx, |this, cx| { + if this.contains(&pkey) { + this.remove(&pkey); + } else { + this.insert(pkey); + } + cx.notify(); + }); + } + + /// Check if a public key is selected in the sidebar. + fn is_selected(&self, pkey: PublicKey, cx: &App) -> bool { + self.selected_pkeys.read(cx).contains(&pkey) + } + + /// Get all selected public keys in the sidebar. + fn selected(&self, cx: &Context) -> HashSet { + self.selected_pkeys.read(cx).clone() + } + + /// Create a new room + fn create_room(&mut self, window: &mut Window, cx: &mut Context) { + let chat = ChatRegistry::global(cx); + let async_chat = chat.downgrade(); + + let nostr = NostrRegistry::global(cx); + let signer_pkey = nostr.read(cx).signer_pkey(cx); + + // Get all selected public keys + let receivers = self.selected(cx); + + let task: Task> = cx.spawn_in(window, async move |_this, cx| { + let public_key = signer_pkey.await?; + + async_chat.update_in(cx, |this, window, cx| { + let room = cx.new(|_| Room::new(public_key, receivers)); + this.emit_room(room.downgrade(), cx); + + window.close_modal(cx); + })?; + + Ok(()) + }); + + task.detach(); + } + /// Get the active filter. fn current_filter(&self, kind: &RoomKind, cx: &Context) -> bool { self.filter.read(cx) == kind @@ -111,6 +377,67 @@ impl Sidebar { }) .collect() } + + fn render_contacts(&self, range: Range, cx: &Context) -> Vec { + let persons = PersonRegistry::global(cx); + let hide_avatar = AppSettings::get_hide_avatar(cx); + + let Some(contacts) = self.contact_list.read(cx) else { + return vec![]; + }; + + contacts + .get(range.clone()) + .into_iter() + .flatten() + .enumerate() + .map(|(ix, item)| { + let profile = persons.read(cx).get(item, cx); + let pkey = item.to_owned(); + let id = range.start + ix; + + h_flex() + .id(id) + .h_8() + .w_full() + .px_1() + .gap_2() + .rounded(cx.theme().radius) + .when(!hide_avatar, |this| { + this.child( + div() + .flex_shrink_0() + .size_6() + .rounded_full() + .overflow_hidden() + .child(Avatar::new(profile.avatar()).size(rems(1.5))), + ) + }) + .child( + h_flex() + .flex_1() + .justify_between() + .line_clamp(1) + .text_ellipsis() + .truncate() + .text_sm() + .child(profile.name()) + .when(self.is_selected(pkey, cx), |this| { + this.child( + Icon::new(IconName::CheckCircle) + .small() + .text_color(cx.theme().icon_accent), + ) + }), + ) + .hover(|this| this.bg(cx.theme().elevated_surface_background)) + .on_click(cx.listener(move |this, _ev, _window, cx| { + this.select(pkey, cx); + })) + .into_any_element() + }) + .collect() + } } impl Panel for Sidebar { @@ -133,89 +460,124 @@ impl Render for Sidebar { let loading = chat.read(cx).loading(); let total_rooms = chat.read(cx).count(self.filter.read(cx), cx); + // Whether the find panel should be shown + let show_find_panel = self.has_search || self.find_focused; + + // Set button label based on total selected users + let button_label = if self.selected_pkeys.read(cx).len() > 1 { + "Create Group DM" + } else { + "Create DM" + }; + v_flex() .image_cache(self.image_cache.clone()) .size_full() .relative() - .gap_2() .child( h_flex() .h(TABBAR_HEIGHT) - .w_full() .border_b_1() .border_color(cx.theme().border) .child( - h_flex() - .flex_1() - .h_full() - .gap_2() - .p_2() - .justify_center() - .child( - Button::new("all") - .map(|this| { - if self.current_filter(&RoomKind::Ongoing, cx) { - this.icon(IconName::InboxFill) - } else { - this.icon(IconName::Inbox) - } - }) - .label("Inbox") - .tooltip("All ongoing conversations") - .xsmall() - .bold() - .ghost() - .flex_1() - .rounded_none() - .selected(self.current_filter(&RoomKind::Ongoing, cx)) - .on_click(cx.listener(|this, _, _, cx| { - this.set_filter(RoomKind::Ongoing, cx); - })), - ) - .child( - Button::new("requests") - .map(|this| { - if self.current_filter(&RoomKind::Request, cx) { - this.icon(IconName::FistbumpFill) - } else { - this.icon(IconName::Fistbump) - } - }) - .label("Requests") - .tooltip("Incoming new conversations") - .xsmall() - .bold() - .ghost() - .flex_1() - .rounded_none() - .selected(!self.current_filter(&RoomKind::Ongoing, cx)) - .when(self.new_requests, |this| { - this.child( - div().size_1().rounded_full().bg(cx.theme().cursor), - ) - }) - .on_click(cx.listener(|this, _, _, cx| { - this.set_filter(RoomKind::default(), cx); - })), - ), - ) - .child( - h_flex() - .h_full() - .px_2() - .border_l_1() - .border_color(cx.theme().border) - .child( - Button::new("option") - .icon(IconName::Ellipsis) - .small() - .ghost(), - ), + TextInput::new(&self.find_input) + .appearance(false) + .bordered(false) + .small() + .text_xs() + .when(!self.find_input.read(cx).loading, |this| { + this.suffix( + Button::new("find-icon") + .icon(IconName::Search) + .tooltip("Press Enter to search") + .transparent() + .small(), + ) + }), ), ) - .when(!loading && total_rooms == 0, |this| { + .child( + h_flex() + .h(TABBAR_HEIGHT) + .justify_center() + .border_b_1() + .border_color(cx.theme().border) + .when(show_find_panel, |this| { + this.child( + Button::new("search-results") + .icon(IconName::Search) + .label("Search") + .tooltip("All search results") + .small() + .underline() + .ghost() + .font_semibold() + .rounded_none() + .h_full() + .flex_1() + .selected(true), + ) + }) + .child( + Button::new("all") + .map(|this| { + if self.current_filter(&RoomKind::Ongoing, cx) { + this.icon(IconName::InboxFill) + } else { + this.icon(IconName::Inbox) + } + }) + .when(!show_find_panel, |this| this.label("Inbox")) + .tooltip("All ongoing conversations") + .small() + .underline() + .ghost() + .font_semibold() + .rounded_none() + .h_full() + .flex_1() + .disabled(show_find_panel) + .selected( + !show_find_panel && self.current_filter(&RoomKind::Ongoing, cx), + ) + .on_click(cx.listener(|this, _ev, _window, cx| { + this.set_filter(RoomKind::Ongoing, cx); + })), + ) + .child(Divider::vertical()) + .child( + Button::new("requests") + .map(|this| { + if self.current_filter(&RoomKind::Request, cx) { + this.icon(IconName::FistbumpFill) + } else { + this.icon(IconName::Fistbump) + } + }) + .when(!show_find_panel, |this| this.label("Requests")) + .tooltip("Incoming new conversations") + .small() + .ghost() + .underline() + .font_semibold() + .rounded_none() + .h_full() + .flex_1() + .disabled(show_find_panel) + .selected( + !show_find_panel && !self.current_filter(&RoomKind::Ongoing, cx), + ) + .when(self.new_requests, |this| { + this.child(div().size_1().rounded_full().bg(cx.theme().cursor)) + }) + .on_click(cx.listener(|this, _ev, _window, cx| { + this.set_filter(RoomKind::default(), cx); + })), + ), + ) + .when(!show_find_panel && !loading && total_rooms == 0, |this| { this.child( - div().px_2p5().child(deferred( + div().mt_2().px_2().child( v_flex() .p_3() .h_24() @@ -238,47 +600,131 @@ impl Render for Sidebar { "Start a conversation with someone to get started.", ), )), - )), + ), ) }) .child( v_flex() + .h_full() .px_1p5() - .w_full() + .mt_2() .flex_1() .gap_1() .overflow_y_hidden() - .child( - uniform_list( - "rooms", - total_rooms, - cx.processor(|this, range, _window, cx| { - this.render_list_items(range, cx) - }), - ) - .h_full(), - ) - .when(loading, |this| { + .when(show_find_panel, |this| { + this.gap_2() + .when_some(self.find_results.read(cx).as_ref(), |this, results| { + this.child( + v_flex() + .gap_2() + .flex_1() + .child( + div() + .text_xs() + .font_semibold() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Results")), + ) + .child( + uniform_list( + "rooms", + results.len(), + cx.processor(|this, range, _window, cx| { + this.render_list_items(range, cx) + }), + ) + .flex_1() + .h_full(), + ), + ) + }) + .when_some(self.contact_list.read(cx).as_ref(), |this, contacts| { + this.child( + v_flex() + .gap_2() + .flex_1() + .child( + div() + .text_xs() + .font_semibold() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Suggestions")), + ) + .child( + uniform_list( + "contacts", + contacts.len(), + cx.processor(move |this, range, _window, cx| { + this.render_contacts(range, cx) + }), + ) + .flex_1() + .h_full(), + ), + ) + }) + }) + .when(!show_find_panel, |this| { this.child( - div().absolute().top_2().left_0().w_full().px_8().child( - h_flex() - .gap_2() - .w_full() - .h_9() - .justify_center() - .bg(cx.theme().background.opacity(0.85)) - .border_color(cx.theme().border_disabled) - .border_1() - .when(cx.theme().shadow, |this| this.shadow_sm()) - .rounded_full() - .text_xs() - .font_semibold() - .text_color(cx.theme().text_muted) - .child(Indicator::new().small().color(cx.theme().icon_accent)) - .child(SharedString::from("Getting messages...")), - ), + uniform_list( + "rooms", + total_rooms, + cx.processor(|this, range, _window, cx| { + this.render_list_items(range, cx) + }), + ) + .flex_1() + .h_full(), ) }), ) + .when(!self.selected_pkeys.read(cx).is_empty(), |this| { + this.child( + div() + .absolute() + .bottom_0() + .left_0() + .h_9() + .w_full() + .px_2() + .child( + Button::new("create") + .label(button_label) + .primary() + .small() + .on_click(cx.listener(move |this, _ev, window, cx| { + this.create_room(window, cx); + })), + ), + ) + }) + .when(loading, |this| { + this.child( + div() + .absolute() + .bottom_2() + .left_0() + .h_9() + .w_full() + .px_8() + .child( + h_flex() + .gap_2() + .w_full() + .h_9() + .justify_center() + .bg(cx.theme().background.opacity(0.85)) + .border_color(cx.theme().border_disabled) + .border_1() + .when(cx.theme().shadow, |this| this.shadow_sm()) + .rounded_full() + .text_xs() + .font_semibold() + .text_color(cx.theme().text_muted) + .child(Indicator::new().small().color(cx.theme().icon_accent)) + .child(SharedString::from("Getting messages...")), + ), + ) + }) } } diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index 4d32e2b..4d82554 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -13,12 +13,13 @@ use nostr_sdk::prelude::*; use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; use state::NostrRegistry; -use theme::{ActiveTheme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT}; +use theme::{SIDEBAR_WIDTH, TITLEBAR_HEIGHT}; use titlebar::TitleBar; use ui::avatar::Avatar; -use ui::{h_flex, v_flex, Icon, IconName, Root, Sizable, WindowExtension}; +use ui::button::{Button, ButtonVariants}; +use ui::popup_menu::PopupMenuExt; +use ui::{h_flex, v_flex, Root, Sizable, WindowExtension}; -use crate::command_bar::CommandBar; use crate::panels::greeter; use crate::sidebar; @@ -33,9 +34,6 @@ pub struct Workspace { /// App's Dock Area dock: Entity, - /// App's Command Bar - command_bar: Entity, - /// Current User current_user: Entity>, @@ -45,22 +43,16 @@ pub struct Workspace { impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { + let titlebar = cx.new(|_| TitleBar::new()); + let dock = + cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar)); + let chat = ChatRegistry::global(cx); let current_user = cx.new(|_| None); let nostr = NostrRegistry::global(cx); let nip65_state = nostr.read(cx).nip65_state(); - // Titlebar - let titlebar = cx.new(|_| TitleBar::new()); - - // Command bar - let command_bar = cx.new(|cx| CommandBar::new(window, cx)); - - // Dock - let dock = - cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar)); - let mut subscriptions = smallvec![]; subscriptions.push( @@ -124,7 +116,6 @@ impl Workspace { Self { titlebar, dock, - command_bar, current_user, _subscriptions: subscriptions, } @@ -213,7 +204,8 @@ impl Workspace { fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { h_flex() .h(TITLEBAR_HEIGHT) - .flex_1() + .w(SIDEBAR_WIDTH) + .flex_shrink_0() .justify_between() .gap_2() .when_some(self.current_user.read(cx).as_ref(), |this, public_key| { @@ -221,24 +213,30 @@ impl Workspace { let profile = persons.read(cx).get(public_key, cx); this.child( - h_flex() - .gap_0p5() + Button::new("current-user") .child(Avatar::new(profile.avatar()).size(rems(1.25))) - .child( - Icon::new(IconName::ChevronDown) - .small() - .text_color(cx.theme().text_muted), - ), + .small() + .caret() + .compact() + .transparent() + .popup_menu(move |this, _window, _cx| { + this.label(profile.name()) + .separator() + .menu("Profile", Box::new(ClosePanel)) + .menu("Backup", Box::new(ClosePanel)) + .menu("Themes", Box::new(ClosePanel)) + .menu("Settings", Box::new(ClosePanel)) + }), ) }) } fn titlebar_center(&mut self, _window: &mut Window, _cx: &Context) -> impl IntoElement { - h_flex().flex_1().w_full().child(self.command_bar.clone()) + h_flex().h(TITLEBAR_HEIGHT).flex_1() } fn titlebar_right(&mut self, _window: &mut Window, _cx: &Context) -> impl IntoElement { - h_flex().flex_1() + h_flex().h(TITLEBAR_HEIGHT).w(SIDEBAR_WIDTH).flex_shrink_0() } } diff --git a/crates/dock/src/stack_panel.rs b/crates/dock/src/stack_panel.rs index 2a2dd19..4a684bd 100644 --- a/crates/dock/src/stack_panel.rs +++ b/crates/dock/src/stack_panel.rs @@ -65,7 +65,7 @@ impl StackPanel { } /// Return true if self or parent only have last panel. - pub(super) fn is_last_panel(&self, cx: &App) -> bool { + pub fn is_last_panel(&self, cx: &App) -> bool { if self.panels.len() > 1 { return false; } @@ -79,12 +79,12 @@ impl StackPanel { true } - pub(super) fn panels_len(&self) -> usize { + pub fn panels_len(&self) -> usize { self.panels.len() } /// Return the index of the panel. - pub(crate) fn index_of_panel(&self, panel: Arc) -> Option { + pub fn index_of_panel(&self, panel: Arc) -> Option { self.panels.iter().position(|p| p == &panel) } @@ -253,11 +253,12 @@ impl StackPanel { }); cx.emit(PanelEvent::LayoutChanged); + self.remove_self_if_empty(window, cx); } /// Replace the old panel with the new panel at same index. - pub(super) fn replace_panel( + pub fn replace_panel( &mut self, old_panel: Arc, new_panel: Entity, @@ -266,16 +267,15 @@ impl StackPanel { ) { if let Some(ix) = self.index_of_panel(old_panel.clone()) { self.panels[ix] = Arc::new(new_panel.clone()); - let panel_state = ResizablePanelState::default(); self.state.update(cx, |state, cx| { - state.replace_panel(ix, panel_state, cx); + state.replace_panel(ix, ResizablePanelState::default(), cx); }); cx.emit(PanelEvent::LayoutChanged); } } /// If children is empty, remove self from parent view. - pub(crate) fn remove_self_if_empty(&mut self, window: &mut Window, cx: &mut Context) { + pub fn remove_self_if_empty(&mut self, window: &mut Window, cx: &mut Context) { if self.is_root() { return; } @@ -296,11 +296,7 @@ impl StackPanel { } /// Find the first top left in the stack. - pub(super) fn left_top_tab_panel( - &self, - check_parent: bool, - cx: &App, - ) -> Option> { + pub fn left_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option> { if check_parent { if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) { if let Some(panel) = parent.read(cx).left_top_tab_panel(true, cx) { @@ -324,11 +320,7 @@ impl StackPanel { } /// Find the first top right in the stack. - pub(super) fn right_top_tab_panel( - &self, - check_parent: bool, - cx: &App, - ) -> Option> { + pub fn right_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option> { if check_parent { if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) { if let Some(panel) = parent.read(cx).right_top_tab_panel(true, cx) { @@ -357,7 +349,7 @@ impl StackPanel { } /// Remove all panels from the stack. - pub(super) fn remove_all_panels(&mut self, _: &mut Window, cx: &mut Context) { + pub fn remove_all_panels(&mut self, _: &mut Window, cx: &mut Context) { self.panels.clear(); self.state.update(cx, |state, cx| { state.clear(); @@ -366,7 +358,7 @@ impl StackPanel { } /// Change the axis of the stack panel. - pub(super) fn set_axis(&mut self, axis: Axis, _: &mut Window, cx: &mut Context) { + pub fn set_axis(&mut self, axis: Axis, _: &mut Window, cx: &mut Context) { self.axis = axis; cx.notify(); } diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index adfdf84..0aa9bcf 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -241,7 +241,6 @@ impl AppSettings { // Save event to the local database only client.database().save_event(&event).await?; - log::info!("Settings saved successfully"); Ok(()) })); diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 06e1147..cf3b9e9 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::os::unix::fs::PermissionsExt; use std::sync::Arc; use std::time::Duration; @@ -576,17 +576,15 @@ impl NostrRegistry { } /// Get contact list for the current user - pub fn get_contact_list(&self, cx: &App) -> Task, Error>> { + pub fn get_contact_list(&self, cx: &App) -> Task, Error>> { let client = self.client(); cx.background_spawn(async move { let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; - let contacts = client.database().contacts_public_keys(public_key).await?; - let results = contacts.into_iter().collect(); - Ok(results) + Ok(contacts) }) } diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index 0c151fe..e15b2ea 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -10,7 +10,7 @@ use theme::ActiveTheme; use crate::indicator::Indicator; use crate::tooltip::Tooltip; -use crate::{h_flex, Disableable, Icon, Selectable, Sizable, Size, StyledExt}; +use crate::{h_flex, Disableable, Icon, IconName, Selectable, Sizable, Size, StyledExt}; #[derive(Clone, Copy, PartialEq, Eq)] pub struct ButtonCustomVariant { @@ -20,50 +20,6 @@ pub struct ButtonCustomVariant { active: Hsla, } -pub trait ButtonVariants: Sized { - fn with_variant(self, variant: ButtonVariant) -> Self; - - /// With the primary style for the Button. - fn primary(self) -> Self { - self.with_variant(ButtonVariant::Primary) - } - - /// With the secondary style for the Button. - fn secondary(self) -> Self { - self.with_variant(ButtonVariant::Secondary) - } - - /// With the danger style for the Button. - fn danger(self) -> Self { - self.with_variant(ButtonVariant::Danger) - } - - /// With the warning style for the Button. - fn warning(self) -> Self { - self.with_variant(ButtonVariant::Warning) - } - - /// With the ghost style for the Button. - fn ghost(self) -> Self { - self.with_variant(ButtonVariant::Ghost { alt: false }) - } - - /// With the ghost style for the Button. - fn ghost_alt(self) -> Self { - self.with_variant(ButtonVariant::Ghost { alt: true }) - } - - /// With the transparent style for the Button. - fn transparent(self) -> Self { - self.with_variant(ButtonVariant::Transparent) - } - - /// With the custom style for the Button. - fn custom(self, style: ButtonCustomVariant) -> Self { - self.with_variant(ButtonVariant::Custom(style)) - } -} - impl ButtonCustomVariant { pub fn new(_window: &Window, cx: &App) -> Self { Self { @@ -110,6 +66,50 @@ pub enum ButtonVariant { Custom(ButtonCustomVariant), } +pub trait ButtonVariants: Sized { + fn with_variant(self, variant: ButtonVariant) -> Self; + + /// With the primary style for the Button. + fn primary(self) -> Self { + self.with_variant(ButtonVariant::Primary) + } + + /// With the secondary style for the Button. + fn secondary(self) -> Self { + self.with_variant(ButtonVariant::Secondary) + } + + /// With the danger style for the Button. + fn danger(self) -> Self { + self.with_variant(ButtonVariant::Danger) + } + + /// With the warning style for the Button. + fn warning(self) -> Self { + self.with_variant(ButtonVariant::Warning) + } + + /// With the ghost style for the Button. + fn ghost(self) -> Self { + self.with_variant(ButtonVariant::Ghost { alt: false }) + } + + /// With the ghost style for the Button. + fn ghost_alt(self) -> Self { + self.with_variant(ButtonVariant::Ghost { alt: true }) + } + + /// With the transparent style for the Button. + fn transparent(self) -> Self { + self.with_variant(ButtonVariant::Transparent) + } + + /// With the custom style for the Button. + fn custom(self, style: ButtonCustomVariant) -> Self { + self.with_variant(ButtonVariant::Custom(style)) + } +} + /// A Button element. #[derive(IntoElement)] #[allow(clippy::type_complexity)] @@ -124,17 +124,15 @@ pub struct Button { children: Vec, variant: ButtonVariant, - center: bool, - rounded: bool, size: Size, disabled: bool, - reverse: bool, - bold: bool, - cta: bool, - loading: bool, - loading_icon: Option, + + rounded: bool, + compact: bool, + underline: bool, + caret: bool, on_click: Option>, on_hover: Option>, @@ -161,21 +159,19 @@ impl Button { style: StyleRefinement::default(), icon: None, label: None, + variant: ButtonVariant::default(), disabled: false, selected: false, - variant: ButtonVariant::default(), + underline: false, + compact: false, + caret: false, rounded: false, size: Size::Medium, tooltip: None, on_click: None, on_hover: None, loading: false, - reverse: false, - center: true, - bold: false, - cta: false, children: Vec::new(), - loading_icon: None, tab_index: 0, tab_stop: true, } @@ -211,33 +207,21 @@ impl Button { self } - /// Set reverse the position between icon and label. - pub fn reverse(mut self) -> Self { - self.reverse = true; + /// Set true to make the button compact (no padding). + pub fn compact(mut self) -> Self { + self.compact = true; self } - /// Set bold the button (label will be use the semi-bold font). - pub fn bold(mut self) -> Self { - self.bold = true; + /// Set true to show the caret indicator. + pub fn caret(mut self) -> Self { + self.caret = true; self } - /// Disable centering the button's content. - pub fn no_center(mut self) -> Self { - self.center = false; - self - } - - /// Set the cta style of the button. - pub fn cta(mut self) -> Self { - self.cta = true; - self - } - - /// Set the loading icon of the button. - pub fn loading_icon(mut self, icon: impl Into) -> Self { - self.loading_icon = Some(icon.into()); + /// Set true to show the underline indicator. + pub fn underline(mut self) -> Self { + self.underline = true; self } @@ -346,7 +330,7 @@ impl RenderOnce for Button { }; let focus_handle = window - .use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle()) + .use_keyed_state(self.id.clone(), cx, |_window, cx| cx.focus_handle()) .read(cx) .clone(); @@ -358,10 +342,11 @@ impl RenderOnce for Button { .tab_stop(self.tab_stop), ) }) + .relative() .flex_shrink_0() .flex() .items_center() - .when(self.center, |this| this.justify_center()) + .justify_center() .cursor_default() .overflow_hidden() .refine_style(&self.style) @@ -369,39 +354,15 @@ impl RenderOnce for Button { false => this.rounded(cx.theme().radius), true => this.rounded_full(), }) - .map(|this| { + .when(!self.compact, |this| { if self.label.is_none() && self.children.is_empty() { // Icon Button match self.size { Size::Size(px) => this.size(px), - Size::XSmall => { - if self.cta { - this.w_10().h_5() - } else { - this.size_5() - } - } - Size::Small => { - if self.cta { - this.w_12().h_6() - } else { - this.size_6() - } - } - Size::Medium => { - if self.cta { - this.w_12().h_7() - } else { - this.size_7() - } - } - _ => { - if self.cta { - this.w_16().h_9() - } else { - this.size_9() - } - } + Size::XSmall => this.size_5(), + Size::Small => this.size_6(), + Size::Medium => this.size_7(), + _ => this.size_9(), } } else { // Normal Button @@ -410,8 +371,6 @@ impl RenderOnce for Button { Size::XSmall => { if self.icon.is_some() { this.h_6().pl_2().pr_2p5() - } else if self.cta { - this.h_6().px_4() } else { this.h_6().px_2() } @@ -419,8 +378,6 @@ impl RenderOnce for Button { Size::Small => { if self.icon.is_some() { this.h_7().pl_2().pr_2p5() - } else if self.cta { - this.h_7().px_4() } else { this.h_7().px_2() } @@ -442,13 +399,27 @@ impl RenderOnce for Button { } } }) - .on_mouse_down(gpui::MouseButton::Left, |_, window, _| { + .refine_style(&self.style) + .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| { + // Stop handle any click event when disabled. + // To avoid handle dropdown menu open when button is disabled. + if self.disabled { + cx.stop_propagation(); + return; + } // Avoid focus on mouse down. window.prevent_default(); }) - .when_some(self.on_click.filter(|_| clickable), |this, on_click| { + .when_some(self.on_click, |this, on_click| { this.on_click(move |event, window, cx| { - (on_click)(event, window, cx); + // Stop handle any click event when disabled. + // To avoid handle dropdown menu open when button is disabled. + if !clickable { + cx.stop_propagation(); + return; + } + + on_click(event, window, cx); }) }) .when_some(self.on_hover.filter(|_| hoverable), |this, on_hover| { @@ -459,7 +430,6 @@ impl RenderOnce for Button { .child({ h_flex() .id("label") - .when(self.reverse, |this| this.flex_row_reverse()) .justify_center() .map(|this| match self.size { Size::XSmall => this.text_xs().gap_1(), @@ -471,22 +441,18 @@ impl RenderOnce for Button { this.child(icon.with_size(icon_size)) }) }) - .when(self.loading, |this| { - this.child( - Indicator::new() - .when_some(self.loading_icon, |this, icon| this.icon(icon)), - ) - }) + .when(self.loading, |this| this.child(Indicator::new())) .when_some(self.label, |this, label| { - this.child( - div() - .flex_none() - .line_height(relative(1.)) - .child(label) - .when(self.bold, |this| this.font_semibold()), - ) + this.child(div().flex_none().line_height(relative(1.)).child(label)) }) .children(self.children) + .when(self.caret, |this| { + this.justify_between().gap_0p5().child( + Icon::new(IconName::ChevronDown) + .small() + .text_color(cx.theme().text_muted), + ) + }) }) .text_color(normal_style.fg) .when(!self.disabled && !self.selected, |this| { @@ -504,6 +470,17 @@ impl RenderOnce for Button { let selected_style = style.selected(cx); this.bg(selected_style.bg).text_color(selected_style.fg) }) + .when(self.selected && self.underline, |this| { + this.child( + div() + .absolute() + .bottom_0() + .left_0() + .h_px() + .w_full() + .bg(cx.theme().element_background), + ) + }) .when(self.disabled, |this| { let disabled_style = style.disabled(cx); this.cursor_not_allowed() diff --git a/crates/ui/src/divider.rs b/crates/ui/src/divider.rs index 4086dce..45aa100 100644 --- a/crates/ui/src/divider.rs +++ b/crates/ui/src/divider.rs @@ -61,8 +61,8 @@ impl RenderOnce for Divider { .absolute() .rounded_full() .map(|this| match self.axis { - Axis::Vertical => this.w(px(2.)).h_full(), - Axis::Horizontal => this.h(px(2.)).w_full(), + Axis::Vertical => this.w(px(1.)).h_full(), + Axis::Horizontal => this.h(px(1.)).w_full(), }) .bg(self.color.unwrap_or(cx.theme().border_variant)), ) diff --git a/crates/ui/src/menu/popup_menu.rs b/crates/ui/src/menu/popup_menu.rs index cf3105e..a9c8d75 100644 --- a/crates/ui/src/menu/popup_menu.rs +++ b/crates/ui/src/menu/popup_menu.rs @@ -58,6 +58,7 @@ pub trait PopupMenuExt: Styled + Selectable + InteractiveElement + IntoElement + }) } } + impl PopupMenuExt for Button {} #[allow(clippy::type_complexity)] @@ -1074,7 +1075,9 @@ impl PopupMenu { } impl FluentBuilder for PopupMenu {} + impl EventEmitter for PopupMenu {} + impl Focusable for PopupMenu { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() diff --git a/crates/ui/src/popup_menu.rs b/crates/ui/src/popup_menu.rs deleted file mode 100644 index a12a30c..0000000 --- a/crates/ui/src/popup_menu.rs +++ /dev/null @@ -1,776 +0,0 @@ -use std::ops::Deref; -use std::rc::Rc; - -use gpui::prelude::FluentBuilder; -use gpui::{ - actions, anchored, canvas, div, px, rems, Action, AnyElement, App, AppContext, AsKeystroke, - Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, - InteractiveElement, IntoElement, KeyBinding, Keystroke, ParentElement, Pixels, Render, - ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Subscription, WeakEntity, - Window, -}; -use theme::ActiveTheme; - -use crate::button::Button; -use crate::list::ListItem; -use crate::popover::Popover; -use crate::scroll::{Scrollbar, ScrollbarState}; -use crate::{h_flex, v_flex, Icon, IconName, Selectable, Sizable as _, StyledExt}; - -actions!( - menu, - [ - /// Trigger confirm action when user presses enter button - Confirm, - /// Trigger dismiss action when user presses escape button - Dismiss, - /// Select the next item when user presses up button - SelectNext, - /// Select the previous item when user preses down button - SelectPrev - ] -); - -const ITEM_HEIGHT: Pixels = px(26.); - -pub fn init(cx: &mut App) { - let context = Some("PopupMenu"); - - cx.bind_keys([ - KeyBinding::new("enter", Confirm, context), - KeyBinding::new("escape", Dismiss, context), - KeyBinding::new("up", SelectPrev, context), - KeyBinding::new("down", SelectNext, context), - ]); -} - -pub trait PopupMenuExt: Styled + Selectable + InteractiveElement + IntoElement + 'static { - /// Create a popup menu with the given items, anchored to the TopLeft corner - fn popup_menu( - self, - f: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static, - ) -> Popover { - self.popup_menu_with_anchor(Corner::TopLeft, f) - } - - /// Create a popup menu with the given items, anchored to the given corner - fn popup_menu_with_anchor( - mut self, - anchor: impl Into, - f: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static, - ) -> Popover { - let style = self.style().clone(); - let id = self.interactivity().element_id.clone(); - - Popover::new(SharedString::from(format!("popup-menu:{id:?}"))) - .no_style() - .trigger(self) - .trigger_style(style) - .anchor(anchor.into()) - .content(move |window, cx| { - PopupMenu::build(window, cx, |menu, window, cx| f(menu, window, cx)) - }) - } -} - -impl PopupMenuExt for Button {} - -enum PopupMenuItem { - Title(SharedString), - Separator, - Item { - icon: Option, - label: SharedString, - action: Option>, - #[allow(clippy::type_complexity)] - handler: Rc, - }, - ElementItem { - #[allow(clippy::type_complexity)] - render: Box AnyElement + 'static>, - #[allow(clippy::type_complexity)] - handler: Rc, - }, - Submenu { - icon: Option, - label: SharedString, - menu: Entity, - }, -} - -impl PopupMenuItem { - fn is_clickable(&self) -> bool { - !matches!(self, PopupMenuItem::Separator) - } - - fn is_separator(&self) -> bool { - matches!(self, PopupMenuItem::Separator) - } - - fn has_icon(&self) -> bool { - matches!(self, PopupMenuItem::Item { icon: Some(_), .. }) - } -} - -pub struct PopupMenu { - /// The parent menu of this menu, if this is a submenu - parent_menu: Option>, - focus_handle: FocusHandle, - menu_items: Vec, - has_icon: bool, - selected_index: Option, - min_width: Pixels, - max_width: Pixels, - hovered_menu_ix: Option, - bounds: Bounds, - - scrollable: bool, - scroll_handle: ScrollHandle, - scroll_state: ScrollbarState, - - action_focus_handle: Option, - #[allow(dead_code)] - subscriptions: Vec, -} - -impl PopupMenu { - pub fn build( - window: &mut Window, - cx: &mut App, - f: impl FnOnce(Self, &mut Window, &mut Context) -> Self, - ) -> Entity { - cx.new(|cx| { - let focus_handle = cx.focus_handle(); - let subscriptions = - vec![ - cx.on_blur(&focus_handle, window, |this: &mut PopupMenu, window, cx| { - this.dismiss(&Dismiss, window, cx) - }), - ]; - let menu = Self { - focus_handle, - action_focus_handle: None, - parent_menu: None, - menu_items: Vec::new(), - selected_index: None, - min_width: px(120.), - max_width: px(500.), - has_icon: false, - hovered_menu_ix: None, - bounds: Bounds::default(), - scrollable: false, - scroll_handle: ScrollHandle::default(), - scroll_state: ScrollbarState::default(), - subscriptions, - }; - - f(menu, window, cx) - }) - } - - /// Bind the focus handle of the menu, when clicked, it will focus back to this handle and then dispatch the action - pub fn track_focus(mut self, focus_handle: &FocusHandle) -> Self { - self.action_focus_handle = Some(focus_handle.clone()); - self - } - - /// Set min width of the popup menu, default is 120px - pub fn min_w(mut self, width: impl Into) -> Self { - self.min_width = width.into(); - self - } - - /// Set max width of the popup menu, default is 500px - pub fn max_w(mut self, width: impl Into) -> Self { - self.max_width = width.into(); - self - } - - /// Set the menu to be scrollable to show vertical scrollbar. - /// - /// NOTE: If this is true, the sub-menus will cannot be support. - pub fn scrollable(mut self) -> Self { - self.scrollable = true; - self - } - - /// Add Menu Item - pub fn menu(mut self, label: impl Into, action: Box) -> Self { - self.add_menu_item(label, None, action); - self - } - - /// Add Menu to open link - pub fn link(mut self, label: impl Into, href: impl Into) -> Self { - let href = href.into(); - self.menu_items.push(PopupMenuItem::Item { - icon: None, - label: label.into(), - action: None, - handler: Rc::new(move |_window, cx| cx.open_url(&href)), - }); - self - } - - /// Add Menu to open link - pub fn link_with_icon( - mut self, - label: impl Into, - icon: impl Into, - href: impl Into, - ) -> Self { - let href = href.into(); - self.menu_items.push(PopupMenuItem::Item { - icon: Some(icon.into()), - label: label.into(), - action: None, - handler: Rc::new(move |_window, cx| cx.open_url(&href)), - }); - self - } - - /// Add Menu Item with Icon - pub fn menu_with_icon( - mut self, - label: impl Into, - icon: impl Into, - action: Box, - ) -> Self { - self.add_menu_item(label, Some(icon.into()), action); - self - } - - /// Add Menu Item with check icon - pub fn menu_with_check( - mut self, - label: impl Into, - checked: bool, - action: Box, - ) -> Self { - if checked { - self.add_menu_item(label, Some(IconName::Check.into()), action); - } else { - self.add_menu_item(label, None, action); - } - - self - } - - /// Add Menu Item with custom element render. - pub fn menu_with_element(mut self, builder: F, action: Box) -> Self - where - F: Fn(&mut Window, &mut App) -> E + 'static, - E: IntoElement, - { - self.menu_items.push(PopupMenuItem::ElementItem { - render: Box::new(move |window, cx| builder(window, cx).into_any_element()), - handler: self.wrap_handler(action), - }); - self - } - - #[allow(clippy::type_complexity)] - fn wrap_handler(&self, action: Box) -> Rc { - let action_focus_handle = self.action_focus_handle.clone(); - - Rc::new(move |window, cx| { - window.activate_window(); - - // Focus back to the user expected focus handle - // Then the actions listened on that focus handle can be received - // - // For example: - // - // TabPanel - // |- PopupMenu - // |- PanelContent (actions are listened here) - // - // The `PopupMenu` and `PanelContent` are at the same level in the TabPanel - // If the actions are listened on the `PanelContent`, - // it can't receive the actions from the `PopupMenu`, unless we focus on `PanelContent`. - if let Some(handle) = action_focus_handle.as_ref() { - window.focus(handle); - } - - window.dispatch_action(action.boxed_clone(), cx); - }) - } - - fn add_menu_item( - &mut self, - label: impl Into, - icon: Option, - action: Box, - ) -> &mut Self { - if icon.is_some() { - self.has_icon = true; - } - - self.menu_items.push(PopupMenuItem::Item { - icon, - label: label.into(), - action: Some(action.boxed_clone()), - handler: self.wrap_handler(action), - }); - self - } - - /// Add a title menu item - pub fn title(mut self, label: impl Into) -> Self { - if self.menu_items.is_empty() { - return self; - } - - if let Some(PopupMenuItem::Title(_)) = self.menu_items.last() { - return self; - } - - self.menu_items.push(PopupMenuItem::Title(label.into())); - self - } - - /// Add a separator Menu Item - pub fn separator(mut self) -> Self { - if self.menu_items.is_empty() { - return self; - } - - if let Some(PopupMenuItem::Separator) = self.menu_items.last() { - return self; - } - - self.menu_items.push(PopupMenuItem::Separator); - self - } - - pub fn submenu( - self, - label: impl Into, - window: &mut Window, - cx: &mut Context, - f: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static, - ) -> Self { - self.submenu_with_icon(None, label, window, cx, f) - } - - /// Add a Submenu item with icon - pub fn submenu_with_icon( - mut self, - icon: Option, - label: impl Into, - window: &mut Window, - cx: &mut Context, - f: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static, - ) -> Self { - let submenu = PopupMenu::build(window, cx, f); - let parent_menu = cx.entity().downgrade(); - - submenu.update(cx, |view, _| { - view.parent_menu = Some(parent_menu); - }); - - self.menu_items.push(PopupMenuItem::Submenu { - icon, - label: label.into(), - menu: submenu, - }); - self - } - - pub(crate) fn active_submenu(&self) -> Option> { - if let Some(ix) = self.hovered_menu_ix { - if let Some(item) = self.menu_items.get(ix) { - return match item { - PopupMenuItem::Submenu { menu, .. } => Some(menu.clone()), - _ => None, - }; - } - } - - None - } - - pub fn is_empty(&self) -> bool { - self.menu_items.is_empty() - } - - fn clickable_menu_items(&self) -> impl Iterator { - self.menu_items - .iter() - .enumerate() - .filter(|(_, item)| item.is_clickable()) - } - - fn on_click(&mut self, ix: usize, window: &mut Window, cx: &mut Context) { - cx.stop_propagation(); - window.prevent_default(); - self.selected_index = Some(ix); - self.confirm(&Confirm, window, cx); - } - - fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { - if let Some(index) = self.selected_index { - let item = self.menu_items.get(index); - match item { - Some(PopupMenuItem::Item { handler, .. }) => { - handler(window, cx); - self.dismiss(&Dismiss, window, cx) - } - Some(PopupMenuItem::ElementItem { handler, .. }) => { - handler(window, cx); - self.dismiss(&Dismiss, window, cx) - } - _ => {} - } - } - } - - fn select_next(&mut self, _: &SelectNext, _window: &mut Window, cx: &mut Context) { - let count = self.clickable_menu_items().count(); - if count > 0 { - let last_ix = count.saturating_sub(1); - let ix = self - .selected_index - .map(|index| if index == last_ix { 0 } else { index + 1 }) - .unwrap_or(0); - - self.selected_index = Some(ix); - cx.notify(); - } - } - - fn select_prev(&mut self, _: &SelectPrev, _window: &mut Window, cx: &mut Context) { - let count = self.clickable_menu_items().count(); - if count > 0 { - let last_ix = count.saturating_sub(1); - - let ix = self - .selected_index - .map(|index| { - if index == last_ix { - 0 - } else { - index.saturating_sub(1) - } - }) - .unwrap_or(last_ix); - self.selected_index = Some(ix); - cx.notify(); - } - } - - // TODO: fix this - #[allow(clippy::only_used_in_recursion)] - fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context) { - if self.active_submenu().is_some() { - return; - } - - cx.emit(DismissEvent); - - // Dismiss parent menu, when this menu is dismissed - if let Some(parent_menu) = self.parent_menu.clone().and_then(|menu| menu.upgrade()) { - parent_menu.update(cx, |view, cx| { - view.hovered_menu_ix = None; - view.dismiss(&Dismiss, window, cx); - }) - } - } - - fn render_keybinding( - action: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Option { - if let Some(action) = action { - if let Some(keybinding) = window.bindings_for_action(action.deref()).first() { - let el = div().text_color(cx.theme().text_muted).children( - keybinding - .keystrokes() - .iter() - .map(|key| key_shortcut(key.as_keystroke().clone())), - ); - - return Some(el); - } - } - - None - } - - fn render_icon( - has_icon: bool, - icon: Option, - _window: &Window, - _cx: &Context, - ) -> Option { - let icon_placeholder = if has_icon { Some(Icon::empty()) } else { None }; - - if !has_icon { - return None; - } - - let icon = h_flex() - .w_3p5() - .h_3p5() - .items_center() - .justify_center() - .text_sm() - .map(|this| { - if let Some(icon) = icon { - this.child(icon.clone().small()) - } else { - this.children(icon_placeholder.clone()) - } - }); - - Some(icon) - } -} - -impl FluentBuilder for PopupMenu {} - -impl EventEmitter for PopupMenu {} - -impl Focusable for PopupMenu { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for PopupMenu { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let view = cx.entity().clone(); - let has_icon = self.menu_items.iter().any(|item| item.has_icon()); - let items_count = self.menu_items.len(); - let max_width = self.max_width; - let bounds = self.bounds; - - let window_haft_height = window.window_bounds().get_bounds().size.height * 0.5; - let max_height = window_haft_height.min(px(450.)); - - v_flex() - .id("popup-menu") - .key_context("PopupMenu") - .track_focus(&self.focus_handle) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_prev)) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::dismiss)) - .on_mouse_down_out(cx.listener(|this, _, window, cx| this.dismiss(&Dismiss, window, cx))) - .popover_style(cx) - .relative() - .p_1() - .child( - div() - .id("popup-menu-items") - .when(self.scrollable, |this| { - this.max_h(max_height) - .overflow_y_scroll() - .track_scroll(&self.scroll_handle) - }) - .child( - v_flex() - .gap_y_0p5() - .min_w(self.min_width) - .max_w(self.max_width) - .min_w(rems(8.)) - .child({ - canvas( - move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds), - |_, _, _, _| {}, - ) - .absolute() - .size_full() - }) - .children( - self.menu_items - .iter_mut() - .enumerate() - // Skip last separator - .filter(|(ix, item)| !(*ix == items_count - 1 && item.is_separator())) - .map(|(ix, item)| { - let this = ListItem::new(("menu-item", ix)) - .relative() - .items_center() - .py_0() - .px_2() - .rounded_md() - .text_sm() - .on_mouse_enter(cx.listener(move |this, _, _window, cx| { - this.hovered_menu_ix = Some(ix); - cx.notify(); - })); - - match item { - PopupMenuItem::Title(label) => { - this.child( - div() - .text_xs() - .font_semibold() - .text_color(cx.theme().text_muted) - .child(label.clone()) - ) - }, - PopupMenuItem::Separator => this.h_auto().p_0().disabled(true).child( - div() - .rounded_none() - .h(px(1.)) - .mx_neg_1() - .my_0p5() - .bg(cx.theme().border_disabled), - ), - PopupMenuItem::ElementItem { render, .. } => this - .on_click( - cx.listener(move |this, _, window, cx| { - this.on_click(ix, window, cx) - }), - ) - .child( - h_flex() - .min_h(ITEM_HEIGHT) - .items_center() - .gap_x_1() - .children(Self::render_icon(has_icon, None, window, cx)) - .child((render)(window, cx)), - ), - PopupMenuItem::Item { - icon, label, action, .. - } => { - let action = action.as_ref().map(|action| action.boxed_clone()); - let key = Self::render_keybinding(action, window, cx); - - this.on_click( - cx.listener(move |this, _, window, cx| { - this.on_click(ix, window, cx) - }), - ) - .child( - h_flex() - .h(ITEM_HEIGHT) - .items_center() - .gap_x_1p5() - .children(Self::render_icon(has_icon, icon.clone(), window, cx)) - .child( - h_flex() - .flex_1() - .gap_2() - .items_center() - .justify_between() - .child(label.clone()) - .children(key), - ), - ) - } - PopupMenuItem::Submenu { icon, label, menu } => this - .when(self.hovered_menu_ix == Some(ix), |this| this.selected(true)) - .child( - h_flex() - .items_start() - .child( - h_flex() - .size_full() - .items_center() - .gap_x_1p5() - .children(Self::render_icon( - has_icon, - icon.clone(), - window, - cx, - )) - .child( - h_flex() - .flex_1() - .gap_2() - .items_center() - .justify_between() - .child(label.clone()) - .child(IconName::CaretRight), - ), - ) - .when_some(self.hovered_menu_ix, |this, hovered_ix| { - let (anchor, left) = if window.bounds().size.width - - bounds.origin.x - < max_width - { - (Corner::TopRight, -px(15.)) - } else { - (Corner::TopLeft, bounds.size.width - px(10.)) - }; - - let top = if bounds.origin.y + bounds.size.height - > window.bounds().size.height - { - px(32.) - } else { - -px(10.) - }; - - if hovered_ix == ix { - this.child( - anchored() - .anchor(anchor) - .child( - div() - .occlude() - .top(top) - .left(left) - .child(menu.clone()), - ) - .snap_to_window_with_margin(Edges::all(px(8.))), - ) - } else { - this - } - }), - ), - } - }), - ), - ), - ) - .when(self.scrollable, |this| { - // TODO: When the menu is limited by `overflow_y_scroll`, the sub-menu will cannot be displayed. - this.child( - div() - .absolute() - .top_0() - .left_0() - .right_0p5() - .bottom_0() - .child(Scrollbar::vertical(&self.scroll_state, &self.scroll_handle)), - ) - }) - } -} - -/// Return the Platform specific keybinding string by KeyStroke -pub fn key_shortcut(key: Keystroke) -> String { - if cfg!(target_os = "macos") { - return format!("{key}"); - } - - let mut parts = vec![]; - if key.modifiers.control { - parts.push("Ctrl"); - } - if key.modifiers.alt { - parts.push("Alt"); - } - if key.modifiers.platform { - parts.push("Win"); - } - if key.modifiers.shift { - parts.push("Shift"); - } - - // Capitalize the first letter - let key = if let Some(first_c) = key.key.chars().next() { - format!("{}{}", first_c.to_uppercase(), &key.key[1..]) - } else { - key.key.to_string() - }; - - parts.push(&key); - parts.join("+") -} diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index 6cc9480..febd958 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -183,39 +183,43 @@ impl StyleSized for T { fn input_pl(self, size: Size) -> Self { match size { - Size::Large => self.pl_5(), + Size::XSmall => self.pl_1(), Size::Medium => self.pl_3(), + Size::Large => self.pl_5(), _ => self.pl_2(), } } fn input_pr(self, size: Size) -> Self { match size { - Size::Large => self.pr_5(), + Size::XSmall => self.pr_1(), Size::Medium => self.pr_3(), + Size::Large => self.pr_5(), _ => self.pr_2(), } } fn input_px(self, size: Size) -> Self { match size { - Size::Large => self.px_5(), + Size::XSmall => self.px_1(), Size::Medium => self.px_3(), + Size::Large => self.px_5(), _ => self.px_2(), } } fn input_py(self, size: Size) -> Self { match size { - Size::Large => self.py_5(), + Size::XSmall => self.py_0p5(), Size::Medium => self.py_2(), + Size::Large => self.py_5(), _ => self.py_1(), } } fn input_h(self, size: Size) -> Self { match size { - Size::XSmall => self.h_7(), + Size::XSmall => self.h_6(), Size::Small => self.h_8(), Size::Medium => self.h_9(), Size::Large => self.h_12(), -- 2.49.1 From 0581cd2969d94187582c76c85f9303335176d9d0 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 11 Feb 2026 15:24:04 +0700 Subject: [PATCH 08/11] . --- crates/chat/src/room.rs | 2 + crates/coop/src/actions.rs | 19 - crates/coop/src/main.rs | 7 +- crates/coop/src/sidebar/command_bar.rs | 586 ------------------ .../src/sidebar/{list_item.rs => entry.rs} | 50 +- crates/coop/src/sidebar/mod.rs | 150 +++-- 6 files changed, 126 insertions(+), 688 deletions(-) delete mode 100644 crates/coop/src/actions.rs delete mode 100644 crates/coop/src/sidebar/command_bar.rs rename crates/coop/src/sidebar/{list_item.rs => entry.rs} (76%) diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index 516e785..f83cced 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -177,7 +177,9 @@ impl Room { where T: IntoIterator, { + // Map receiver public keys to tags let tags = Tags::from_list(receivers.into_iter().map(Tag::public_key).collect()); + // Construct an unsigned event for a direct message // // WARNING: never sign this event diff --git a/crates/coop/src/actions.rs b/crates/coop/src/actions.rs deleted file mode 100644 index 37c2cb9..0000000 --- a/crates/coop/src/actions.rs +++ /dev/null @@ -1,19 +0,0 @@ -use gpui::actions; - -// Sidebar actions -actions!(sidebar, [Reload, RelayStatus]); - -// User actions -actions!( - coop, - [ - KeyringPopup, - DarkMode, - ViewProfile, - ViewRelays, - Themes, - Settings, - Logout, - Quit - ] -); diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index 02df297..0336fbd 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -2,21 +2,20 @@ use std::sync::{Arc, Mutex}; use assets::Assets; use gpui::{ - point, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, + actions, point, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString, Size, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions, }; use state::{APP_ID, CLIENT_NAME}; use ui::Root; -use crate::actions::Quit; - -mod actions; mod dialogs; mod panels; mod sidebar; mod workspace; +actions!(coop, [Quit]); + fn main() { // Initialize logging tracing_subscriber::fmt::init(); diff --git a/crates/coop/src/sidebar/command_bar.rs b/crates/coop/src/sidebar/command_bar.rs deleted file mode 100644 index 09962d2..0000000 --- a/crates/coop/src/sidebar/command_bar.rs +++ /dev/null @@ -1,586 +0,0 @@ -use std::collections::HashSet; -use std::ops::Range; -use std::time::Duration; - -use anyhow::Error; -use chat::{ChatRegistry, Room}; -use common::DebouncedDelay; -use gpui::prelude::FluentBuilder; -use gpui::{ - anchored, deferred, div, point, px, rems, uniform_list, App, AppContext, Bounds, Context, - Entity, Focusable, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, Point, - Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription, - Task, Window, -}; -use nostr_sdk::prelude::*; -use person::PersonRegistry; -use settings::AppSettings; -use smallvec::{smallvec, SmallVec}; -use state::{NostrRegistry, FIND_DELAY}; -use theme::{ActiveTheme, TITLEBAR_HEIGHT}; -use ui::avatar::Avatar; -use ui::button::{Button, ButtonVariants}; -use ui::input::{InputEvent, InputState, TextInput}; -use ui::notification::Notification; -use ui::{h_flex, v_flex, window_paddings, Icon, IconName, Sizable, WindowExtension}; - -const WIDTH: Pixels = px(425.); - -/// Command bar for searching conversations. -pub struct CommandBar { - /// Selected public keys - selected_pkeys: Entity>, - - /// User's contacts - contact_list: Entity>, - - /// Whether to show the contact list - show_contact_list: bool, - - /// Find input state - find_input: Entity, - - /// Debounced delay for find input - find_debouncer: DebouncedDelay, - - /// Whether a search is in progress - finding: bool, - - /// Find results - find_results: Entity>>, - - /// Async find operation - find_task: Option>>, - - /// Image cache for avatars - image_cache: Entity, - - /// Async tasks - tasks: SmallVec<[Task<()>; 1]>, - - /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 1]>, -} - -impl CommandBar { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let selected_pkeys = cx.new(|_| HashSet::new()); - let contact_list = cx.new(|_| vec![]); - let find_results = cx.new(|_| None); - let find_input = cx.new(|cx| { - InputState::new(window, cx) - .placeholder("Find or start a conversation") - .clean_on_escape() - }); - - let mut subscriptions = smallvec![]; - - subscriptions.push( - // Subscribe to find input events - cx.subscribe_in(&find_input, window, |this, state, event, window, cx| { - let delay = Duration::from_millis(FIND_DELAY); - - match event { - InputEvent::PressEnter { .. } => { - this.search(window, cx); - } - InputEvent::Change => { - if state.read(cx).value().is_empty() { - // Clear results when input is empty - this.reset(window, cx); - } else { - // Run debounced search - this.find_debouncer - .fire_new(delay, window, cx, |this, window, cx| { - this.debounced_search(window, cx) - }); - } - } - InputEvent::Focus => { - this.get_contact_list(window, cx); - } - _ => {} - }; - }), - ); - - Self { - selected_pkeys, - contact_list, - show_contact_list: false, - find_debouncer: DebouncedDelay::new(), - finding: false, - find_input, - find_results, - find_task: None, - image_cache: RetainAllImageCache::new(cx), - tasks: smallvec![], - _subscriptions: subscriptions, - } - } - - fn get_contact_list(&mut self, window: &mut Window, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let task = nostr.read(cx).get_contact_list(cx); - - self.tasks.push(cx.spawn_in(window, async move |this, cx| { - match task.await { - Ok(contacts) => { - this.update(cx, |this, cx| { - this.extend_contacts(contacts, cx); - }) - .ok(); - } - Err(e) => { - cx.update(|window, cx| { - window.push_notification(Notification::error(e.to_string()), cx); - }) - .ok(); - } - }; - })); - } - - /// Extend the contact list with new contacts. - fn extend_contacts(&mut self, contacts: I, cx: &mut Context) - where - I: IntoIterator, - { - self.contact_list.update(cx, |this, cx| { - this.extend(contacts); - cx.notify(); - }); - } - - /// Toggle the visibility of the contact list. - fn toggle_contact_list(&mut self, cx: &mut Context) { - self.show_contact_list = !self.show_contact_list; - cx.notify(); - } - - fn debounced_search(&self, window: &mut Window, cx: &mut Context) -> Task<()> { - cx.spawn_in(window, async move |this, cx| { - this.update_in(cx, |this, window, cx| { - this.search(window, cx); - }) - .ok(); - }) - } - - fn search(&mut self, window: &mut Window, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let query = self.find_input.read(cx).value(); - - // Return if the query is empty - if query.is_empty() { - return; - } - - // Return if a search is already in progress - if self.finding { - if self.find_task.is_none() { - window.push_notification("There is another search in progress", cx); - return; - } else { - // Cancel the ongoing search request - self.find_task = None; - } - } - - // Block the input until the search completes - self.set_finding(true, window, cx); - - let find_users = nostr.read(cx).search(&query, cx); - - // Run task in the main thread - self.find_task = Some(cx.spawn_in(window, async move |this, cx| { - let rooms = find_users.await?; - // Update the UI with the search results - this.update_in(cx, |this, window, cx| { - this.set_results(rooms, cx); - this.set_finding(false, window, cx); - })?; - - Ok(()) - })); - } - - fn set_results(&mut self, results: Vec, cx: &mut Context) { - self.find_results.update(cx, |this, cx| { - *this = Some(results); - cx.notify(); - }); - } - - fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context) { - // Disable the input to prevent duplicate requests - self.find_input.update(cx, |this, cx| { - this.set_disabled(status, cx); - this.set_loading(status, cx); - }); - // Set the search status - self.finding = status; - cx.notify(); - } - - fn reset(&mut self, window: &mut Window, cx: &mut Context) { - // Clear all search results - self.find_results.update(cx, |this, cx| { - *this = None; - cx.notify(); - }); - - // Reset the search status - self.set_finding(false, window, cx); - - // Cancel the current search task - self.find_task = None; - cx.notify(); - } - - fn create(&mut self, window: &mut Window, cx: &mut Context) { - let chat = ChatRegistry::global(cx); - let async_chat = chat.downgrade(); - - let nostr = NostrRegistry::global(cx); - let signer_pkey = nostr.read(cx).signer_pkey(cx); - - // Get all selected public keys - let receivers = self.selected(cx); - - let task: Task> = cx.spawn_in(window, async move |_this, cx| { - let public_key = signer_pkey.await?; - - async_chat.update_in(cx, |this, window, cx| { - let room = cx.new(|_| Room::new(public_key, receivers)); - this.emit_room(room.downgrade(), cx); - - window.close_modal(cx); - })?; - - Ok(()) - }); - - task.detach(); - } - - fn select(&mut self, pkey: PublicKey, cx: &mut Context) { - self.selected_pkeys.update(cx, |this, cx| { - if this.contains(&pkey) { - this.remove(&pkey); - } else { - this.insert(pkey); - } - cx.notify(); - }); - } - - fn is_selected(&self, pkey: PublicKey, cx: &App) -> bool { - self.selected_pkeys.read(cx).contains(&pkey) - } - - fn selected(&self, cx: &Context) -> HashSet { - self.selected_pkeys.read(cx).clone() - } - - fn render_results(&self, range: Range, cx: &Context) -> Vec { - let persons = PersonRegistry::global(cx); - let hide_avatar = AppSettings::get_hide_avatar(cx); - - let Some(rooms) = self.find_results.read(cx) else { - return vec![]; - }; - - rooms - .get(range.clone()) - .into_iter() - .flatten() - .enumerate() - .map(|(ix, item)| { - let profile = persons.read(cx).get(item, cx); - let pkey = item.to_owned(); - let id = range.start + ix; - - h_flex() - .id(id) - .h_8() - .w_full() - .px_1() - .gap_2() - .rounded(cx.theme().radius) - .when(!hide_avatar, |this| { - this.child( - div() - .flex_shrink_0() - .size_6() - .rounded_full() - .overflow_hidden() - .child(Avatar::new(profile.avatar()).size(rems(1.5))), - ) - }) - .child( - h_flex() - .flex_1() - .justify_between() - .line_clamp(1) - .text_ellipsis() - .truncate() - .text_sm() - .child(profile.name()) - .when(self.is_selected(pkey, cx), |this| { - this.child( - Icon::new(IconName::CheckCircle) - .small() - .text_color(cx.theme().icon_accent), - ) - }), - ) - .hover(|this| this.bg(cx.theme().elevated_surface_background)) - .on_click(cx.listener(move |this, _ev, _window, cx| { - this.select(pkey, cx); - })) - .into_any_element() - }) - .collect() - } - - fn render_contacts(&self, range: Range, cx: &Context) -> Vec { - let persons = PersonRegistry::global(cx); - let hide_avatar = AppSettings::get_hide_avatar(cx); - let contacts = self.contact_list.read(cx); - - contacts - .get(range.clone()) - .into_iter() - .flatten() - .enumerate() - .map(|(ix, item)| { - let profile = persons.read(cx).get(item, cx); - let pkey = item.to_owned(); - let id = range.start + ix; - - h_flex() - .id(id) - .h_8() - .w_full() - .px_1() - .gap_2() - .rounded(cx.theme().radius) - .when(!hide_avatar, |this| { - this.child( - div() - .flex_shrink_0() - .size_6() - .rounded_full() - .overflow_hidden() - .child(Avatar::new(profile.avatar()).size(rems(1.5))), - ) - }) - .child( - h_flex() - .flex_1() - .justify_between() - .line_clamp(1) - .text_ellipsis() - .truncate() - .text_sm() - .child(profile.name()) - .when(self.is_selected(pkey, cx), |this| { - this.child( - Icon::new(IconName::CheckCircle) - .small() - .text_color(cx.theme().icon_accent), - ) - }), - ) - .hover(|this| this.bg(cx.theme().elevated_surface_background)) - .on_click(cx.listener(move |this, _ev, _window, cx| { - this.select(pkey, cx); - })) - .into_any_element() - }) - .collect() - } -} - -impl Render for CommandBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let window_paddings = window_paddings(window, cx); - let view_size = window.viewport_size() - - gpui::size( - window_paddings.left + window_paddings.right, - window_paddings.top + window_paddings.bottom, - ); - - let bounds = Bounds { - origin: Point::default(), - size: view_size, - }; - - let x = bounds.center().x - WIDTH / 2.; - let y = TITLEBAR_HEIGHT; - - let input_focus_handle = self.find_input.read(cx).focus_handle(cx); - let input_focused = input_focus_handle.is_focused(window); - - let results = self.find_results.read(cx).as_ref(); - let total_results = results.map_or(0, |r| r.len()); - - let contacts = self.contact_list.read(cx); - let button_label = if self.selected_pkeys.read(cx).len() > 1 { - "Create Group DM" - } else { - "Create DM" - }; - - div() - .image_cache(self.image_cache.clone()) - .w_full() - .child( - TextInput::new(&self.find_input) - .appearance(false) - .bordered(false) - .small() - .text_xs() - .when(!self.find_input.read(cx).loading, |this| { - this.suffix( - Button::new("find-icon") - .icon(IconName::Search) - .tooltip("Press Enter to search") - .transparent() - .small(), - ) - }), - ) - .when(input_focused, |this| { - this.child(deferred( - anchored() - .position(point(window_paddings.left, window_paddings.top)) - .snap_to_window() - .child( - div() - .occlude() - .w(view_size.width) - .h(view_size.height) - .on_mouse_down(MouseButton::Left, move |_ev, window, cx| { - window.focus_prev(cx); - }) - .child( - v_flex() - .absolute() - .occlude() - .relative() - .left(x) - .top(y) - .w(WIDTH) - .min_h_24() - .overflow_y_hidden() - .p_1() - .gap_1() - .justify_between() - .border_1() - .border_color(cx.theme().border.alpha(0.4)) - .bg(cx.theme().surface_background) - .shadow_md() - .rounded(cx.theme().radius_lg) - .map(|this| { - if self.show_contact_list { - this.child( - uniform_list( - "contacts", - contacts.len(), - cx.processor(|this, range, _window, cx| { - this.render_contacts(range, cx) - }), - ) - .when(!contacts.is_empty(), |this| this.h_40()), - ) - .when(contacts.is_empty(), |this| { - this.child( - h_flex() - .h_10() - .w_full() - .items_center() - .justify_center() - .text_center() - .text_xs() - .text_color(cx.theme().text_muted) - .child(SharedString::from( - "Your contact list is empty", - )), - ) - }) - } else { - this.child( - uniform_list( - "rooms", - total_results, - cx.processor(|this, range, _window, cx| { - this.render_results(range, cx) - }), - ) - .when(total_results > 0, |this| this.h_40()), - ) - .when(total_results == 0, |this| { - this.child( - h_flex() - .h_10() - .w_full() - .items_center() - .justify_center() - .text_center() - .text_xs() - .text_color(cx.theme().text_muted) - .child(SharedString::from( - "Search results appear here", - )), - ) - }) - } - }) - .child( - h_flex() - .pt_1() - .border_t_1() - .border_color(cx.theme().border_variant) - .justify_end() - .child( - Button::new("show-contacts") - .label({ - if self.show_contact_list { - "Hide contact list" - } else { - "Show contact list" - } - }) - .ghost() - .xsmall() - .on_click(cx.listener( - move |this, _ev, _window, cx| { - this.toggle_contact_list(cx); - }, - )), - ) - .when( - !self.selected_pkeys.read(cx).is_empty(), - |this| { - this.child( - Button::new("create") - .label(button_label) - .primary() - .xsmall() - .on_click(cx.listener( - move |this, _ev, window, cx| { - this.create(window, cx); - }, - )), - ) - }, - ), - ), - ), - ), - )) - }) - } -} diff --git a/crates/coop/src/sidebar/list_item.rs b/crates/coop/src/sidebar/entry.rs similarity index 76% rename from crates/coop/src/sidebar/list_item.rs rename to crates/coop/src/sidebar/entry.rs index a49f78f..e8c25d9 100644 --- a/crates/coop/src/sidebar/list_item.rs +++ b/crates/coop/src/sidebar/entry.rs @@ -14,23 +14,24 @@ use theme::ActiveTheme; use ui::avatar::Avatar; use ui::context_menu::ContextMenuExt; use ui::modal::ModalButtonProps; -use ui::{h_flex, StyledExt, WindowExtension}; +use ui::{h_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension}; use crate::dialogs::screening; #[derive(IntoElement)] -pub struct RoomListItem { +pub struct RoomEntry { ix: usize, public_key: Option, name: Option, avatar: Option, created_at: Option, kind: Option, + selected: bool, #[allow(clippy::type_complexity)] handler: Option>, } -impl RoomListItem { +impl RoomEntry { pub fn new(ix: usize) -> Self { Self { ix, @@ -40,6 +41,7 @@ impl RoomListItem { created_at: None, kind: None, handler: None, + selected: false, } } @@ -77,11 +79,25 @@ impl RoomListItem { } } -impl RenderOnce for RoomListItem { +impl Selectable for RoomEntry { + fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + + fn is_selected(&self) -> bool { + self.selected + } +} + +impl RenderOnce for RoomEntry { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let hide_avatar = AppSettings::get_hide_avatar(cx); let screening = AppSettings::get_screening(cx); + let public_key = self.public_key; + let is_selected = self.is_selected(); + h_flex() .id(self.ix) .h_9() @@ -110,13 +126,21 @@ impl RenderOnce for RoomListItem { .justify_between() .when_some(self.name, |this, name| { this.child( - div() + h_flex() .flex_1() + .justify_between() .line_clamp(1) .text_ellipsis() .truncate() .font_medium() - .child(name), + .child(name) + .when(is_selected, |this| { + this.child( + Icon::new(IconName::CheckCircle) + .small() + .text_color(cx.theme().icon_accent), + ) + }), ) }) .child( @@ -129,15 +153,17 @@ impl RenderOnce for RoomListItem { ), ) .hover(|this| this.bg(cx.theme().elevated_surface_background)) - .when_some(self.public_key, |this, public_key| { + .when_some(public_key, |this, public_key| { this.context_menu(move |this, _window, _cx| { this.menu("View Profile", Box::new(OpenPublicKey(public_key))) .menu("Copy Public Key", Box::new(CopyPublicKey(public_key))) }) - .when_some(self.handler, |this, handler| { - this.on_click(move |event, window, cx| { - handler(event, window, cx); + }) + .when_some(self.handler, |this, handler| { + this.on_click(move |event, window, cx| { + handler(event, window, cx); + if let Some(public_key) = public_key { if self.kind != Some(RoomKind::Ongoing) && screening { let screening = screening::init(public_key, window, cx); @@ -152,12 +178,12 @@ impl RenderOnce for RoomListItem { .on_cancel(move |_event, window, cx| { window.dispatch_action(Box::new(ClosePanel), cx); // Prevent closing the modal on click - // Modal will be automatically closed after closing panel + // modal will be automatically closed after closing panel false }) }); } - }) + } }) }) } diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index 041d181..22faa11 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -6,30 +6,26 @@ use anyhow::Error; use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; use common::{DebouncedDelay, RenderedTimestamp}; use dock::panel::{Panel, PanelEvent}; +use entry::RoomEntry; use gpui::prelude::FluentBuilder; use gpui::{ - div, rems, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, - Focusable, InteractiveElement, IntoElement, ParentElement, Render, RetainAllImageCache, - SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, + div, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, + IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled, Subscription, + Task, Window, }; -use list_item::RoomListItem; use nostr_sdk::prelude::*; use person::PersonRegistry; -use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use state::{NostrRegistry, FIND_DELAY}; use theme::{ActiveTheme, TABBAR_HEIGHT}; -use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::divider::Divider; use ui::indicator::Indicator; use ui::input::{InputEvent, InputState, TextInput}; use ui::notification::Notification; -use ui::{ - h_flex, v_flex, Disableable, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, -}; +use ui::{h_flex, v_flex, Disableable, IconName, Selectable, Sizable, StyledExt, WindowExtension}; -mod list_item; +mod entry; const INPUT_PLACEHOLDER: &str = "Find or start a conversation"; @@ -264,9 +260,14 @@ impl Sidebar { cx.notify(); } - fn set_input_focus(&mut self, cx: &mut Context) { + fn set_input_focus(&mut self, window: &mut Window, cx: &mut Context) { self.find_focused = !self.find_focused; cx.notify(); + + // Reset the find panel + if !self.find_focused { + self.reset(window, cx); + } } fn reset(&mut self, window: &mut Window, cx: &mut Context) { @@ -276,6 +277,12 @@ impl Sidebar { cx.notify(); }); + // Clear all selected public keys + self.selected_pkeys.update(cx, |this, cx| { + this.clear(); + cx.notify(); + }); + // Reset the search status self.set_finding(false, window, cx); @@ -285,20 +292,20 @@ impl Sidebar { } /// Select a public key in the sidebar. - fn select(&mut self, pkey: PublicKey, cx: &mut Context) { + fn select(&mut self, public_key: &PublicKey, cx: &mut Context) { self.selected_pkeys.update(cx, |this, cx| { - if this.contains(&pkey) { - this.remove(&pkey); + if this.contains(public_key) { + this.remove(public_key); } else { - this.insert(pkey); + this.insert(public_key.to_owned()); } cx.notify(); }); } /// Check if a public key is selected in the sidebar. - fn is_selected(&self, pkey: PublicKey, cx: &App) -> bool { - self.selected_pkeys.read(cx).contains(&pkey) + fn is_selected(&self, public_key: &PublicKey, cx: &App) -> bool { + self.selected_pkeys.read(cx).contains(public_key) } /// Get all selected public keys in the sidebar. @@ -317,14 +324,18 @@ impl Sidebar { // Get all selected public keys let receivers = self.selected(cx); - let task: Task> = cx.spawn_in(window, async move |_this, cx| { + let task: Task> = cx.spawn_in(window, async move |this, cx| { let public_key = signer_pkey.await?; - async_chat.update_in(cx, |this, window, cx| { - let room = cx.new(|_| Room::new(public_key, receivers)); - this.emit_room(room.downgrade(), cx); + // Reset the find panel + this.update_in(cx, |this, window, cx| { + this.reset(window, cx); + })?; - window.close_modal(cx); + // Create a new room and emit it + async_chat.update_in(cx, |this, _window, cx| { + let room = cx.new(|_| Room::new(public_key, receivers).kind(RoomKind::Ongoing)); + this.emit_room(room.downgrade(), cx); })?; Ok(()) @@ -366,7 +377,7 @@ impl Sidebar { }); }); - RoomListItem::new(range.start + ix) + RoomEntry::new(range.start + ix) .name(room.display_name(cx)) .avatar(room.display_image(cx)) .public_key(public_key) @@ -378,62 +389,67 @@ impl Sidebar { .collect() } + /// Render the contact list + fn render_results(&self, range: Range, cx: &Context) -> Vec { + let persons = PersonRegistry::global(cx); + + // Get the contact list + let Some(results) = self.find_results.read(cx) else { + return vec![]; + }; + + // Map the contact list to a list of elements + results + .get(range.clone()) + .into_iter() + .flatten() + .enumerate() + .map(|(ix, public_key)| { + let selected = self.is_selected(public_key, cx); + let profile = persons.read(cx).get(public_key, cx); + let pkey_clone = public_key.to_owned(); + let handler = cx.listener(move |this, _ev, _window, cx| { + this.select(&pkey_clone, cx); + }); + + RoomEntry::new(range.start + ix) + .name(profile.name()) + .avatar(profile.avatar()) + .on_click(handler) + .selected(selected) + .into_any_element() + }) + .collect() + } + + /// Render the contact list fn render_contacts(&self, range: Range, cx: &Context) -> Vec { let persons = PersonRegistry::global(cx); - let hide_avatar = AppSettings::get_hide_avatar(cx); + // Get the contact list let Some(contacts) = self.contact_list.read(cx) else { return vec![]; }; + // Map the contact list to a list of elements contacts .get(range.clone()) .into_iter() .flatten() .enumerate() - .map(|(ix, item)| { - let profile = persons.read(cx).get(item, cx); - let pkey = item.to_owned(); - let id = range.start + ix; + .map(|(ix, public_key)| { + let selected = self.is_selected(public_key, cx); + let profile = persons.read(cx).get(public_key, cx); + let pkey_clone = public_key.to_owned(); + let handler = cx.listener(move |this, _ev, _window, cx| { + this.select(&pkey_clone, cx); + }); - h_flex() - .id(id) - .h_8() - .w_full() - .px_1() - .gap_2() - .rounded(cx.theme().radius) - .when(!hide_avatar, |this| { - this.child( - div() - .flex_shrink_0() - .size_6() - .rounded_full() - .overflow_hidden() - .child(Avatar::new(profile.avatar()).size(rems(1.5))), - ) - }) - .child( - h_flex() - .flex_1() - .justify_between() - .line_clamp(1) - .text_ellipsis() - .truncate() - .text_sm() - .child(profile.name()) - .when(self.is_selected(pkey, cx), |this| { - this.child( - Icon::new(IconName::CheckCircle) - .small() - .text_color(cx.theme().icon_accent), - ) - }), - ) - .hover(|this| this.bg(cx.theme().elevated_surface_background)) - .on_click(cx.listener(move |this, _ev, _window, cx| { - this.select(pkey, cx); - })) + RoomEntry::new(range.start + ix) + .name(profile.name()) + .avatar(profile.avatar()) + .on_click(handler) + .selected(selected) .into_any_element() }) .collect() @@ -630,7 +646,7 @@ impl Render for Sidebar { "rooms", results.len(), cx.processor(|this, range, _window, cx| { - this.render_list_items(range, cx) + this.render_results(range, cx) }), ) .flex_1() -- 2.49.1 From 9380288fc10691e4cf044c15a6e8924c0effebeb Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 11 Feb 2026 16:08:22 +0700 Subject: [PATCH 09/11] fix --- crates/chat/src/lib.rs | 19 +++++++------ crates/coop/src/sidebar/mod.rs | 49 +++++++++++++++++----------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 001e0e6..43b6448 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -333,18 +333,17 @@ impl ChatRegistry { /// Emit an open room event. /// /// If the room is new, add it to the registry. - pub fn emit_room(&mut self, room: WeakEntity, cx: &mut Context) { - if let Some(room) = room.upgrade() { - let id = room.read(cx).id; + pub fn emit_room(&mut self, room: &Entity, cx: &mut Context) { + // Get the room's ID. + let id = room.read(cx).id; - // If the room is new, add it to the registry. - if !self.rooms.iter().any(|r| r.read(cx).id == id) { - self.rooms.insert(0, room); - } - - // Emit the open room event. - cx.emit(ChatEvent::OpenRoom(id)); + // If the room is new, add it to the registry. + if !self.rooms.iter().any(|r| r.read(cx).id == id) { + self.rooms.insert(0, room.to_owned()); } + + // Emit the open room event. + cx.emit(ChatEvent::OpenRoom(id)); } /// Close a room. diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index 22faa11..942ef6d 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -75,7 +75,7 @@ pub struct Sidebar { contact_list: Entity>>, /// Async tasks - tasks: SmallVec<[Task<()>; 1]>, + tasks: SmallVec<[Task>; 1]>, /// Event subscriptions _subscriptions: SmallVec<[Subscription; 1]>, @@ -118,11 +118,11 @@ impl Sidebar { } } InputEvent::Focus => { - this.set_input_focus(cx); + this.set_input_focus(window, cx); this.get_contact_list(window, cx); } InputEvent::Blur => { - this.set_input_focus(cx); + this.set_input_focus(window, cx); } }; }), @@ -168,16 +168,16 @@ impl Sidebar { Ok(contacts) => { this.update(cx, |this, cx| { this.set_contact_list(contacts, cx); - }) - .ok(); + })?; } Err(e) => { cx.update(|window, cx| { window.push_notification(Notification::error(e.to_string()), cx); - }) - .ok(); + })?; } }; + + Ok(()) })); } @@ -309,7 +309,7 @@ impl Sidebar { } /// Get all selected public keys in the sidebar. - fn selected(&self, cx: &Context) -> HashSet { + fn get_selected(&self, cx: &Context) -> HashSet { self.selected_pkeys.read(cx).clone() } @@ -322,26 +322,24 @@ impl Sidebar { let signer_pkey = nostr.read(cx).signer_pkey(cx); // Get all selected public keys - let receivers = self.selected(cx); + let receivers = self.get_selected(cx); - let task: Task> = cx.spawn_in(window, async move |this, cx| { + self.tasks.push(cx.spawn_in(window, async move |this, cx| { let public_key = signer_pkey.await?; + // Create a new room and emit it + async_chat.update_in(cx, |this, _window, cx| { + let room = cx.new(|_| Room::new(public_key, receivers).kind(RoomKind::Ongoing)); + this.emit_room(&room, cx); + })?; + // Reset the find panel this.update_in(cx, |this, window, cx| { this.reset(window, cx); })?; - // Create a new room and emit it - async_chat.update_in(cx, |this, _window, cx| { - let room = cx.new(|_| Room::new(public_key, receivers).kind(RoomKind::Ongoing)); - this.emit_room(room.downgrade(), cx); - })?; - Ok(()) - }); - - task.detach(); + })); } /// Get the active filter. @@ -369,11 +367,11 @@ impl Sidebar { .enumerate() .map(|(ix, item)| { let room = item.read(cx); - let weak_room = item.downgrade(); + let room_clone = item.clone(); let public_key = room.display_member(cx).public_key(); let handler = cx.listener(move |_this, _ev, _window, cx| { ChatRegistry::global(cx).update(cx, |s, cx| { - s.emit_room(weak_room.clone(), cx); + s.emit_room(&room_clone, cx); }); }); @@ -628,12 +626,14 @@ impl Render for Sidebar { .gap_1() .overflow_y_hidden() .when(show_find_panel, |this| { - this.gap_2() + this.gap_3() .when_some(self.find_results.read(cx).as_ref(), |this, results| { this.child( v_flex() - .gap_2() + .gap_1() .flex_1() + .border_b_1() + .border_color(cx.theme().border_variant) .child( div() .text_xs() @@ -657,7 +657,7 @@ impl Render for Sidebar { .when_some(self.contact_list.read(cx).as_ref(), |this, contacts| { this.child( v_flex() - .gap_2() + .gap_1() .flex_1() .child( div() @@ -708,6 +708,7 @@ impl Render for Sidebar { .label(button_label) .primary() .small() + .shadow_lg() .on_click(cx.listener(move |this, _ev, window, cx| { this.create_room(window, cx); })), -- 2.49.1 From 836d9a99e17549e82990b9722ccd3a19ea3e2a94 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 11 Feb 2026 17:46:10 +0700 Subject: [PATCH 10/11] add relay states to ui --- Cargo.lock | 1 + crates/chat/src/room.rs | 2 +- crates/coop/src/sidebar/mod.rs | 12 +++++-- crates/coop/src/workspace.rs | 61 ++++++++++++++++++++++++++++------ crates/device/src/device.rs | 14 ++++++++ crates/device/src/lib.rs | 6 ++-- crates/person/Cargo.toml | 1 + crates/person/src/lib.rs | 3 +- crates/person/src/person.rs | 2 +- crates/state/src/lib.rs | 26 +++++++++++---- 10 files changed, 102 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d14b4c6..17071ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4651,6 +4651,7 @@ version = "0.3.0" dependencies = [ "anyhow", "common", + "device", "flume", "gpui", "log", diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index f83cced..a09cfea 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -287,7 +287,7 @@ impl Room { .collect::>() .join(", "); - if profiles.len() > 2 { + if profiles.len() > 3 { name = format!("{}, +{}", name, profiles.len() - 2); } diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index 942ef6d..fa8d81c 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -23,7 +23,9 @@ use ui::divider::Divider; use ui::indicator::Indicator; use ui::input::{InputEvent, InputState, TextInput}; use ui::notification::Notification; -use ui::{h_flex, v_flex, Disableable, IconName, Selectable, Sizable, StyledExt, WindowExtension}; +use ui::{ + h_flex, v_flex, Disableable, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, +}; mod entry; @@ -635,10 +637,12 @@ impl Render for Sidebar { .border_b_1() .border_color(cx.theme().border_variant) .child( - div() + h_flex() + .gap_0p5() .text_xs() .font_semibold() .text_color(cx.theme().text_muted) + .child(Icon::new(IconName::ChevronDown)) .child(SharedString::from("Results")), ) .child( @@ -660,10 +664,12 @@ impl Render for Sidebar { .gap_1() .flex_1() .child( - div() + h_flex() + .gap_0p5() .text_xs() .font_semibold() .text_color(cx.theme().text_muted) + .child(Icon::new(IconName::ChevronDown)) .child(SharedString::from("Suggestions")), ) .child( diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index 4d82554..272c9cf 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -12,8 +12,8 @@ use gpui::{ use nostr_sdk::prelude::*; use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; -use state::NostrRegistry; -use theme::{SIDEBAR_WIDTH, TITLEBAR_HEIGHT}; +use state::{NostrRegistry, RelayState}; +use theme::{ActiveTheme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT}; use titlebar::TitleBar; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; @@ -102,7 +102,7 @@ impl Workspace { subscriptions.push( // Observe the NIP-65 state cx.observe(&nip65_state, move |this, state, cx| { - if state.read(cx).idle() { + if state.read(cx).idle() || state.read(cx).checking() { this.get_current_user(cx); } }), @@ -202,9 +202,12 @@ impl Workspace { } fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { + let nostr = NostrRegistry::global(cx); + let nip65 = nostr.read(cx).nip65_state(); + let nip17 = nostr.read(cx).nip17_state(); + h_flex() .h(TITLEBAR_HEIGHT) - .w(SIDEBAR_WIDTH) .flex_shrink_0() .justify_between() .gap_2() @@ -229,14 +232,51 @@ impl Workspace { }), ) }) - } - - fn titlebar_center(&mut self, _window: &mut Window, _cx: &Context) -> impl IntoElement { - h_flex().h(TITLEBAR_HEIGHT).flex_1() + .map(|this| match nip65.read(cx) { + RelayState::Checking => this.child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Fetching user's relay list...")), + ), + RelayState::NotConfigured => this.child( + h_flex() + .h_6() + .w_full() + .px_1() + .text_xs() + .text_color(cx.theme().warning_foreground) + .bg(cx.theme().warning_background) + .rounded_xs() + .child(SharedString::from("User hasn't configured a relay list")), + ), + _ => this, + }) + .map(|this| match nip17.read(cx) { + RelayState::Checking => { + this.child(div().text_xs().text_color(cx.theme().text_muted).child( + SharedString::from("Fetching user's messaging relay list..."), + )) + } + RelayState::NotConfigured => this.child( + h_flex() + .h_6() + .w_full() + .px_1() + .text_xs() + .text_color(cx.theme().warning_foreground) + .bg(cx.theme().warning_background) + .rounded_xs() + .child(SharedString::from( + "User hasn't configured a messaging relay list", + )), + ), + _ => this, + }) } fn titlebar_right(&mut self, _window: &mut Window, _cx: &Context) -> impl IntoElement { - h_flex().h(TITLEBAR_HEIGHT).w(SIDEBAR_WIDTH).flex_shrink_0() + h_flex().h(TITLEBAR_HEIGHT).flex_shrink_0() } } @@ -247,12 +287,11 @@ impl Render for Workspace { // Titlebar elements let left = self.titlebar_left(window, cx).into_any_element(); - let center = self.titlebar_center(window, cx).into_any_element(); let right = self.titlebar_right(window, cx).into_any_element(); // Update title bar children self.titlebar.update(cx, |this, _cx| { - this.set_children(vec![left, center, right]); + this.set_children(vec![left, right]); }); div() diff --git a/crates/device/src/device.rs b/crates/device/src/device.rs index 156be81..2f3b0d8 100644 --- a/crates/device/src/device.rs +++ b/crates/device/src/device.rs @@ -9,6 +9,20 @@ pub enum DeviceState { Set, } +impl DeviceState { + pub fn initial(&self) -> bool { + matches!(self, DeviceState::Initial) + } + + pub fn requesting(&self) -> bool { + matches!(self, DeviceState::Requesting) + } + + pub fn set(&self) -> bool { + matches!(self, DeviceState::Set) + } +} + /// Announcement #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Announcement { diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 3aefe87..199ff94 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -30,12 +30,12 @@ pub struct DeviceRegistry { /// Device signer pub device_signer: Entity>>, + /// Device state + pub state: DeviceState, + /// Device requests requests: Entity>, - /// Device state - state: DeviceState, - /// Async tasks tasks: Vec>>, diff --git a/crates/person/Cargo.toml b/crates/person/Cargo.toml index f0239bf..9d59298 100644 --- a/crates/person/Cargo.toml +++ b/crates/person/Cargo.toml @@ -7,6 +7,7 @@ publish.workspace = true [dependencies] common = { path = "../common" } state = { path = "../state" } +device = { path = "../device" } gpui.workspace = true nostr-sdk.workspace = true diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs index 3d15c38..624efe8 100644 --- a/crates/person/src/lib.rs +++ b/crates/person/src/lib.rs @@ -5,11 +5,12 @@ use std::time::Duration; use anyhow::{anyhow, Error}; use common::EventUtils; +use device::Announcement; use gpui::{App, AppContext, Context, Entity, Global, Task}; use nostr_sdk::prelude::*; pub use person::*; use smallvec::{smallvec, SmallVec}; -use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT}; +use state::{NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT}; mod person; diff --git a/crates/person/src/person.rs b/crates/person/src/person.rs index cd6a2a8..f37fffd 100644 --- a/crates/person/src/person.rs +++ b/crates/person/src/person.rs @@ -1,9 +1,9 @@ use std::cmp::Ordering; use std::hash::{Hash, Hasher}; +use device::Announcement; use gpui::SharedString; use nostr_sdk::prelude::*; -use state::Announcement; const IMAGE_RESIZER: &str = "https://wsrv.nl"; diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index cf3b9e9..553f86d 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -12,13 +12,11 @@ use nostr_lmdb::NostrLmdb; use nostr_sdk::prelude::*; mod constants; -mod device; mod event; mod nip05; mod signer; pub use constants::*; -pub use device::*; pub use event::*; pub use nip05::*; pub use signer::*; @@ -154,13 +152,13 @@ impl NostrRegistry { let client = self.client(); self.tasks.push(cx.background_spawn(async move { - // Add bootstrap relay to the relay pool - for url in BOOTSTRAP_RELAYS.into_iter() { + // Add search relay to the relay pool + for url in SEARCH_RELAYS.into_iter() { client.add_relay(url).await?; } - // Add search relay to the relay pool - for url in SEARCH_RELAYS.into_iter() { + // Add bootstrap relay to the relay pool + for url in BOOTSTRAP_RELAYS.into_iter() { client.add_relay(url).await?; } @@ -404,6 +402,12 @@ impl NostrRegistry { let client = self.client(); let nip65 = self.nip65.downgrade(); + // Set state to checking + self.nip65.update(cx, |this, cx| { + *this = RelayState::Checking; + cx.notify(); + }); + let task: Task> = cx.background_spawn(async move { let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; @@ -479,6 +483,12 @@ impl NostrRegistry { let client = self.client(); let nip17 = self.nip17.downgrade(); + // Set state to checking + self.nip17.update(cx, |this, cx| { + *this = RelayState::Checking; + cx.notify(); + }); + let task: Task> = cx.background_spawn(async move { let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; @@ -966,6 +976,10 @@ impl RelayState { matches!(self, RelayState::Idle) } + pub fn checking(&self) -> bool { + matches!(self, RelayState::Checking) + } + pub fn not_configured(&self) -> bool { matches!(self, RelayState::NotConfigured) } -- 2.49.1 From 58571a806e50028b8ae327c99849474e3f26f0f0 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 12 Feb 2026 14:11:55 +0700 Subject: [PATCH 11/11] . --- Cargo.lock | 78 ++++++------ Cargo.toml | 3 +- crates/common/Cargo.toml | 2 +- crates/coop/src/panels/greeter.rs | 16 +-- crates/coop/src/sidebar/mod.rs | 4 +- crates/coop/src/workspace.rs | 67 ++++------- crates/dock/src/lib.rs | 2 +- crates/state/src/constants.rs | 5 +- crates/state/src/lib.rs | 194 ++++++++++++++++-------------- crates/state/src/signer.rs | 45 ++++++- 10 files changed, 226 insertions(+), 190 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17071ae..f7e54a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -601,9 +601,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" dependencies = [ "arrayvec", ] @@ -620,9 +620,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.0" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" dependencies = [ "cc", "cmake", @@ -1142,9 +1142,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.57" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" dependencies = [ "clap_builder", "clap_derive", @@ -1152,9 +1152,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.57" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" dependencies = [ "anstream", "anstyle", @@ -1176,9 +1176,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "cmake" @@ -1263,7 +1263,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1708,7 +1708,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "proc-macro2", "quote", @@ -2639,7 +2639,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2741,7 +2741,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2752,7 +2752,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "anyhow", "gpui", @@ -2987,7 +2987,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "anyhow", "async-compression", @@ -3012,7 +3012,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3567,7 +3567,7 @@ checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall 0.7.0", + "redox_syscall 0.7.1", ] [[package]] @@ -3780,7 +3780,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "anyhow", "bindgen", @@ -4020,7 +4020,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" +source = "git+https://github.com/rust-nostr/nostr#b968620ae4b8c31ee1cbcdb8ef8ec9ccd87e1fc6" dependencies = [ "aes", "base64", @@ -4045,7 +4045,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" +source = "git+https://github.com/rust-nostr/nostr#b968620ae4b8c31ee1cbcdb8ef8ec9ccd87e1fc6" dependencies = [ "async-utility", "futures-core", @@ -4058,7 +4058,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" +source = "git+https://github.com/rust-nostr/nostr#b968620ae4b8c31ee1cbcdb8ef8ec9ccd87e1fc6" dependencies = [ "btreecap", "flatbuffers", @@ -4070,7 +4070,7 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" +source = "git+https://github.com/rust-nostr/nostr#b968620ae4b8c31ee1cbcdb8ef8ec9ccd87e1fc6" dependencies = [ "nostr", ] @@ -4078,7 +4078,7 @@ dependencies = [ [[package]] name = "nostr-gossip-memory" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" +source = "git+https://github.com/rust-nostr/nostr#b968620ae4b8c31ee1cbcdb8ef8ec9ccd87e1fc6" dependencies = [ "indexmap", "lru", @@ -4090,7 +4090,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" +source = "git+https://github.com/rust-nostr/nostr#b968620ae4b8c31ee1cbcdb8ef8ec9ccd87e1fc6" dependencies = [ "async-utility", "flume", @@ -4104,7 +4104,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#aeee536bdec863afffffe4819150230e20b8ad6b" +source = "git+https://github.com/rust-nostr/nostr#b968620ae4b8c31ee1cbcdb8ef8ec9ccd87e1fc6" dependencies = [ "async-utility", "async-wsocket", @@ -4638,7 +4638,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "collections", "serde", @@ -5258,9 +5258,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" dependencies = [ "bitflags 2.10.0", ] @@ -5299,7 +5299,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "derive_refineable", ] @@ -5398,7 +5398,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "anyhow", "bytes", @@ -5453,7 +5453,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "arrayvec", "log", @@ -5715,7 +5715,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "async-task", "backtrace", @@ -6330,7 +6330,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "arrayvec", "log", @@ -7297,7 +7297,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "anyhow", "async-fs", @@ -7335,7 +7335,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "perf", "quote", @@ -8936,7 +8936,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "anyhow", "chrono", @@ -8953,7 +8953,7 @@ checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" dependencies = [ "tracing", "tracing-subscriber", @@ -8964,7 +8964,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#6cd7586c161b579ba7c1ecad7dea1b95ca3dd239" +source = "git+https://github.com/zed-industries/zed#ee3f40fe25d206ca363b753e5b86e09ac6181eca" [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index 5d2a2da..8911aac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,10 @@ gpui_tokio = { git = "https://github.com/zed-industries/zed" } reqwest_client = { git = "https://github.com/zed-industries/zed" } # Nostr +nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] } +nostr-sdk = { git = "https://github.com/rust-nostr/nostr" } nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" } nostr-connect = { git = "https://github.com/rust-nostr/nostr" } -nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] } nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" } # Others diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 3fd083a..44b7514 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -6,6 +6,7 @@ publish.workspace = true [dependencies] gpui.workspace = true +nostr.workspace = true nostr-sdk.workspace = true anyhow.workspace = true @@ -19,4 +20,3 @@ log.workspace = true dirs = "5.0" qrcode = "0.14.1" -nostr = { git = "https://github.com/rust-nostr/nostr" } diff --git a/crates/coop/src/panels/greeter.rs b/crates/coop/src/panels/greeter.rs index 7b0dce1..961bc17 100644 --- a/crates/coop/src/panels/greeter.rs +++ b/crates/coop/src/panels/greeter.rs @@ -32,10 +32,10 @@ impl GreeterPanel { fn add_profile_panel(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let signer_pkey = nostr.read(cx).signer_pkey(cx); + let signer = nostr.read(cx).signer(); - cx.spawn_in(window, async move |_this, cx| { - if let Ok(public_key) = signer_pkey.await { + if let Some(public_key) = signer.public_key() { + cx.spawn_in(window, async move |_this, cx| { cx.update(|window, cx| { Workspace::add_panel( profile::init(public_key, window, cx), @@ -45,9 +45,9 @@ impl GreeterPanel { ); }) .ok(); - } - }) - .detach(); + }) + .detach(); + } } } @@ -84,6 +84,8 @@ impl Render for GreeterPanel { let nostr = NostrRegistry::global(cx); let nip65_state = nostr.read(cx).nip65_state(); let nip17_state = nostr.read(cx).nip17_state(); + let signer = nostr.read(cx).signer(); + let owned = signer.owned(); let required_actions = nip65_state.read(cx) == &RelayState::NotConfigured || nip17_state.read(cx) == &RelayState::NotConfigured; @@ -186,7 +188,7 @@ impl Render for GreeterPanel { ), ) }) - .when(!nostr.read(cx).owned_signer(), |this| { + .when(!owned, |this| { this.child( v_flex() .gap_2() diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index fa8d81c..9ea5e6f 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -321,13 +321,13 @@ impl Sidebar { let async_chat = chat.downgrade(); let nostr = NostrRegistry::global(cx); - let signer_pkey = nostr.read(cx).signer_pkey(cx); + let signer = nostr.read(cx).signer(); // Get all selected public keys let receivers = self.get_selected(cx); self.tasks.push(cx.spawn_in(window, async move |this, cx| { - let public_key = signer_pkey.await?; + let public_key = signer.get_public_key().await?; // Create a new room and emit it async_chat.update_in(cx, |this, _window, cx| { diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index 272c9cf..e2ee5e3 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -2,14 +2,13 @@ use std::sync::Arc; use chat::{ChatEvent, ChatRegistry}; use dock::dock::DockPlacement; -use dock::panel::PanelView; +use dock::panel::{PanelStyle, PanelView}; use dock::{ClosePanel, DockArea, DockItem}; use gpui::prelude::FluentBuilder; use gpui::{ div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window, }; -use nostr_sdk::prelude::*; use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; use state::{NostrRegistry, RelayState}; @@ -34,24 +33,15 @@ pub struct Workspace { /// App's Dock Area dock: Entity, - /// Current User - current_user: Entity>, - /// Event subscriptions _subscriptions: SmallVec<[Subscription; 3]>, } impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { - let titlebar = cx.new(|_| TitleBar::new()); - let dock = - cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar)); - let chat = ChatRegistry::global(cx); - let current_user = cx.new(|_| None); - - let nostr = NostrRegistry::global(cx); - let nip65_state = nostr.read(cx).nip65_state(); + let titlebar = cx.new(|_| TitleBar::new()); + let dock = cx.new(|cx| DockArea::new(window, cx).style(PanelStyle::TabBar)); let mut subscriptions = smallvec![]; @@ -99,15 +89,6 @@ impl Workspace { }), ); - subscriptions.push( - // Observe the NIP-65 state - cx.observe(&nip65_state, move |this, state, cx| { - if state.read(cx).idle() || state.read(cx).checking() { - this.get_current_user(cx); - } - }), - ); - // Set the default layout for app's dock cx.defer_in(window, |this, window, cx| { this.set_layout(window, cx); @@ -116,7 +97,6 @@ impl Workspace { Self { titlebar, dock, - current_user, _subscriptions: subscriptions, } } @@ -181,37 +161,19 @@ impl Workspace { }); } - fn get_current_user(&self, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - let current_user = self.current_user.downgrade(); - - cx.spawn(async move |_this, cx| { - if let Some(signer) = client.signer() { - if let Ok(public_key) = signer.get_public_key().await { - current_user - .update(cx, |this, cx| { - *this = Some(public_key); - cx.notify(); - }) - .ok(); - } - } - }) - .detach(); - } - fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { let nostr = NostrRegistry::global(cx); let nip65 = nostr.read(cx).nip65_state(); let nip17 = nostr.read(cx).nip17_state(); + let signer = nostr.read(cx).signer(); + let current_user = signer.public_key(); h_flex() .h(TITLEBAR_HEIGHT) .flex_shrink_0() .justify_between() .gap_2() - .when_some(self.current_user.read(cx).as_ref(), |this, public_key| { + .when_some(current_user.as_ref(), |this, public_key| { let persons = PersonRegistry::global(cx); let profile = persons.read(cx).get(public_key, cx); @@ -232,6 +194,19 @@ impl Workspace { }), ) }) + .when(nostr.read(cx).creating_signer(), |this| { + this.child(div().text_xs().text_color(cx.theme().text_muted).child( + SharedString::from("Coop is creating a new identity for you..."), + )) + }) + .when(!nostr.read(cx).connected(), |this| { + this.child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Connecting...")), + ) + }) .map(|this| match nip65.read(cx) { RelayState::Checking => this.child( div() @@ -247,7 +222,7 @@ impl Workspace { .text_xs() .text_color(cx.theme().warning_foreground) .bg(cx.theme().warning_background) - .rounded_xs() + .rounded_sm() .child(SharedString::from("User hasn't configured a relay list")), ), _ => this, @@ -266,7 +241,7 @@ impl Workspace { .text_xs() .text_color(cx.theme().warning_foreground) .bg(cx.theme().warning_background) - .rounded_xs() + .rounded_sm() .child(SharedString::from( "User hasn't configured a messaging relay list", )), diff --git a/crates/dock/src/lib.rs b/crates/dock/src/lib.rs index 972fb27..bb04b0f 100644 --- a/crates/dock/src/lib.rs +++ b/crates/dock/src/lib.rs @@ -351,7 +351,7 @@ impl DockArea { } /// Set the panel style of the dock area. - pub fn panel_style(mut self, style: PanelStyle) -> Self { + pub fn style(mut self, style: PanelStyle) -> Self { self.panel_style = style; self } diff --git a/crates/state/src/constants.rs b/crates/state/src/constants.rs index 90a1993..2c0d539 100644 --- a/crates/state/src/constants.rs +++ b/crates/state/src/constants.rs @@ -37,13 +37,12 @@ pub const USER_GIFTWRAP: &str = "user-gift-wraps"; pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"]; /// Default search relays -pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://relay.noswhere.com"]; +pub const SEARCH_RELAYS: [&str; 1] = ["wss://antiprimal.net"]; /// Default bootstrap relays -pub const BOOTSTRAP_RELAYS: [&str; 4] = [ +pub const BOOTSTRAP_RELAYS: [&str; 3] = [ "wss://relay.damus.io", "wss://relay.primal.net", - "wss://relay.nos.social", "wss://user.kindpag.es", ]; diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 553f86d..45031b2 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -48,14 +48,15 @@ pub struct NostrRegistry { /// Nostr client client: Client, + /// Whether the bootstrapping relays is connected + connected: bool, + + /// Whether coop is creating a default signer + creating_signer: bool, + /// Nostr signer signer: Arc, - /// By default, Coop generates a new signer for new users. - /// - /// This flag indicates whether the signer is user-owned or Coop-generated. - owned_signer: bool, - /// NIP-65 relay state nip65: Entity, @@ -129,16 +130,17 @@ impl NostrRegistry { cx.defer(|cx| { let nostr = NostrRegistry::global(cx); + // Connect to the bootstrapping relays nostr.update(cx, |this, cx| { this.connect(cx); - this.get_identity(cx); }); }); Self { client, + connected: false, + creating_signer: false, signer, - owned_signer: false, nip65, nip17, app_keys, @@ -151,19 +153,29 @@ impl NostrRegistry { fn connect(&mut self, cx: &mut Context) { let client = self.client(); - self.tasks.push(cx.background_spawn(async move { + let task: Task> = cx.background_spawn(async move { // Add search relay to the relay pool for url in SEARCH_RELAYS.into_iter() { - client.add_relay(url).await?; + client.add_relay(url).and_connect().await?; } // Add bootstrap relay to the relay pool for url in BOOTSTRAP_RELAYS.into_iter() { - client.add_relay(url).await?; + client.add_relay(url).and_connect().await?; } - // Connect to all added relays - client.connect().await; + Ok(()) + }); + + self.tasks.push(cx.spawn(async move |this, cx| { + // Wait for the task to complete + task.await?; + + // Update the state + this.update(cx, |this, cx| { + this.set_connected(cx); + this.get_signer(cx); + })?; Ok(()) })); @@ -174,22 +186,16 @@ impl NostrRegistry { self.client.clone() } + /// Get the nostr signer + pub fn signer(&self) -> Arc { + self.signer.clone() + } + /// Get the app keys pub fn app_keys(&self) -> &Keys { &self.app_keys } - /// Returns whether the current signer is owned by user - pub fn owned_signer(&self) -> bool { - self.owned_signer - } - - /// Set whether the current signer is owned by user - pub fn set_owned_signer(&mut self, owned: bool, cx: &mut Context) { - self.owned_signer = owned; - cx.notify(); - } - /// Get the NIP-65 state pub fn nip65_state(&self) -> Entity { self.nip65.clone() @@ -200,16 +206,26 @@ impl NostrRegistry { self.nip17.clone() } - /// Get current signer's public key - pub fn signer_pkey(&self, cx: &App) -> Task> { - let client = self.client(); + /// Get the connected state + pub fn connected(&self) -> bool { + self.connected + } - cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; + /// Set the connected state + fn set_connected(&mut self, cx: &mut Context) { + self.connected = true; + cx.notify(); + } - Ok(public_key) - }) + /// Get the creating signer status + pub fn creating_signer(&self) -> bool { + self.creating_signer + } + + /// Set the creating signer status + fn set_creating_signer(&mut self, status: bool, cx: &mut Context) { + self.creating_signer = status; + cx.notify(); } /// Get a relay hint (messaging relay) for a given public key @@ -290,18 +306,17 @@ impl NostrRegistry { T: NostrSigner + 'static, { let client = self.client(); - let signer = self.signer.clone(); + let signer = self.signer(); // Create a task to update the signer and verify the public key let task: Task> = cx.background_spawn(async move { // Update signer - signer.switch(new).await; + signer.switch(new, owned).await; // Verify signer let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; - - log::info!("Signer's public key: {public_key}"); + log::info!("Signer's public key: {}", public_key); Ok(()) }); @@ -313,8 +328,6 @@ impl NostrRegistry { // Update states this.update(cx, |this, cx| { this.reset_relay_states(cx); - this.get_relay_list(cx); - this.set_owned_signer(owned, cx); })?; Ok(()) @@ -615,83 +628,61 @@ impl NostrRegistry { }) } - /// Get local stored identity - fn get_identity(&mut self, cx: &mut Context) { - let read_credential = cx.read_credentials(KEYRING); - - self.tasks.push(cx.spawn(async move |this, cx| { - match read_credential.await { - Ok(Some((_, secret))) => { - let secret = SecretKey::from_slice(&secret)?; - let keys = Keys::new(secret); - - this.update(cx, |this, cx| { - this.set_signer(keys, false, cx); - }) - .ok(); - } - _ => { - this.update(cx, |this, cx| { - this.get_bunker(cx); - }) - .ok(); - } - } - - Ok(()) - })); - } - /// Create a new identity - fn create_identity(&mut self, cx: &mut Context) { + fn set_default_signer(&mut self, cx: &mut Context) { let client = self.client(); let keys = Keys::generate(); - let async_keys = keys.clone(); - // Get write credential task + // Create a write credential task let write_credential = cx.write_credentials( KEYRING, &keys.public_key().to_hex(), &keys.secret_key().to_secret_bytes(), ); + // Update the signer + self.set_signer(keys, false, cx); + + // Set the creating signer status + self.set_creating_signer(true, cx); + // Run async tasks in background let task: Task> = cx.background_spawn(async move { - // Build and sign the relay list event + let signer = client.signer().context("Signer not found")?; + + // Get default relay list let relay_list = default_relay_list(); - let event = EventBuilder::relay_list(relay_list) - .sign(&async_keys) - .await?; // Publish relay list event - client.send_event(&event).await?; + let event = EventBuilder::relay_list(relay_list).sign(signer).await?; + let output = client.send_event(&event).broadcast().await?; + log::info!("Published relay list event: {:?}", output.id()); - // Build and sign the metadata event + // Construct the default metadata let name = petname::petname(2, "-").unwrap_or("Cooper".to_string()); let avatar = Url::parse(&format!("https://avatar.vercel.sh/{name}")).unwrap(); let metadata = Metadata::new().display_name(&name).picture(avatar); - let event = EventBuilder::metadata(&metadata).sign(&async_keys).await?; // Publish metadata event - client.send_event(&event).await?; + let event = EventBuilder::metadata(&metadata).sign(signer).await?; + let output = client.send_event(&event).broadcast().await?; + log::info!("Published metadata event: {:?}", output.id()); - // Build and sign the contact list event + // Construct the default contact list let contacts = vec![Contact::new(PublicKey::parse(COOP_PUBKEY).unwrap())]; - let event = EventBuilder::contact_list(contacts) - .sign(&async_keys) - .await?; // Publish contact list event - client.send_event(&event).await?; + let event = EventBuilder::contact_list(contacts).sign(signer).await?; + let output = client.send_event(&event).broadcast().await?; + log::info!("Published contact list event: {:?}", output.id()); - // Build and sign the messaging relay list event + // Construct the default messaging relay list let relays = default_messaging_relays(); - let event = EventBuilder::nip17_relay_list(relays) - .sign(&async_keys) - .await?; // Publish messaging relay list event - client.send_event(&event).await?; + let event = EventBuilder::nip17_relay_list(relays).sign(signer).await?; + let output = client.send_event(&event).to_nip65().await?; + log::info!("Published messaging relay list event: {:?}", output.id()); // Write user's credentials to the system keyring write_credential.await?; @@ -703,15 +694,41 @@ impl NostrRegistry { // Wait for the task to complete task.await?; - // Update the signer this.update(cx, |this, cx| { - this.set_signer(keys, false, cx); + this.set_creating_signer(false, cx); + this.get_relay_list(cx); })?; Ok(()) })); } + /// Get local stored signer + fn get_signer(&mut self, cx: &mut Context) { + let read_credential = cx.read_credentials(KEYRING); + + self.tasks.push(cx.spawn(async move |this, cx| { + match read_credential.await { + Ok(Some((_user, secret))) => { + let secret = SecretKey::from_slice(&secret)?; + let keys = Keys::new(secret); + + this.update(cx, |this, cx| { + this.set_signer(keys, false, cx); + this.get_relay_list(cx); + })?; + } + _ => { + this.update(cx, |this, cx| { + this.get_bunker(cx); + })?; + } + } + + Ok(()) + })); + } + /// Get local stored bunker connection fn get_bunker(&mut self, cx: &mut Context) { let client = self.client(); @@ -741,6 +758,7 @@ impl NostrRegistry { Ok(signer) => { this.update(cx, |this, cx| { this.set_signer(signer, true, cx); + this.get_relay_list(cx); }) .ok(); } @@ -748,7 +766,7 @@ impl NostrRegistry { log::warn!("Failed to get bunker: {e}"); // Create a new identity if no stored bunker exists this.update(cx, |this, cx| { - this.create_identity(cx); + this.set_default_signer(cx); }) .ok(); } diff --git a/crates/state/src/signer.rs b/crates/state/src/signer.rs index afe26bf..6067fb6 100644 --- a/crates/state/src/signer.rs +++ b/crates/state/src/signer.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; use std::result::Result; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use nostr_sdk::prelude::*; @@ -8,6 +9,17 @@ use smol::lock::RwLock; #[derive(Debug)] pub struct CoopSigner { signer: RwLock>, + + /// Signer's public key + signer_pkey: RwLock>, + + /// Whether coop is creating a new identity + creating: AtomicBool, + + /// By default, Coop generates a new signer for new users. + /// + /// This flag indicates whether the signer is user-owned or Coop-generated. + owned: AtomicBool, } impl CoopSigner { @@ -17,6 +29,9 @@ impl CoopSigner { { Self { signer: RwLock::new(signer.into_nostr_signer()), + signer_pkey: RwLock::new(None), + creating: AtomicBool::new(false), + owned: AtomicBool::new(false), } } @@ -25,13 +40,39 @@ impl CoopSigner { self.signer.read().await.clone() } + /// Get public key + pub fn public_key(&self) -> Option { + self.signer_pkey.read_blocking().to_owned() + } + + /// Get the flag indicating whether the signer is creating a new identity. + pub fn creating(&self) -> bool { + self.creating.load(Ordering::SeqCst) + } + + /// Get the flag indicating whether the signer is user-owned. + pub fn owned(&self) -> bool { + self.owned.load(Ordering::SeqCst) + } + /// Switch the current signer to a new signer. - pub async fn switch(&self, new: T) + pub async fn switch(&self, new: T, owned: bool) where T: IntoNostrSigner, { + let new_signer = new.into_nostr_signer(); + let public_key = new_signer.get_public_key().await.ok(); let mut signer = self.signer.write().await; - *signer = new.into_nostr_signer(); + let mut signer_pkey = self.signer_pkey.write().await; + + // Switch to the new signer + *signer = new_signer; + + // Update the public key + *signer_pkey = public_key; + + // Update the owned flag + self.owned.store(owned, Ordering::SeqCst); } } -- 2.49.1