feat: manually handle NIP-42 auth request (#132)

* improve fetch relays

* .

* .

* .

* refactor

* refactor

* remove identity

* manually auth

* auth

* prevent duplicate message

* clean up
This commit is contained in:
reya
2025-08-30 14:38:00 +07:00
committed by GitHub
parent 49a3dedd9c
commit 807851518a
33 changed files with 1810 additions and 1443 deletions

190
Cargo.lock generated
View File

@@ -76,6 +76,12 @@ dependencies = [
"equator", "equator",
] ]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@@ -212,9 +218,9 @@ dependencies = [
[[package]] [[package]]
name = "async-compression" name = "async-compression"
version = "0.4.28" version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6448dfb3960f0b038e88c781ead1e7eb7929dfc3a71a1336ec9086c00f6d1e75" checksum = "5bee399cc3a623ec5a2db2c5b90ee0190a2260241fbe0c023ac8f7bab426aaf8"
dependencies = [ dependencies = [
"compression-codecs", "compression-codecs",
"compression-core", "compression-core",
@@ -228,9 +234,9 @@ dependencies = [
[[package]] [[package]]
name = "async-executor" name = "async-executor"
version = "1.13.2" version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8"
dependencies = [ dependencies = [
"async-task", "async-task",
"concurrent-queue", "concurrent-queue",
@@ -501,9 +507,9 @@ dependencies = [
[[package]] [[package]]
name = "base62" name = "base62"
version = "2.2.1" version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10e52a7bcb1d6beebee21fb5053af9e3cbb7a7ed1a4909e534040e676437ab1f" checksum = "0104d4d8d15e458f21dcd027ea350bf38e4364954909402f4da075aca8d0f136"
dependencies = [ dependencies = [
"rustversion", "rustversion",
] ]
@@ -597,9 +603,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]] [[package]]
name = "bit_field" name = "bit_field"
version = "0.10.2" version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]] [[package]]
name = "bitcoin-internals" name = "bitcoin-internals"
@@ -1125,7 +1131,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#11545c669e100392a8ca60063476037ab52c7cb5" source = "git+https://github.com/zed-industries/zed#7d0a303785fd73677c255fb15657e6af8dc1e3e8"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@@ -1181,9 +1187,9 @@ dependencies = [
[[package]] [[package]]
name = "compression-codecs" name = "compression-codecs"
version = "0.4.28" version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46cc6539bf1c592cff488b9f253b30bc0ec50d15407c2cf45e27bd8f308d5905" checksum = "c7eea68f0e02c2b0aa8856e9a9478444206d4b6828728e7b0697c0f8cca265cb"
dependencies = [ dependencies = [
"compression-core", "compression-core",
"deflate64", "deflate64",
@@ -1195,9 +1201,9 @@ dependencies = [
[[package]] [[package]]
name = "compression-core" name = "compression-core"
version = "0.4.28" version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2957e823c15bde7ecf1e8b64e537aa03a6be5fda0e2334e99887669e75b12e01" checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb"
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
@@ -1249,7 +1255,6 @@ dependencies = [
"gpui", "gpui",
"gpui_tokio", "gpui_tokio",
"i18n", "i18n",
"identity",
"itertools 0.13.0", "itertools 0.13.0",
"log", "log",
"nostr", "nostr",
@@ -1262,12 +1267,14 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"settings", "settings",
"signer_proxy",
"smallvec", "smallvec",
"smol", "smol",
"theme", "theme",
"title_bar", "title_bar",
"tracing-subscriber", "tracing-subscriber",
"ui", "ui",
"webbrowser",
] ]
[[package]] [[package]]
@@ -1566,7 +1573,7 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#11545c669e100392a8ca60063476037ab52c7cb5" source = "git+https://github.com/zed-industries/zed#7d0a303785fd73677c255fb15657e6af8dc1e3e8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2305,12 +2312,12 @@ dependencies = [
[[package]] [[package]]
name = "gethostname" name = "gethostname"
version = "0.4.3" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55"
dependencies = [ dependencies = [
"libc", "rustix 1.0.8",
"windows-targets 0.48.5", "windows-targets 0.52.6",
] ]
[[package]] [[package]]
@@ -2336,7 +2343,7 @@ dependencies = [
"js-sys", "js-sys",
"libc", "libc",
"r-efi", "r-efi",
"wasi 0.14.2+wasi-0.2.4", "wasi 0.14.3+wasi-0.2.4",
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -2458,7 +2465,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#11545c669e100392a8ca60063476037ab52c7cb5" source = "git+https://github.com/zed-industries/zed#7d0a303785fd73677c255fb15657e6af8dc1e3e8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@@ -2552,7 +2559,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#11545c669e100392a8ca60063476037ab52c7cb5" source = "git+https://github.com/zed-industries/zed#7d0a303785fd73677c255fb15657e6af8dc1e3e8"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -2564,7 +2571,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_tokio" name = "gpui_tokio"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#11545c669e100392a8ca60063476037ab52c7cb5" source = "git+https://github.com/zed-industries/zed#7d0a303785fd73677c255fb15657e6af8dc1e3e8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -2630,6 +2637,8 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [ dependencies = [
"allocator-api2",
"equivalent",
"foldhash", "foldhash",
] ]
@@ -2787,7 +2796,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#11545c669e100392a8ca60063476037ab52c7cb5" source = "git+https://github.com/zed-industries/zed#7d0a303785fd73677c255fb15657e6af8dc1e3e8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -2807,7 +2816,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client_tls" name = "http_client_tls"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#11545c669e100392a8ca60063476037ab52c7cb5" source = "git+https://github.com/zed-industries/zed#7d0a303785fd73677c255fb15657e6af8dc1e3e8"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
@@ -2900,7 +2909,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2 0.6.0", "socket2",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
@@ -3025,26 +3034,6 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "identity"
version = "0.2.2"
dependencies = [
"anyhow",
"client_keys",
"common",
"global",
"gpui",
"log",
"nostr-connect",
"nostr-sdk",
"settings",
"signer_proxy",
"smallvec",
"smol",
"ui",
"webbrowser",
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "1.1.0" version = "1.1.0"
@@ -3107,9 +3096,9 @@ dependencies = [
[[package]] [[package]]
name = "image-webp" name = "image-webp"
version = "0.2.3" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b" checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
dependencies = [ dependencies = [
"byteorder-lite", "byteorder-lite",
"quick-error", "quick-error",
@@ -3620,7 +3609,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#11545c669e100392a8ca60063476037ab52c7cb5" source = "git+https://github.com/zed-industries/zed#7d0a303785fd73677c255fb15657e6af8dc1e3e8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen 0.71.1", "bindgen 0.71.1",
@@ -3858,7 +3847,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#81c3b2fcc87bac915aaae3fb6c3668508d970087" source = "git+https://github.com/rust-nostr/nostr#84b1a016cffc30625567a03e2d3bcae86463f075"
dependencies = [ dependencies = [
"aes", "aes",
"base64", "base64",
@@ -3869,6 +3858,7 @@ dependencies = [
"chacha20", "chacha20",
"chacha20poly1305", "chacha20poly1305",
"getrandom 0.2.16", "getrandom 0.2.16",
"hex",
"instant", "instant",
"scrypt", "scrypt",
"secp256k1", "secp256k1",
@@ -3881,7 +3871,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#81c3b2fcc87bac915aaae3fb6c3668508d970087" source = "git+https://github.com/rust-nostr/nostr#84b1a016cffc30625567a03e2d3bcae86463f075"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3893,7 +3883,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#81c3b2fcc87bac915aaae3fb6c3668508d970087" source = "git+https://github.com/rust-nostr/nostr#84b1a016cffc30625567a03e2d3bcae86463f075"
dependencies = [ dependencies = [
"flatbuffers", "flatbuffers",
"lru", "lru",
@@ -3904,7 +3894,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#81c3b2fcc87bac915aaae3fb6c3668508d970087" source = "git+https://github.com/rust-nostr/nostr#84b1a016cffc30625567a03e2d3bcae86463f075"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"flume", "flume",
@@ -3918,11 +3908,12 @@ dependencies = [
[[package]] [[package]]
name = "nostr-relay-pool" name = "nostr-relay-pool"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#81c3b2fcc87bac915aaae3fb6c3668508d970087" source = "git+https://github.com/rust-nostr/nostr#84b1a016cffc30625567a03e2d3bcae86463f075"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
"atomic-destructor", "atomic-destructor",
"hex",
"lru", "lru",
"negentropy", "negentropy",
"nostr", "nostr",
@@ -3934,7 +3925,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#81c3b2fcc87bac915aaae3fb6c3668508d970087" source = "git+https://github.com/rust-nostr/nostr#84b1a016cffc30625567a03e2d3bcae86463f075"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3955,12 +3946,11 @@ dependencies = [
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.50.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
dependencies = [ dependencies = [
"overload", "windows-sys 0.52.0",
"winapi",
] ]
[[package]] [[package]]
@@ -4352,12 +4342,6 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "page_size" name = "page_size"
version = "0.6.0" version = "0.6.0"
@@ -4615,9 +4599,9 @@ dependencies = [
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.2" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
dependencies = [ dependencies = [
"zerovec", "zerovec",
] ]
@@ -4759,9 +4743,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn" name = "quinn"
version = "0.11.8" version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [ dependencies = [
"bytes", "bytes",
"cfg_aliases", "cfg_aliases",
@@ -4770,7 +4754,7 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"rustls", "rustls",
"socket2 0.5.10", "socket2",
"thiserror 2.0.16", "thiserror 2.0.16",
"tokio", "tokio",
"tracing", "tracing",
@@ -4779,9 +4763,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn-proto" name = "quinn-proto"
version = "0.11.12" version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [ dependencies = [
"bytes", "bytes",
"getrandom 0.3.3", "getrandom 0.3.3",
@@ -4800,16 +4784,16 @@ dependencies = [
[[package]] [[package]]
name = "quinn-udp" name = "quinn-udp"
version = "0.5.13" version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [ dependencies = [
"cfg_aliases", "cfg_aliases",
"libc", "libc",
"once_cell", "once_cell",
"socket2 0.5.10", "socket2",
"tracing", "tracing",
"windows-sys 0.59.0", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -5033,7 +5017,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#11545c669e100392a8ca60063476037ab52c7cb5" source = "git+https://github.com/zed-industries/zed#7d0a303785fd73677c255fb15657e6af8dc1e3e8"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
"workspace-hack", "workspace-hack",
@@ -5073,11 +5057,11 @@ name = "registry"
version = "0.2.2" version = "0.2.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono",
"common", "common",
"fuzzy-matcher", "fuzzy-matcher",
"global", "global",
"gpui", "gpui",
"hashbrown 0.15.5",
"itertools 0.13.0", "itertools 0.13.0",
"log", "log",
"nostr", "nostr",
@@ -5188,7 +5172,7 @@ dependencies = [
[[package]] [[package]]
name = "reqwest_client" name = "reqwest_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#11545c669e100392a8ca60063476037ab52c7cb5" source = "git+https://github.com/zed-industries/zed#7d0a303785fd73677c255fb15657e6af8dc1e3e8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -5724,7 +5708,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#11545c669e100392a8ca60063476037ab52c7cb5" source = "git+https://github.com/zed-industries/zed#7d0a303785fd73677c255fb15657e6af8dc1e3e8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@@ -6030,16 +6014,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.0" version = "0.6.0"
@@ -6175,7 +6149,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#11545c669e100392a8ca60063476037ab52c7cb5" source = "git+https://github.com/zed-industries/zed#7d0a303785fd73677c255fb15657e6af8dc1e3e8"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -6668,7 +6642,7 @@ dependencies = [
"mio", "mio",
"pin-project-lite", "pin-project-lite",
"slab", "slab",
"socket2 0.6.0", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -6915,9 +6889,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.3.19" version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [ dependencies = [
"nu-ansi-term", "nu-ansi-term",
"sharded-slab", "sharded-slab",
@@ -7203,7 +7177,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#11545c669e100392a8ca60063476037ab52c7cb5" source = "git+https://github.com/zed-industries/zed#7d0a303785fd73677c255fb15657e6af8dc1e3e8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",
@@ -7372,11 +7346,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.14.2+wasi-0.2.4" version = "0.14.3+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95"
dependencies = [ dependencies = [
"wit-bindgen-rt", "wit-bindgen",
] ]
[[package]] [[package]]
@@ -8211,13 +8185,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "wit-bindgen-rt" name = "wit-bindgen"
version = "0.39.0" version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814"
dependencies = [
"bitflags 2.9.3",
]
[[package]] [[package]]
name = "workspace-hack" name = "workspace-hack"
@@ -8253,22 +8224,23 @@ dependencies = [
[[package]] [[package]]
name = "x11rb" name = "x11rb"
version = "0.13.1" version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [ dependencies = [
"as-raw-xcb-connection", "as-raw-xcb-connection",
"gethostname", "gethostname",
"libc", "libc",
"rustix 0.38.44", "rustix 1.0.8",
"x11rb-protocol", "x11rb-protocol",
"xcursor",
] ]
[[package]] [[package]]
name = "x11rb-protocol" name = "x11rb-protocol"
version = "0.13.1" version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]] [[package]]
name = "xattr" name = "xattr"

View File

@@ -1,21 +1,27 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use chrono::{Local, TimeZone};
use global::constants::IMAGE_RESIZE_SERVICE; use global::constants::IMAGE_RESIZE_SERVICE;
use gpui::{Image, ImageFormat, SharedString}; use gpui::{Image, ImageFormat};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use qrcode::render::svg; use qrcode::render::svg;
use qrcode::QrCode; use qrcode::QrCode;
const NOW: &str = "now";
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 FALLBACK_IMG: &str = "https://image.nostr.build/c30703b48f511c293a9003be8100cdad37b8798b77a1dc3ec6eb8a20443d5dea.png"; const FALLBACK_IMG: &str = "https://image.nostr.build/c30703b48f511c293a9003be8100cdad37b8798b77a1dc3ec6eb8a20443d5dea.png";
pub trait DisplayProfile { pub trait ReadableProfile {
fn avatar_url(&self, proxy: bool) -> SharedString; fn avatar_url(&self, proxy: bool) -> String;
fn display_name(&self) -> SharedString; fn display_name(&self) -> String;
} }
impl DisplayProfile for Profile { impl ReadableProfile for Profile {
fn avatar_url(&self, proxy: bool) -> SharedString { fn avatar_url(&self, proxy: bool) -> String {
self.metadata() self.metadata()
.picture .picture
.as_ref() .as_ref()
@@ -25,7 +31,6 @@ impl DisplayProfile for Profile {
format!( format!(
"{IMAGE_RESIZE_SERVICE}/?url={picture}&w=100&h=100&fit=cover&mask=circle&default={FALLBACK_IMG}&n=-1" "{IMAGE_RESIZE_SERVICE}/?url={picture}&w=100&h=100&fit=cover&mask=circle&default={FALLBACK_IMG}&n=-1"
) )
.into()
} else { } else {
picture.into() picture.into()
} }
@@ -33,7 +38,7 @@ impl DisplayProfile for Profile {
.unwrap_or_else(|| "brand/avatar.png".into()) .unwrap_or_else(|| "brand/avatar.png".into())
} }
fn display_name(&self) -> SharedString { fn display_name(&self) -> String {
if let Some(display_name) = self.metadata().display_name.as_ref() { if let Some(display_name) = self.metadata().display_name.as_ref() {
if !display_name.is_empty() { if !display_name.is_empty() {
return display_name.into(); return display_name.into();
@@ -50,6 +55,51 @@ impl DisplayProfile for Profile {
} }
} }
pub trait ReadableTimestamp {
fn to_human_time(&self) -> String;
fn to_ago(&self) -> String;
}
impl ReadableTimestamp for Timestamp {
fn to_human_time(&self) -> String {
let input_time = match Local.timestamp_opt(self.as_u64() as i64, 0) {
chrono::LocalResult::Single(time) => time,
_ => return "9999".into(),
};
let now = Local::now();
let input_date = input_time.date_naive();
let now_date = now.date_naive();
let yesterday_date = (now - chrono::Duration::days(1)).date_naive();
let time_format = input_time.format("%H:%M %p");
match input_date {
date if date == now_date => format!("Today at {time_format}"),
date if date == yesterday_date => format!("Yesterday at {time_format}"),
_ => format!("{}, {time_format}", input_time.format("%d/%m/%y")),
}
}
fn to_ago(&self) -> String {
let input_time = match Local.timestamp_opt(self.as_u64() as i64, 0) {
chrono::LocalResult::Single(time) => time,
_ => return "1m".into(),
};
let now = Local::now();
let duration = now.signed_duration_since(input_time);
match duration {
d if d.num_seconds() < SECONDS_IN_MINUTE => NOW.into(),
d if d.num_minutes() < MINUTES_IN_HOUR => format!("{}m", d.num_minutes()),
d if d.num_hours() < HOURS_IN_DAY => format!("{}h", d.num_hours()),
d if d.num_days() < DAYS_IN_MONTH => format!("{}d", d.num_days()),
_ => input_time.format("%b %d").to_string(),
}
}
}
pub trait TextUtils { pub trait TextUtils {
fn to_public_key(&self) -> Result<PublicKey, Error>; fn to_public_key(&self) -> Result<PublicKey, Error>;
fn to_qr(&self) -> Option<Arc<Image>>; fn to_qr(&self) -> Option<Arc<Image>>;
@@ -84,7 +134,7 @@ impl<T: AsRef<str>> TextUtils for T {
} }
} }
pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> SharedString { pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> String {
let Ok(pubkey) = public_key.to_bech32(); let Ok(pubkey) = public_key.to_bech32();
format!( format!(
@@ -92,5 +142,4 @@ pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> SharedString {
&pubkey[0..(len + 1)], &pubkey[0..(len + 1)],
&pubkey[pubkey.len() - len..] &pubkey[pubkey.len() - len..]
) )
.into()
} }

View File

@@ -30,7 +30,6 @@ icons = [
assets = { path = "../assets" } assets = { path = "../assets" }
ui = { path = "../ui" } ui = { path = "../ui" }
title_bar = { path = "../title_bar" } title_bar = { path = "../title_bar" }
identity = { path = "../identity" }
theme = { path = "../theme" } theme = { path = "../theme" }
common = { path = "../common" } common = { path = "../common" }
global = { path = "../global" } global = { path = "../global" }
@@ -38,6 +37,7 @@ registry = { path = "../registry" }
settings = { path = "../settings" } settings = { path = "../settings" }
client_keys = { path = "../client_keys" } client_keys = { path = "../client_keys" }
auto_update = { path = "../auto_update" } auto_update = { path = "../auto_update" }
signer_proxy = { path = "../signer_proxy" }
rust-i18n.workspace = true rust-i18n.workspace = true
i18n.workspace = true i18n.workspace = true
@@ -59,5 +59,6 @@ smallvec.workspace = true
smol.workspace = true smol.workspace = true
futures.workspace = true futures.workspace = true
oneshot.workspace = true oneshot.workspace = true
webbrowser.workspace = true
tracing-subscriber = { version = "0.3.18", features = ["fmt"] } tracing-subscriber = { version = "0.3.18", features = ["fmt"] }

View File

@@ -0,0 +1,34 @@
use std::sync::Mutex;
use gpui::{actions, App};
actions!(coop, [DarkMode, Settings, Logout, Quit]);
pub fn load_embedded_fonts(cx: &App) {
let asset_source = cx.asset_source();
let font_paths = asset_source.list("fonts").unwrap();
let embedded_fonts = Mutex::new(Vec::new());
let executor = cx.background_executor();
executor.block(executor.scoped(|scope| {
for font_path in &font_paths {
if !font_path.ends_with(".ttf") {
continue;
}
scope.spawn(async {
let font_bytes = asset_source.load(font_path).unwrap().unwrap();
embedded_fonts.lock().unwrap().push(font_bytes);
});
}
}));
cx.text_system()
.add_fonts(embedded_fonts.into_inner().unwrap())
.unwrap();
}
pub fn quit(_: &Quit, cx: &mut App) {
log::info!("Gracefully quitting the application . . .");
cx.quit();
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,193 +1,45 @@
use std::collections::BTreeSet; use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::{anyhow, Error};
use assets::Assets; use assets::Assets;
use common::event::EventUtils; use global::constants::{APP_ID, APP_NAME};
use global::constants::{ use global::{ingester, nostr_client, sent_ids, starting_time};
APP_ID, APP_NAME, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT,
SEARCH_RELAYS, WAIT_FOR_FINISH,
};
use global::{global_channel, nostr_client, processed_events, starting_time, NostrSignal};
use gpui::{ use gpui::{
actions, point, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString,
SharedString, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
WindowKind, WindowOptions, WindowOptions,
}; };
use itertools::Itertools;
use nostr_sdk::prelude::*;
use smol::channel::{self, Sender};
use theme::Theme; use theme::Theme;
use ui::Root; use ui::Root;
use crate::actions::{load_embedded_fonts, quit, Quit};
pub(crate) mod actions;
pub(crate) mod chatspace; pub(crate) mod chatspace;
pub(crate) mod views; pub(crate) mod views;
i18n::init!(); i18n::init!();
actions!(coop, [Quit]);
fn main() { fn main() {
// Initialize logging // Initialize logging
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
// Initialize the Nostr Client // Initialize the Nostr client
let client = nostr_client(); let _client = nostr_client();
// Initialize the ingester
let _ingester = ingester();
// Initialize the starting time // Initialize the starting time
let _starting_time = starting_time(); let _starting_time = starting_time();
// Initialize the sent IDs storage
let _sent_ids = sent_ids();
// Initialize the Application // Initialize the Application
let app = Application::new() let app = Application::new()
.with_assets(Assets) .with_assets(Assets)
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new())); .with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
let (pubkey_tx, pubkey_rx) = channel::bounded::<PublicKey>(1024);
let (event_tx, event_rx) = channel::bounded::<Event>(2048);
app.background_executor()
.spawn(async move {
// Subscribe for app updates from the bootstrap relays.
if let Err(e) = connect(client).await {
log::error!("Failed to connect to bootstrap relays: {e}");
}
// Handle Nostr notifications.
//
// Send the redefined signal back to GPUI via channel.
if let Err(e) = handle_nostr_notifications(&event_tx).await {
log::error!("Failed to handle Nostr notifications: {e}");
}
})
.detach();
app.background_executor()
.spawn(async move {
let channel = global_channel();
loop {
if let Ok(signer) = client.signer().await {
if let Ok(public_key) = signer.get_public_key().await {
// Notify the app that the signer has been set.
_ = channel.0.send(NostrSignal::SignerSet(public_key)).await;
// Get the NIP-65 relays for the public key.
get_nip65_relays(public_key).await.ok();
break;
}
}
smol::Timer::after(Duration::from_secs(1)).await;
}
})
.detach();
app.background_executor()
.spawn(async move {
let duration = Duration::from_millis(METADATA_BATCH_TIMEOUT);
let mut processed_pubkeys: BTreeSet<PublicKey> = BTreeSet::new();
let mut batch: BTreeSet<PublicKey> = BTreeSet::new();
/// Internal events for the metadata batching system
enum BatchEvent {
NewKeys(PublicKey),
Timeout,
Closed,
}
loop {
let duration = smol::Timer::after(duration);
let recv = || async {
if let Ok(public_key) = pubkey_rx.recv().await {
BatchEvent::NewKeys(public_key)
} else {
BatchEvent::Closed
}
};
let timeout = || async {
duration.await;
BatchEvent::Timeout
};
match smol::future::or(recv(), timeout()).await {
BatchEvent::NewKeys(public_key) => {
// Prevent duplicate keys from being processed
if processed_pubkeys.insert(public_key) {
batch.insert(public_key);
}
// Process the batch if it's full
if batch.len() >= METADATA_BATCH_LIMIT {
sync_data_for_pubkeys(std::mem::take(&mut batch)).await;
}
}
BatchEvent::Timeout => {
if !batch.is_empty() {
sync_data_for_pubkeys(std::mem::take(&mut batch)).await;
}
}
BatchEvent::Closed => {
if !batch.is_empty() {
sync_data_for_pubkeys(std::mem::take(&mut batch)).await;
}
break;
}
}
}
})
.detach();
app.background_executor()
.spawn(async move {
let channel = global_channel();
let mut counter = 0;
loop {
// Signer is unset, probably user is not ready to retrieve gift wrap events
if client.signer().await.is_err() {
smol::Timer::after(Duration::from_secs(1)).await;
continue;
}
let duration = smol::Timer::after(Duration::from_secs(WAIT_FOR_FINISH));
let recv = || async {
// no inline
(event_rx.recv().await).ok()
};
let timeout = || async {
duration.await;
None
};
match smol::future::or(recv(), timeout()).await {
Some(event) => {
let cached = unwrap_gift(&event, &pubkey_tx).await;
// Increment the total messages counter if message is not from cache
if !cached {
counter += 1;
}
// Send partial finish signal to GPUI
if counter >= 20 {
channel.0.send(NostrSignal::PartialFinish).await.ok();
// Reset counter
counter = 0;
}
}
None => {
// Notify the UI that the processing is finished
channel.0.send(NostrSignal::Finish).await.ok();
}
}
}
})
.detach();
// Run application // Run application
app.run(move |cx| { app.run(move |cx| {
// Load embedded fonts in assets/fonts // Load embedded fonts in assets/fonts
@@ -264,280 +116,3 @@ fn main() {
.expect("Failed to open window. Please restart the application."); .expect("Failed to open window. Please restart the application.");
}); });
} }
fn load_embedded_fonts(cx: &App) {
let asset_source = cx.asset_source();
let font_paths = asset_source.list("fonts").unwrap();
let embedded_fonts = Mutex::new(Vec::new());
let executor = cx.background_executor();
executor.block(executor.scoped(|scope| {
for font_path in &font_paths {
if !font_path.ends_with(".ttf") {
continue;
}
scope.spawn(async {
let font_bytes = asset_source.load(font_path).unwrap().unwrap();
embedded_fonts.lock().unwrap().push(font_bytes);
});
}
}));
cx.text_system()
.add_fonts(embedded_fonts.into_inner().unwrap())
.unwrap();
}
fn quit(_: &Quit, cx: &mut App) {
log::info!("Gracefully quitting the application . . .");
cx.quit();
}
async fn connect(client: &Client) -> Result<(), Error> {
for relay in BOOTSTRAP_RELAYS.into_iter() {
client.add_relay(relay).await?;
}
log::info!("Connected to bootstrap relays");
for relay in SEARCH_RELAYS.into_iter() {
client.add_relay(relay).await?;
}
log::info!("Connected to search relays");
// Establish connection to relays
client.connect().await;
Ok(())
}
async fn handle_nostr_notifications(event_tx: &Sender<Event>) -> Result<(), Error> {
let client = nostr_client();
let channel = global_channel();
let auto_close = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let mut notifications = client.notifications();
while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, .. } = notification else {
continue;
};
let RelayMessage::Event { event, .. } = message else {
continue;
};
// Skip events that have already been processed
if !processed_events().write().await.insert(event.id) {
continue;
}
match event.kind {
Kind::RelayList => {
// Get metadata for event's pubkey that matches the current user's pubkey
if let Ok(true) = is_from_current_user(&event).await {
let sub_id = SubscriptionId::new("metadata");
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::InboxRelays];
let filter = Filter::new().kinds(kinds).author(event.pubkey).limit(10);
client
.subscribe_with_id(sub_id, filter, Some(auto_close))
.await
.ok();
}
}
Kind::InboxRelays => {
if let Ok(true) = is_from_current_user(&event).await {
// Get all inbox relays
let relays = event
.tags
.filter_standardized(TagKind::Relay)
.filter_map(|t| {
if let TagStandard::Relay(url) = t {
Some(url.to_owned())
} else {
None
}
})
.collect_vec();
if !relays.is_empty() {
// Add relays to nostr client
for relay in relays.iter() {
_ = client.add_relay(relay).await;
_ = client.connect_relay(relay).await;
}
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(event.pubkey);
let sub_id = SubscriptionId::new("gift-wrap");
// Notify the UI that the current user has set up the DM relays
channel.0.send(NostrSignal::DmRelaysFound).await.ok();
if client
.subscribe_with_id_to(relays.clone(), sub_id, filter, None)
.await
.is_ok()
{
log::info!("Subscribing to messages in: {relays:?}");
}
}
}
}
Kind::ContactList => {
if let Ok(true) = is_from_current_user(&event).await {
let public_keys: Vec<PublicKey> = event.tags.public_keys().copied().collect();
let kinds = vec![Kind::Metadata, Kind::ContactList];
let lens = public_keys.len() * kinds.len();
let filter = Filter::new().limit(lens).authors(public_keys).kinds(kinds);
client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(auto_close))
.await
.ok();
}
}
Kind::Metadata => {
channel
.0
.send(NostrSignal::Metadata(event.into_owned()))
.await
.ok();
}
Kind::GiftWrap => {
event_tx.send(event.into_owned()).await.ok();
}
_ => {}
}
}
Ok(())
}
async fn get_nip65_relays(public_key: PublicKey) -> Result<(), Error> {
let client = nostr_client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let sub_id = SubscriptionId::new("nip65-relays");
let filter = Filter::new()
.kind(Kind::RelayList)
.author(public_key)
.limit(1);
client.subscribe_with_id(sub_id, filter, Some(opts)).await?;
Ok(())
}
async fn is_from_current_user(event: &Event) -> Result<bool, Error> {
let client = nostr_client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
Ok(public_key == event.pubkey)
}
async fn sync_data_for_pubkeys(public_keys: BTreeSet<PublicKey>) {
let client = nostr_client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList];
let filter = Filter::new()
.limit(public_keys.len() * kinds.len())
.authors(public_keys)
.kinds(kinds);
client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await
.ok();
}
/// Stores an unwrapped event in local database with reference to original
async fn set_unwrapped(root: EventId, unwrapped: &Event) -> Result<(), Error> {
let client = nostr_client();
// Save unwrapped event
client.database().save_event(unwrapped).await?;
// Create a reference event pointing to the unwrapped event
let event = EventBuilder::new(Kind::ApplicationSpecificData, "")
.tags(vec![Tag::identifier(root), Tag::event(unwrapped.id)])
.sign(&Keys::generate())
.await?;
// Save reference event
client.database().save_event(&event).await?;
Ok(())
}
/// Retrieves a previously unwrapped event from local database
async fn get_unwrapped(root: EventId) -> Result<Event, Error> {
let client = nostr_client();
let filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.identifier(root)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() {
let target_id = event.tags.event_ids().collect_vec()[0];
if let Some(event) = client.database().event_by_id(target_id).await? {
Ok(event)
} else {
Err(anyhow!("Event not found."))
}
} else {
Err(anyhow!("Event is not cached yet."))
}
}
/// Unwraps a gift-wrapped event and processes its contents.
async fn unwrap_gift(gift: &Event, pubkey_tx: &Sender<PublicKey>) -> bool {
let client = nostr_client();
let channel = global_channel();
let mut is_cached = false;
let event = match get_unwrapped(gift.id).await {
Ok(event) => {
is_cached = true;
event
}
Err(_) => {
match client.unwrap_gift_wrap(gift).await {
Ok(unwrap) => {
// Sign the unwrapped event with a RANDOM KEYS
let Ok(unwrapped) = unwrap.rumor.sign_with_keys(&Keys::generate()) else {
log::error!("Failed to sign event");
return false;
};
// Save this event to the database for future use.
if let Err(e) = set_unwrapped(gift.id, &unwrapped).await {
log::warn!("Failed to cache unwrapped event: {e}")
}
unwrapped
}
Err(e) => {
log::error!("Failed to unwrap event: {e}");
return false;
}
}
}
};
// Send all pubkeys to the metadata batch to sync data
for public_key in event.all_pubkeys() {
pubkey_tx.send(public_key).await.ok();
}
// Send a notify to GPUI if this is a new message
if starting_time() <= &event.created_at {
channel.0.send(NostrSignal::GiftWrap(event)).await.ok();
}
is_cached
}

View File

@@ -2,10 +2,10 @@ use std::time::Duration;
use anyhow::Error; use anyhow::Error;
use client_keys::ClientKeys; use client_keys::ClientKeys;
use common::display::DisplayProfile; use common::display::ReadableProfile;
use common::handle_auth::CoopAuthUrlHandler; use common::handle_auth::CoopAuthUrlHandler;
use global::constants::ACCOUNT_IDENTIFIER; use global::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
use global::nostr_client; use global::{ingester, nostr_client, IngesterSignal};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
@@ -13,7 +13,6 @@ use gpui::{
StatefulInteractiveElement, Styled, Task, WeakEntity, Window, StatefulInteractiveElement, Styled, Task, WeakEntity, Window,
}; };
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use identity::Identity;
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -25,6 +24,8 @@ use ui::input::{InputState, TextInput};
use ui::popup_menu::PopupMenu; use ui::popup_menu::PopupMenu;
use ui::{h_flex, v_flex, ContextModal, Disableable, Sizable, StyledExt}; use ui::{h_flex, v_flex, ContextModal, Disableable, Sizable, StyledExt};
use crate::chatspace::ChatSpace;
pub fn init( pub fn init(
secret: String, secret: String,
profile: Profile, profile: Profile,
@@ -69,7 +70,7 @@ impl Account {
self.nostr_connect(uri, window, cx); self.nostr_connect(uri, window, cx);
} }
} else if self.is_extension { } else if self.is_extension {
self.proxy(window, cx); self.set_proxy(window, cx);
} else if let Ok(enc) = EncryptedSecretKey::from_bech32(&self.stored_secret) { } else if let Ok(enc) = EncryptedSecretKey::from_bech32(&self.stored_secret) {
self.keys(enc, window, cx); self.keys(enc, window, cx);
} else { } else {
@@ -82,8 +83,7 @@ impl Account {
let client_keys = ClientKeys::global(cx); let client_keys = ClientKeys::global(cx);
let app_keys = client_keys.read(cx).keys(); let app_keys = client_keys.read(cx).keys();
let secs = 30; let timeout = Duration::from_secs(BUNKER_TIMEOUT);
let timeout = Duration::from_secs(secs);
let mut signer = NostrConnect::new(uri, app_keys, timeout, None).unwrap(); let mut signer = NostrConnect::new(uri, app_keys, timeout, None).unwrap();
// Handle auth url with the default browser // Handle auth url with the default browser
@@ -109,8 +109,8 @@ impl Account {
.detach(); .detach();
} }
fn proxy(&mut self, _window: &mut Window, cx: &mut Context<Self>) { fn set_proxy(&mut self, window: &mut Window, cx: &mut Context<Self>) {
Identity::start_browser_proxy(cx); ChatSpace::proxy_signer(window, cx);
} }
fn keys(&mut self, enc: EncryptedSecretKey, window: &mut Window, cx: &mut Context<Self>) { fn keys(&mut self, enc: EncryptedSecretKey, window: &mut Window, cx: &mut Context<Self>) {
@@ -239,26 +239,23 @@ impl Account {
.detach(); .detach();
} }
fn logout(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn logout(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
let task: Task<Result<(), Error>> = cx.background_spawn(async move { cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
let ingester = ingester();
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::ApplicationSpecificData) .kind(Kind::ApplicationSpecificData)
.identifier(ACCOUNT_IDENTIFIER); .identifier(ACCOUNT_IDENTIFIER);
// Delete account // Delete account
client.database().delete(filter).await?; client.database().delete(filter).await.ok();
Ok(()) // Unset the client's signer
}); client.unset_signer().await;
cx.spawn_in(window, async move |_this, cx| { // Notify the channel about the signer being unset
if task.await.is_ok() { ingester.send(IngesterSignal::SignerUnset).await;
cx.update(|_window, cx| {
cx.restart();
})
.ok();
}
}) })
.detach(); .detach();
} }

View File

@@ -1,9 +1,9 @@
use std::collections::{BTreeSet, HashMap}; use std::collections::HashMap;
use anyhow::anyhow; use anyhow::anyhow;
use common::display::DisplayProfile; use common::display::{ReadableProfile, ReadableTimestamp};
use common::nip96::nip96_upload; use common::nip96::nip96_upload;
use global::nostr_client; use global::{nostr_client, sent_ids};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, list, px, red, relative, rems, svg, white, Action, AnyElement, App, AppContext, div, img, list, px, red, relative, rems, svg, white, Action, AnyElement, App, AppContext,
@@ -14,7 +14,6 @@ use gpui::{
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use identity::Identity;
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::message::RenderedMessage; use registry::message::RenderedMessage;
@@ -31,7 +30,6 @@ use ui::dock_area::panel::{Panel, PanelEvent};
use ui::emoji_picker::EmojiPicker; use ui::emoji_picker::EmojiPicker;
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::notification::Notification;
use ui::popup_menu::PopupMenu; use ui::popup_menu::PopupMenu;
use ui::text::RenderedText; use ui::text::RenderedText;
use ui::{ use ui::{
@@ -56,7 +54,7 @@ pub struct Chat {
// Chat Room // Chat Room
room: Entity<Room>, room: Entity<Room>,
list_state: ListState, list_state: ListState,
messages: BTreeSet<RenderedMessage>, messages: Vec<RenderedMessage>,
rendered_texts_by_id: HashMap<EventId, RenderedText>, rendered_texts_by_id: HashMap<EventId, RenderedText>,
reports_by_id: HashMap<EventId, Vec<SendReport>>, reports_by_id: HashMap<EventId, Vec<SendReport>>,
// New Message // New Message
@@ -107,7 +105,7 @@ impl Chat {
} }
Err(e) => { Err(e) => {
cx.update(|window, cx| { cx.update(|window, cx| {
window.push_notification(Notification::error(e.to_string()), cx); window.push_notification(e.to_string(), cx);
}) })
.ok(); .ok();
} }
@@ -138,10 +136,10 @@ impl Chat {
// Subscribe to room events // Subscribe to room events
cx.subscribe_in(&room, window, move |this, _, signal, window, cx| { cx.subscribe_in(&room, window, move |this, _, signal, window, cx| {
match signal { match signal {
RoomSignal::NewMessage(event) => { RoomSignal::NewMessage((gift_wrap_id, event)) => {
if !this.is_seen_message(event) { if !this.is_sent_by_coop(gift_wrap_id) {
this.insert_message(event, cx); this.insert_message(event, cx);
}; }
} }
RoomSignal::Refresh => { RoomSignal::Refresh => {
this.load_messages(window, cx); this.load_messages(window, cx);
@@ -156,7 +154,7 @@ impl Chat {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
uploading: false, uploading: false,
sending: false, sending: false,
messages: BTreeSet::new(), messages: Vec::new(),
rendered_texts_by_id: HashMap::new(), rendered_texts_by_id: HashMap::new(),
reports_by_id: HashMap::new(), reports_by_id: HashMap::new(),
room, room,
@@ -219,14 +217,10 @@ impl Chat {
content content
} }
/// Check if the event is a seen message /// Check if the event is sent by Coop
fn is_seen_message(&self, event: &Event) -> bool { fn is_sent_by_coop(&self, gift_wrap_id: &EventId) -> bool {
if let Some(message) = self.messages.last() { let sent_ids = sent_ids();
let duration = event.created_at.as_u64() - message.created_at.as_u64(); sent_ids.read_blocking().contains(gift_wrap_id)
message.content == event.content && message.author == event.pubkey && duration <= 20
} else {
false
}
} }
/// Set the sending state of the chat panel /// Set the sending state of the chat panel
@@ -263,7 +257,7 @@ impl Chat {
// Get the current room entity // Get the current room entity
let room = self.room.read(cx); let room = self.room.read(cx);
let identity = Identity::read_global(cx).public_key(); let identity = Registry::read_global(cx).identity(cx).public_key();
// Create a temporary message for optimistic update // Create a temporary message for optimistic update
let temp_message = room.create_temp_message(identity, &content, replies.as_ref()); let temp_message = room.create_temp_message(identity, &content, replies.as_ref());
@@ -346,7 +340,7 @@ impl Chat {
let new_len = 1; let new_len = 1;
// Extend the messages list with the new events // Extend the messages list with the new events
self.messages.insert(event.into()); self.messages.push(event.into());
// Update list state with the new messages // Update list state with the new messages
self.list_state.splice(old_len..old_len, new_len); self.list_state.splice(old_len..old_len, new_len);
@@ -360,11 +354,12 @@ impl Chat {
E::Item: Into<RenderedMessage>, E::Item: Into<RenderedMessage>,
{ {
let old_len = self.messages.len(); let old_len = self.messages.len();
let events: Vec<_> = events.into_iter().map(Into::into).collect(); let events: Vec<RenderedMessage> = events.into_iter().map(Into::into).collect();
let new_len = events.len(); let new_len = events.len();
// Extend the messages list with the new events // Extend the messages list with the new events
self.messages.extend(events); self.messages.extend(events);
self.messages.sort_by_key(|ev| ev.created_at);
// Update list state with the new messages // Update list state with the new messages
self.list_state.splice(old_len..old_len, new_len); self.list_state.splice(old_len..old_len, new_len);
@@ -532,7 +527,7 @@ impl Chat {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Stateful<Div> { ) -> Stateful<Div> {
let Some(message) = self.messages.iter().nth(ix) else { let Some(message) = self.messages.get(ix) else {
return div().id(ix); return div().id(ix);
}; };
@@ -591,7 +586,7 @@ impl Chat {
.text_color(cx.theme().text) .text_color(cx.theme().text)
.child(author.display_name()), .child(author.display_name()),
) )
.child(div().child(message.ago())) .child(div().child(message.created_at.to_human_time()))
.when_some(is_sent_success, |this, status| { .when_some(is_sent_success, |this, status| {
this.when(status, |this| { this.when(status, |this| {
this.child(self.render_message_sent(&id, cx)) this.child(self.render_message_sent(&id, cx))

View File

@@ -28,8 +28,8 @@ impl Subject {
cx.new(|_| Self { input }) cx.new(|_| Self { input })
} }
pub fn new_subject(&self, cx: &App) -> SharedString { pub fn new_subject(&self, cx: &App) -> String {
self.input.read(cx).value().clone() self.input.read(cx).value().to_string()
} }
} }

View File

@@ -2,7 +2,7 @@ use std::ops::Range;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use common::display::{DisplayProfile, TextUtils}; use common::display::{ReadableProfile, TextUtils};
use common::nip05::nip05_profile; use common::nip05::nip05_profile;
use global::constants::BOOTSTRAP_RELAYS; use global::constants::BOOTSTRAP_RELAYS;
use global::nostr_client; use global::nostr_client;

View File

@@ -2,7 +2,7 @@ use std::time::Duration;
use client_keys::ClientKeys; use client_keys::ClientKeys;
use common::handle_auth::CoopAuthUrlHandler; use common::handle_auth::CoopAuthUrlHandler;
use global::constants::ACCOUNT_IDENTIFIER; use global::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
use global::nostr_client; use global::nostr_client;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
@@ -264,8 +264,7 @@ impl Login {
let client_keys = ClientKeys::global(cx); let client_keys = ClientKeys::global(cx);
let app_keys = client_keys.read(cx).keys(); let app_keys = client_keys.read(cx).keys();
let secs = 30; let timeout = Duration::from_secs(BUNKER_TIMEOUT);
let timeout = Duration::from_secs(secs);
let mut signer = NostrConnect::new(uri, app_keys, timeout, None).unwrap(); let mut signer = NostrConnect::new(uri, app_keys, timeout, None).unwrap();
// Handle auth url with the default browser // Handle auth url with the default browser
@@ -273,7 +272,7 @@ impl Login {
// Start countdown // Start countdown
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
for i in (0..=secs).rev() { for i in (0..=BUNKER_TIMEOUT).rev() {
if i == 0 { if i == 0 {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_countdown(None, cx); this.set_countdown(None, cx);

View File

@@ -4,11 +4,11 @@ pub mod chat;
pub mod compose; pub mod compose;
pub mod edit_profile; pub mod edit_profile;
pub mod login; pub mod login;
pub mod messaging_relays;
pub mod new_account; pub mod new_account;
pub mod onboarding; pub mod onboarding;
pub mod preferences; pub mod preferences;
pub mod screening; pub mod screening;
pub mod setup_relay;
pub mod sidebar; pub mod sidebar;
pub mod user_profile; pub mod user_profile;
pub mod welcome; pub mod welcome;

View File

@@ -12,7 +12,6 @@ use gpui::{
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
}; };
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use identity::Identity;
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -21,7 +20,7 @@ use ui::dock_area::panel::{Panel, PanelEvent};
use ui::popup_menu::PopupMenu; use ui::popup_menu::PopupMenu;
use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt}; use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt};
use crate::chatspace; use crate::chatspace::{self, ChatSpace};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
Onboarding::new(window, cx) Onboarding::new(window, cx)
@@ -159,8 +158,8 @@ impl Onboarding {
.detach(); .detach();
} }
fn set_proxy(&mut self, _window: &mut Window, cx: &mut Context<Self>) { fn set_proxy(&mut self, window: &mut Window, cx: &mut Context<Self>) {
Identity::start_browser_proxy(cx); ChatSpace::proxy_signer(window, cx);
} }
fn write_uri_to_disk(&mut self, uri: &NostrConnectURI, cx: &mut Context<Self>) { fn write_uri_to_disk(&mut self, uri: &NostrConnectURI, cx: &mut Context<Self>) {

View File

@@ -1,11 +1,11 @@
use common::display::DisplayProfile; use common::display::ReadableProfile;
use gpui::http_client::Url; use gpui::http_client::Url;
use gpui::{ use gpui::{
div, px, relative, rems, App, AppContext, Context, Entity, InteractiveElement, IntoElement, div, px, relative, rems, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window,
}; };
use i18n::t; use i18n::t;
use identity::Identity; use nostr_sdk::prelude::*;
use registry::Registry; use registry::Registry;
use settings::AppSettings; use settings::AppSettings;
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -16,7 +16,7 @@ use ui::modal::ModalButtonProps;
use ui::switch::Switch; use ui::switch::Switch;
use ui::{v_flex, ContextModal, IconName, Sizable, Size, StyledExt}; use ui::{v_flex, ContextModal, IconName, Sizable, Size, StyledExt};
use crate::views::{edit_profile, messaging_relays}; use crate::views::{edit_profile, setup_relay};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
Preferences::new(window, cx) Preferences::new(window, cx)
@@ -89,7 +89,7 @@ impl Preferences {
fn open_relays(&self, window: &mut Window, cx: &mut Context<Self>) { fn open_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
let title = SharedString::new(t!("relays.modal_title")); let title = SharedString::new(t!("relays.modal_title"));
let view = messaging_relays::init(window, cx); let view = setup_relay::init(Kind::InboxRelays, window, cx);
let weak_view = view.downgrade(); let weak_view = view.downgrade();
window.open_modal(cx, move |this, _window, _cx| { window.open_modal(cx, move |this, _window, _cx| {
@@ -115,8 +115,7 @@ impl Preferences {
impl Render for Preferences { impl Render for Preferences {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let input_state = self.media_input.downgrade(); let input_state = self.media_input.downgrade();
let identity = Identity::read_global(cx).public_key(); let profile = Registry::read_global(cx).identity(cx);
let profile = Registry::read_global(cx).get_person(&identity, cx);
let backup_messages = AppSettings::get_backup_messages(cx); let backup_messages = AppSettings::get_backup_messages(cx);
let screening = AppSettings::get_screening(cx); let screening = AppSettings::get_screening(cx);

View File

@@ -1,4 +1,4 @@
use common::display::{shorten_pubkey, DisplayProfile}; use common::display::{shorten_pubkey, ReadableProfile};
use common::nip05::nip05_verify; use common::nip05::nip05_verify;
use global::nostr_client; use global::nostr_client;
use gpui::{ use gpui::{
@@ -7,41 +7,35 @@ use gpui::{
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use identity::Identity;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::Registry; use registry::Registry;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonRounded, ButtonVariants}; use ui::button::{Button, ButtonRounded, ButtonVariants};
use ui::{h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt}; use ui::{h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt};
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<Screening> { pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<Screening> {
Screening::new(public_key, window, cx) cx.new(|cx| Screening::new(public_key, window, cx))
} }
pub struct Screening { pub struct Screening {
public_key: PublicKey, profile: Profile,
verified: bool, verified: bool,
followed: bool, followed: bool,
dm_relays: bool, dm_relays: bool,
mutual_contacts: usize, mutual_contacts: usize,
_tasks: SmallVec<[Task<()>; 1]>,
} }
impl Screening { impl Screening {
pub fn new(public_key: PublicKey, _window: &mut Window, cx: &mut App) -> Entity<Self> { pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
cx.new(|_| Self { let registry = Registry::read_global(cx);
public_key, let identity = registry.identity(cx).public_key();
verified: false, let profile = registry.get_person(&public_key, cx);
followed: false,
dm_relays: false,
mutual_contacts: 0,
})
}
pub fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) { let mut tasks = smallvec![];
let identity = Identity::read_global(cx).public_key();
let public_key = self.public_key;
let check_trust_score: Task<(bool, usize, bool)> = cx.background_spawn(async move { let check_trust_score: Task<(bool, usize, bool)> = cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
@@ -69,7 +63,7 @@ impl Screening {
(is_follow, mutual_contacts, dm_relays) (is_follow, mutual_contacts, dm_relays)
}); });
let verify_nip05 = if let Some(address) = self.address(cx) { let verify_nip05 = if let Some(address) = profile.metadata().nip05 {
Some(Tokio::spawn(cx, async move { Some(Tokio::spawn(cx, async move {
nip05_verify(public_key, &address).await.unwrap_or(false) nip05_verify(public_key, &address).await.unwrap_or(false)
})) }))
@@ -77,6 +71,8 @@ impl Screening {
None None
}; };
tasks.push(
// Load all necessary data
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let (followed, mutual_contacts, dm_relays) = check_trust_score.await; let (followed, mutual_contacts, dm_relays) = check_trust_score.await;
@@ -98,26 +94,31 @@ impl Screening {
.ok(); .ok();
} }
} }
}) }),
.detach(); );
Self {
profile,
verified: false,
followed: false,
dm_relays: false,
mutual_contacts: 0,
_tasks: tasks,
}
} }
fn profile(&self, cx: &Context<Self>) -> Profile { fn address(&self, _cx: &Context<Self>) -> Option<String> {
let registry = Registry::read_global(cx); self.profile.metadata().nip05
registry.get_person(&self.public_key, cx)
}
fn address(&self, cx: &Context<Self>) -> Option<String> {
self.profile(cx).metadata().nip05
} }
fn open_njump(&mut self, _window: &mut Window, cx: &mut App) { fn open_njump(&mut self, _window: &mut Window, cx: &mut App) {
let Ok(bech32) = self.public_key.to_bech32(); let Ok(bech32) = self.profile.public_key().to_bech32();
cx.open_url(&format!("https://njump.me/{bech32}")); cx.open_url(&format!("https://njump.me/{bech32}"));
} }
fn report(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn report(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let public_key = self.public_key; let public_key = self.profile.public_key();
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
let builder = EventBuilder::report( let builder = EventBuilder::report(
@@ -145,8 +146,7 @@ impl Screening {
impl Render for Screening { impl Render for Screening {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let proxy = AppSettings::get_proxy_user_avatars(cx); let proxy = AppSettings::get_proxy_user_avatars(cx);
let profile = self.profile(cx); let shorten_pubkey = shorten_pubkey(self.profile.public_key(), 8);
let shorten_pubkey = shorten_pubkey(profile.public_key(), 8);
v_flex() v_flex()
.gap_4() .gap_4()
@@ -156,12 +156,12 @@ impl Render for Screening {
.items_center() .items_center()
.justify_center() .justify_center()
.text_center() .text_center()
.child(Avatar::new(profile.avatar_url(proxy)).size(rems(4.))) .child(Avatar::new(self.profile.avatar_url(proxy)).size(rems(4.)))
.child( .child(
div() div()
.font_semibold() .font_semibold()
.line_height(relative(1.25)) .line_height(relative(1.25))
.child(profile.display_name()), .child(self.profile.display_name()),
), ),
) )
.child( .child(

View File

@@ -10,7 +10,9 @@ use gpui::{
TextAlign, UniformList, Window, TextAlign, UniformList, Window,
}; };
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::Registry;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonRounded, ButtonVariants}; use ui::button::{Button, ButtonRounded, ButtonVariants};
@@ -18,21 +20,23 @@ use ui::input::{InputEvent, InputState, TextInput};
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::{h_flex, v_flex, ContextModal, IconName, Sizable, StyledExt}; use ui::{h_flex, v_flex, ContextModal, IconName, Sizable, StyledExt};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<MessagingRelays> { pub fn init(kind: Kind, window: &mut Window, cx: &mut App) -> Entity<SetupRelay> {
cx.new(|cx| MessagingRelays::new(window, cx)) cx.new(|cx| SetupRelay::new(kind, window, cx))
} }
pub fn relay_button() -> impl IntoElement { pub fn setup_nip17_relay<T>(label: T) -> impl IntoElement
where
T: Into<SharedString>,
{
div().child( div().child(
Button::new("dm-relays") Button::new("setup-relays")
.icon(IconName::Info) .icon(IconName::Info)
.label(t!("relays.button_label")) .label(label)
.warning() .warning()
.xsmall() .xsmall()
.rounded(ButtonRounded::Full) .rounded(ButtonRounded::Full)
.on_click(move |_, window, cx| { .on_click(move |_, window, cx| {
let title = SharedString::new(t!("relays.modal_title")); let view = cx.new(|cx| SetupRelay::new(Kind::InboxRelays, window, cx));
let view = cx.new(|cx| MessagingRelays::new(window, cx));
let weak_view = view.downgrade(); let weak_view = view.downgrade();
window.open_modal(cx, move |modal, _window, _cx| { window.open_modal(cx, move |modal, _window, _cx| {
@@ -40,7 +44,7 @@ pub fn relay_button() -> impl IntoElement {
modal modal
.confirm() .confirm()
.title(title.clone()) .title(shared_t!("relays.modal_title"))
.child(view.clone()) .child(view.clone())
.button_props(ModalButtonProps::default().ok_text(t!("common.update"))) .button_props(ModalButtonProps::default().ok_text(t!("common.update")))
.on_ok(move |_, window, cx| { .on_ok(move |_, window, cx| {
@@ -57,60 +61,41 @@ pub fn relay_button() -> impl IntoElement {
) )
} }
pub struct MessagingRelays { pub struct SetupRelay {
input: Entity<InputState>, input: Entity<InputState>,
relays: Vec<RelayUrl>, relays: Vec<RelayUrl>,
error: Option<SharedString>, error: Option<SharedString>,
#[allow(dead_code)] _subscriptions: SmallVec<[Subscription; 1]>,
subscriptions: SmallVec<[Subscription; 2]>, _tasks: SmallVec<[Task<()>; 1]>,
} }
impl MessagingRelays { impl SetupRelay {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(kind: Kind, window: &mut Window, cx: &mut Context<Self>) -> Self {
let identity = Registry::read_global(cx).identity(cx).public_key();
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com")); let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
subscriptions.push(cx.observe_new::<Self>(move |this, window, cx| { let load_relay = cx.background_spawn(async move {
if let Some(window) = window {
this.load(window, cx);
}
}));
subscriptions.push(cx.subscribe_in(
&input,
window,
move |this: &mut Self, _, event, window, cx| {
if let InputEvent::PressEnter { .. } = event {
this.add(window, cx);
}
},
));
Self {
input,
subscriptions,
relays: vec![],
error: None,
}
}
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
let signer = client.signer().await?; let filter = Filter::new().kind(kind).author(identity).limit(1);
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first() { if let Some(event) = client.database().query(filter).await?.first() {
let relays = event let relays = event
.tags .tags
.filter(TagKind::Relay) .iter()
.filter_map(|tag| RelayUrl::parse(tag.content()?).ok()) .filter_map(|tag| tag.as_standardized())
.collect::<Vec<_>>(); .filter_map(|tag| {
if let TagStandard::RelayMetadata { relay_url, .. } = tag {
Some(relay_url.to_owned())
} else if let TagStandard::Relay(url) = tag {
Some(url.to_owned())
} else {
None
}
})
.collect_vec();
Ok(relays) Ok(relays)
} else { } else {
@@ -118,16 +103,39 @@ impl MessagingRelays {
} }
}); });
tasks.push(
// Load user's relays in the local database
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
if let Ok(relays) = task.await { if let Ok(relays) = load_relay.await {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.relays = relays; this.relays = relays;
cx.notify(); cx.notify();
}) })
.ok(); .ok();
} }
}) }),
.detach(); );
subscriptions.push(
// Subscribe to user's input events
cx.subscribe_in(
&input,
window,
move |this: &mut Self, _, event, window, cx| {
if let InputEvent::PressEnter { .. } = event {
this.add(window, cx);
}
},
),
);
Self {
input,
relays: vec![],
error: None,
_subscriptions: subscriptions,
_tasks: tasks,
}
} }
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -283,11 +291,11 @@ impl MessagingRelays {
.justify_center() .justify_center()
.text_sm() .text_sm()
.text_align(TextAlign::Center) .text_align(TextAlign::Center)
.child(SharedString::new(t!("relays.add_some_relays"))) .child(shared_t!("relays.add_some_relays"))
} }
} }
impl Render for MessagingRelays { impl Render for SetupRelay {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex() v_flex()
.gap_3() .gap_3()

View File

@@ -2,8 +2,8 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, rems, App, ClickEvent, Div, InteractiveElement, IntoElement, ParentElement as _, div, rems, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce,
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, SharedString, StatefulInteractiveElement, Styled, Window,
}; };
use i18n::t; use i18n::t;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
@@ -23,7 +23,6 @@ use crate::views::screening;
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct RoomListItem { pub struct RoomListItem {
ix: usize, ix: usize,
base: Div,
room_id: Option<u64>, room_id: Option<u64>,
public_key: Option<PublicKey>, public_key: Option<PublicKey>,
name: Option<SharedString>, name: Option<SharedString>,
@@ -38,7 +37,6 @@ impl RoomListItem {
pub fn new(ix: usize) -> Self { pub fn new(ix: usize) -> Self {
Self { Self {
ix, ix,
base: h_flex().h_9().w_full().px_1p5().gap_2(),
room_id: None, room_id: None,
public_key: None, public_key: None,
name: None, name: None,
@@ -59,18 +57,18 @@ impl RoomListItem {
self self
} }
pub fn name(mut self, name: SharedString) -> Self { pub fn name(mut self, name: impl Into<SharedString>) -> Self {
self.name = Some(name); self.name = Some(name.into());
self self
} }
pub fn avatar(mut self, avatar: SharedString) -> Self { pub fn avatar(mut self, avatar: impl Into<SharedString>) -> Self {
self.avatar = Some(avatar); self.avatar = Some(avatar.into());
self self
} }
pub fn created_at(mut self, created_at: SharedString) -> Self { pub fn created_at(mut self, created_at: impl Into<SharedString>) -> Self {
self.created_at = Some(created_at); self.created_at = Some(created_at.into());
self self
} }
@@ -111,9 +109,12 @@ impl RenderOnce for RoomListItem {
self.handler, self.handler,
) )
else { else {
return self return h_flex()
.base
.id(self.ix) .id(self.ix)
.h_9()
.w_full()
.px_1p5()
.gap_2()
.child(Skeleton::new().flex_shrink_0().size_6().rounded_full()) .child(Skeleton::new().flex_shrink_0().size_6().rounded_full())
.child( .child(
div() div()
@@ -125,8 +126,12 @@ impl RenderOnce for RoomListItem {
); );
}; };
self.base h_flex()
.id(self.ix) .id(self.ix)
.h_9()
.w_full()
.px_1p5()
.gap_2()
.text_sm() .text_sm()
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.when(!hide_avatar, |this| { .when(!hide_avatar, |this| {

View File

@@ -4,7 +4,7 @@ use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use common::debounced_delay::DebouncedDelay; use common::debounced_delay::DebouncedDelay;
use common::display::TextUtils; use common::display::{ReadableTimestamp, TextUtils};
use global::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS}; use global::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use global::nostr_client; use global::nostr_client;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
@@ -15,12 +15,11 @@ use gpui::{
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::t; use i18n::t;
use identity::Identity;
use itertools::Itertools; use itertools::Itertools;
use list_item::RoomListItem; use list_item::RoomListItem;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::room::{Room, RoomKind}; use registry::room::{Room, RoomKind};
use registry::{Registry, RegistrySignal}; use registry::{Registry, RegistryEvent};
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -82,7 +81,7 @@ impl Sidebar {
&chats, &chats,
window, window,
move |this, _chats, event, _window, cx| { move |this, _chats, event, _window, cx| {
if let RegistrySignal::NewRequest(kind) = event { if let RegistryEvent::NewRequest(kind) = event {
this.indicator.update(cx, |this, cx| { this.indicator.update(cx, |this, cx| {
*this = Some(kind.to_owned()); *this = Some(kind.to_owned());
cx.notify(); cx.notify();
@@ -211,7 +210,7 @@ impl Sidebar {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let identity = Identity::read_global(cx).public_key(); let identity = Registry::read_global(cx).identity(cx).public_key();
let query = query.to_owned(); let query = query.to_owned();
let query_cloned = query.clone(); let query_cloned = query.clone();
@@ -271,7 +270,7 @@ impl Sidebar {
} }
fn search_by_nip05(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) { fn search_by_nip05(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
let identity = Identity::read_global(cx).public_key(); let identity = Registry::read_global(cx).identity(cx).public_key();
let address = query.to_owned(); let address = query.to_owned();
let task = Tokio::spawn(cx, async move { let task = Tokio::spawn(cx, async move {
@@ -325,7 +324,7 @@ impl Sidebar {
return; return;
}; };
let identity = Identity::read_global(cx).public_key(); let identity = Registry::read_global(cx).identity(cx).public_key();
let task: Task<Result<Room, Error>> = cx.background_spawn(async move { let task: Task<Result<Room, Error>> = cx.background_spawn(async move {
// Create a gift wrap event to represent as room // Create a gift wrap event to represent as room
Self::create_temp_room(identity, public_key).await Self::create_temp_room(identity, public_key).await
@@ -553,7 +552,7 @@ impl Sidebar {
.room_id(room_id) .room_id(room_id)
.name(this.display_name(cx)) .name(this.display_name(cx))
.avatar(this.display_image(proxy, cx)) .avatar(this.display_image(proxy, cx))
.created_at(this.ago()) .created_at(this.created_at.to_ago())
.public_key(this.members[0]) .public_key(this.members[0])
.kind(this.kind) .kind(this.kind)
.on_click(handler), .on_click(handler),

View File

@@ -1,6 +1,6 @@
use std::time::Duration; use std::time::Duration;
use common::display::DisplayProfile; use common::display::ReadableProfile;
use common::nip05::nip05_verify; use common::nip05::nip05_verify;
use global::nostr_client; use global::nostr_client;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
@@ -9,40 +9,35 @@ use gpui::{
ParentElement, Render, SharedString, Styled, Task, Window, ParentElement, Render, SharedString, Styled, Task, Window,
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::t; use i18n::{shared_t, t};
use identity::Identity;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::Registry; use registry::Registry;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, StyledExt}; use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, StyledExt};
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<UserProfile> { pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<UserProfile> {
UserProfile::new(public_key, window, cx) cx.new(|cx| UserProfile::new(public_key, window, cx))
} }
pub struct UserProfile { pub struct UserProfile {
public_key: PublicKey, profile: Profile,
followed: bool, followed: bool,
verified: bool, verified: bool,
copied: bool, copied: bool,
_tasks: SmallVec<[Task<()>; 1]>,
} }
impl UserProfile { impl UserProfile {
pub fn new(public_key: PublicKey, _window: &mut Window, cx: &mut App) -> Entity<Self> { pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
cx.new(|_| Self { let registry = Registry::read_global(cx);
public_key, let identity = registry.identity(cx).public_key();
followed: false, let profile = registry.get_person(&public_key, cx);
verified: false,
copied: false,
})
}
pub fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) { let mut tasks = smallvec![];
let identity = Identity::read_global(cx).public_key();
let public_key = self.public_key;
let check_follow: Task<bool> = cx.background_spawn(async move { let check_follow: Task<bool> = cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
@@ -55,7 +50,7 @@ impl UserProfile {
client.database().count(filter).await.unwrap_or(0) >= 1 client.database().count(filter).await.unwrap_or(0) >= 1
}); });
let verify_nip05 = if let Some(address) = self.address(cx) { let verify_nip05 = if let Some(address) = profile.metadata().nip05 {
Some(Tokio::spawn(cx, async move { Some(Tokio::spawn(cx, async move {
nip05_verify(public_key, &address).await.unwrap_or(false) nip05_verify(public_key, &address).await.unwrap_or(false)
})) }))
@@ -63,6 +58,8 @@ impl UserProfile {
None None
}; };
tasks.push(
// Load user profile data
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let followed = check_follow.await; let followed = check_follow.await;
@@ -83,21 +80,24 @@ impl UserProfile {
.ok(); .ok();
} }
} }
}) }),
.detach(); );
Self {
profile,
followed: false,
verified: false,
copied: false,
_tasks: tasks,
}
} }
fn profile(&self, cx: &Context<Self>) -> Profile { fn address(&self, _cx: &Context<Self>) -> Option<String> {
let registry = Registry::read_global(cx); self.profile.metadata().nip05
registry.get_person(&self.public_key, cx)
}
fn address(&self, cx: &Context<Self>) -> Option<String> {
self.profile(cx).metadata().nip05
} }
fn copy_pubkey(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn copy_pubkey(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Ok(bech32) = self.public_key.to_bech32(); let Ok(bech32) = self.profile.public_key().to_bech32();
let item = ClipboardItem::new_string(bech32); let item = ClipboardItem::new_string(bech32);
cx.write_to_clipboard(item); cx.write_to_clipboard(item);
@@ -128,9 +128,8 @@ impl UserProfile {
impl Render for UserProfile { impl Render for UserProfile {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let proxy = AppSettings::get_proxy_user_avatars(cx); let proxy = AppSettings::get_proxy_user_avatars(cx);
let profile = self.profile(cx);
let Ok(bech32) = profile.public_key().to_bech32(); let Ok(bech32) = self.profile.public_key().to_bech32();
let shared_bech32 = SharedString::new(bech32); let shared_bech32 = SharedString::new(bech32);
v_flex() v_flex()
@@ -141,14 +140,14 @@ impl Render for UserProfile {
.items_center() .items_center()
.justify_center() .justify_center()
.text_center() .text_center()
.child(Avatar::new(profile.avatar_url(proxy)).size(rems(4.))) .child(Avatar::new(self.profile.avatar_url(proxy)).size(rems(4.)))
.child( .child(
v_flex() v_flex()
.child( .child(
div() div()
.font_semibold() .font_semibold()
.line_height(relative(1.25)) .line_height(relative(1.25))
.child(profile.display_name()), .child(self.profile.display_name()),
) )
.when_some(self.address(cx), |this, address| { .when_some(self.address(cx), |this, address| {
this.child( this.child(
@@ -183,7 +182,7 @@ impl Render for UserProfile {
.bg(cx.theme().elevated_surface_background) .bg(cx.theme().elevated_surface_background)
.text_xs() .text_xs()
.font_semibold() .font_semibold()
.child(SharedString::new(t!("profile.unknown"))), .child(shared_t!("profile.unknown")),
) )
}), }),
) )
@@ -235,7 +234,7 @@ impl Render for UserProfile {
.child( .child(
div() div()
.text_color(cx.theme().text_muted) .text_color(cx.theme().text_muted)
.child(SharedString::new(t!("profile.label_bio"))), .child(shared_t!("profile.label_bio")),
) )
.child( .child(
div() div()
@@ -243,7 +242,7 @@ impl Render for UserProfile {
.rounded_md() .rounded_md()
.bg(cx.theme().elevated_surface_background) .bg(cx.theme().elevated_surface_background)
.child( .child(
profile self.profile
.metadata() .metadata()
.about .about
.unwrap_or(t!("profile.no_bio").to_string()), .unwrap_or(t!("profile.no_bio").to_string()),

View File

@@ -8,15 +8,16 @@ pub const ACCOUNT_IDENTIFIER: &str = "coop:user";
pub const SETTINGS_D: &str = "coop:settings"; pub const SETTINGS_D: &str = "coop:settings";
/// Bootstrap Relays. /// Bootstrap Relays.
pub const BOOTSTRAP_RELAYS: [&str; 4] = [ pub const BOOTSTRAP_RELAYS: [&str; 5] = [
"wss://relay.damus.io", "wss://relay.damus.io",
"wss://relay.primal.net", "wss://relay.primal.net",
"wss://relay.nos.social",
"wss://user.kindpag.es", "wss://user.kindpag.es",
"wss://purplepag.es", "wss://purplepag.es",
]; ];
/// Search Relays. /// Search Relays.
pub const SEARCH_RELAYS: [&str; 2] = ["wss://search.nos.today", "wss://relay.nostr.band"]; pub const SEARCH_RELAYS: [&str; 1] = ["wss://relay.nostr.band"];
/// NIP65 Relays. Used for new account /// NIP65 Relays. Used for new account
pub const NIP65_RELAYS: [&str; 4] = [ pub const NIP65_RELAYS: [&str; 4] = [
@@ -27,14 +28,20 @@ pub const NIP65_RELAYS: [&str; 4] = [
]; ];
/// Messaging Relays. Used for new account /// Messaging Relays. Used for new account
pub const NIP17_RELAYS: [&str; 2] = ["wss://nip17.com", "wss://relay.0xchat.com"]; pub const NIP17_RELAYS: [&str; 2] = ["wss://nip17.com", "wss://auth.nostr1.com"];
/// Default relay for Nostr Connect /// Default relay for Nostr Connect
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app"; pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
/// Default retry count for fetching NIP-17 relays
pub const TOTAL_RETRY: u64 = 2;
/// Default timeout (in seconds) for Nostr Connect /// Default timeout (in seconds) for Nostr Connect
pub const NOSTR_CONNECT_TIMEOUT: u64 = 200; pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
/// Default timeout (in seconds) for Nostr Connect (Bunker)
pub const BUNKER_TIMEOUT: u64 = 30;
/// Total metadata requests will be grouped. /// Total metadata requests will be grouped.
pub const METADATA_BATCH_LIMIT: usize = 100; pub const METADATA_BATCH_LIMIT: usize = 100;
@@ -44,9 +51,6 @@ pub const METADATA_BATCH_TIMEOUT: u64 = 300;
/// Maximum timeout for waiting for finish (seconds) /// Maximum timeout for waiting for finish (seconds)
pub const WAIT_FOR_FINISH: u64 = 60; pub const WAIT_FOR_FINISH: u64 = 60;
/// Default width for all modals.
pub const DEFAULT_MODAL_WIDTH: f32 = 420.;
/// Default width of the sidebar. /// Default width of the sidebar.
pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.; pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.;

View File

@@ -1,4 +1,3 @@
use std::collections::BTreeSet;
use std::sync::OnceLock; use std::sync::OnceLock;
use std::time::Duration; use std::time::Duration;
@@ -13,41 +12,109 @@ use crate::paths::support_dir;
pub mod constants; pub mod constants;
pub mod paths; pub mod paths;
/// Signals sent through the global event channel to notify UI components #[derive(Debug, Clone)]
pub struct AuthReq {
pub challenge: String,
pub url: RelayUrl,
}
impl AuthReq {
pub fn new(challenge: impl Into<String>, url: RelayUrl) -> Self {
Self {
challenge: challenge.into(),
url,
}
}
}
#[derive(Debug, Clone)]
pub enum Notice {
RelayFailed(RelayUrl),
AuthFailed(RelayUrl),
Custom(String),
}
impl Notice {
pub fn as_str(&self) -> String {
match self {
Notice::AuthFailed(url) => format!("Authenticate failed for relay {url}"),
Notice::RelayFailed(url) => format!("Failed to connect the relay {url}"),
Notice::Custom(msg) => msg.into(),
}
}
}
/// Signals sent through the global event channel to notify UI
#[derive(Debug)] #[derive(Debug)]
pub enum NostrSignal { pub enum IngesterSignal {
/// Signer has been set /// A signal to notify UI that the client's signer has been set
SignerSet(PublicKey), SignerSet(PublicKey),
/// Signer has been unset /// A signal to notify UI that the client's signer has been unset
SignerUnset, SignerUnset,
/// Browser Signer Proxy service is not running /// A signal to notify UI that the relay requires authentication
Auth(AuthReq),
/// A signal to notify UI that the browser proxy service is down
ProxyDown, ProxyDown,
/// Received a new metadata event from Relay Pool /// A signal to notify UI that a new metadata event has been received
Metadata(Event), Metadata(Event),
/// Received a new gift wrap event from Relay Pool /// A signal to notify UI that a new gift wrap event has been received
GiftWrap(Event), GiftWrap((EventId, Event)),
/// Finished processing all gift wrap events /// A signal to notify UI that all gift wrap events have been processed
Finish, Finish,
/// Partially finished processing all gift wrap events /// A signal to notify UI that partial processing of gift wrap events has been completed
PartialFinish, PartialFinish,
/// DM relays have been found /// A signal to notify UI that no DM relay for current user was found
DmRelaysFound, DmRelayNotFound,
/// Notice from Relay Pool /// A signal to notify UI that there are errors or notices occurred
Notice(String), Notice(Notice),
}
#[derive(Debug)]
pub struct Ingester {
rx: Receiver<IngesterSignal>,
tx: Sender<IngesterSignal>,
}
impl Default for Ingester {
fn default() -> Self {
Self::new()
}
}
impl Ingester {
pub fn new() -> Self {
let (tx, rx) = smol::channel::bounded::<IngesterSignal>(2048);
Self { rx, tx }
}
pub fn signals(&self) -> &Receiver<IngesterSignal> {
&self.rx
}
pub async fn send(&self, signal: IngesterSignal) {
if let Err(e) = self.tx.send(signal).await {
log::error!("Failed to send signal: {e}");
}
}
} }
static NOSTR_CLIENT: OnceLock<Client> = OnceLock::new(); static NOSTR_CLIENT: OnceLock<Client> = OnceLock::new();
static GLOBAL_CHANNEL: OnceLock<(Sender<NostrSignal>, Receiver<NostrSignal>)> = OnceLock::new();
static PROCESSED_EVENTS: OnceLock<RwLock<BTreeSet<EventId>>> = OnceLock::new(); static INGESTER: OnceLock<Ingester> = OnceLock::new();
static SENT_IDS: OnceLock<RwLock<Vec<EventId>>> = OnceLock::new();
static CURRENT_TIMESTAMP: OnceLock<Timestamp> = OnceLock::new(); static CURRENT_TIMESTAMP: OnceLock<Timestamp> = OnceLock::new();
static FIRST_RUN: OnceLock<bool> = OnceLock::new(); static FIRST_RUN: OnceLock<bool> = OnceLock::new();
pub fn nostr_client() -> &'static Client { pub fn nostr_client() -> &'static Client {
@@ -63,7 +130,7 @@ pub fn nostr_client() -> &'static Client {
let opts = ClientOptions::new() let opts = ClientOptions::new()
.gossip(true) .gossip(true)
.automatic_authentication(true) .automatic_authentication(false)
.verify_subscriptions(false) .verify_subscriptions(false)
// Sleep after idle for 30 seconds // Sleep after idle for 30 seconds
.sleep_when_idle(SleepWhenIdle::Enabled { .sleep_when_idle(SleepWhenIdle::Enabled {
@@ -74,21 +141,18 @@ pub fn nostr_client() -> &'static Client {
}) })
} }
pub fn global_channel() -> &'static (Sender<NostrSignal>, Receiver<NostrSignal>) { pub fn ingester() -> &'static Ingester {
GLOBAL_CHANNEL.get_or_init(|| { INGESTER.get_or_init(Ingester::new)
let (sender, receiver) = smol::channel::bounded::<NostrSignal>(2048);
(sender, receiver)
})
}
pub fn processed_events() -> &'static RwLock<BTreeSet<EventId>> {
PROCESSED_EVENTS.get_or_init(|| RwLock::new(BTreeSet::new()))
} }
pub fn starting_time() -> &'static Timestamp { pub fn starting_time() -> &'static Timestamp {
CURRENT_TIMESTAMP.get_or_init(Timestamp::now) CURRENT_TIMESTAMP.get_or_init(Timestamp::now)
} }
pub fn sent_ids() -> &'static RwLock<Vec<EventId>> {
SENT_IDS.get_or_init(|| RwLock::new(Vec::new()))
}
pub fn first_run() -> &'static bool { pub fn first_run() -> &'static bool {
FIRST_RUN.get_or_init(|| { FIRST_RUN.get_or_init(|| {
let flag = support_dir().join(format!(".{}-first_run", env!("CARGO_PKG_VERSION"))); let flag = support_dir().join(format!(".{}-first_run", env!("CARGO_PKG_VERSION")));

View File

@@ -1,22 +0,0 @@
[package]
name = "identity"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
ui = { path = "../ui" }
global = { path = "../global" }
common = { path = "../common" }
client_keys = { path = "../client_keys" }
settings = { path = "../settings" }
signer_proxy = { path = "../signer_proxy" }
nostr-sdk.workspace = true
nostr-connect.workspace = true
smol.workspace = true
gpui.workspace = true
anyhow.workspace = true
log.workspace = true
smallvec.workspace = true
webbrowser.workspace = true

View File

@@ -1,122 +0,0 @@
use std::time::Duration;
use global::constants::ACCOUNT_IDENTIFIER;
use global::{global_channel, nostr_client, NostrSignal};
use gpui::{App, AppContext, Context, Entity, Global, Window};
use nostr_connect::prelude::*;
use signer_proxy::{BrowserSignerProxy, BrowserSignerProxyOptions};
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) {
Identity::set_global(cx.new(|cx| Identity::new(public_key, window, cx)), cx);
}
struct GlobalIdentity(Entity<Identity>);
impl Global for GlobalIdentity {}
pub struct Identity {
public_key: PublicKey,
nip17_relays: Option<bool>,
nip65_relays: Option<bool>,
}
impl Identity {
/// Retrieve the Global Identity instance
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalIdentity>().0.clone()
}
/// Retrieve the Identity instance
pub fn read_global(cx: &App) -> &Self {
cx.global::<GlobalIdentity>().0.read(cx)
}
/// Check if the Global Identity instance has been set
pub fn has_global(cx: &App) -> bool {
cx.has_global::<GlobalIdentity>()
}
/// Remove the Global Identity instance
pub fn remove_global(cx: &mut App) {
cx.remove_global::<GlobalIdentity>();
}
/// Set the Global Identity instance
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalIdentity(state));
}
pub(crate) fn new(
public_key: PublicKey,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Self {
Self {
public_key,
nip17_relays: None,
nip65_relays: None,
}
}
/// Returns the current identity's public key
pub fn public_key(&self) -> PublicKey {
self.public_key
}
/// Returns the current identity's NIP-17 relays status
pub fn nip17_relays(&self) -> Option<bool> {
self.nip17_relays
}
/// Returns the current identity's NIP-65 relays status
pub fn nip65_relays(&self) -> Option<bool> {
self.nip65_relays
}
/// Starts the browser proxy for nostr signer
pub fn start_browser_proxy(cx: &App) {
let proxy = BrowserSignerProxy::new(BrowserSignerProxyOptions::default());
let url = proxy.url();
cx.background_spawn(async move {
let client = nostr_client();
let channel = global_channel();
if proxy.start().await.is_ok() {
webbrowser::open(&url).ok();
loop {
if proxy.is_session_active() {
// Save the signer to disk for further logins
if let Ok(public_key) = proxy.get_public_key().await {
let keys = Keys::generate();
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
let kind = Kind::ApplicationSpecificData;
let builder = EventBuilder::new(kind, "extension")
.tags(tags)
.build(public_key)
.sign(&keys)
.await;
if let Ok(event) = builder {
if let Err(e) = client.database().save_event(&event).await {
log::error!("Failed to save event: {e}");
};
}
}
// Set the client's signer with current proxy signer
client.set_signer(proxy.clone()).await;
break;
} else {
channel.0.send(NostrSignal::ProxyDown).await.ok();
}
smol::Timer::after(Duration::from_secs(1)).await;
}
}
})
.detach();
}
}

View File

@@ -14,9 +14,9 @@ nostr.workspace = true
nostr-sdk.workspace = true nostr-sdk.workspace = true
anyhow.workspace = true anyhow.workspace = true
itertools.workspace = true itertools.workspace = true
chrono.workspace = true
smallvec.workspace = true smallvec.workspace = true
smol.workspace = true smol.workspace = true
log.workspace = true log.workspace = true
fuzzy-matcher = "0.3.7" fuzzy-matcher = "0.3.7"
hashbrown = "0.15"

View File

@@ -1,14 +1,12 @@
use std::cmp::Reverse; use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use anyhow::Error; use anyhow::Error;
use common::event::EventUtils; use common::event::EventUtils;
use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use global::nostr_client; use global::nostr_client;
use gpui::{ use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window};
App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window, use hashbrown::{HashMap, HashSet};
};
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use room::RoomKind; use room::RoomKind;
@@ -29,7 +27,7 @@ struct GlobalRegistry(Entity<Registry>);
impl Global for GlobalRegistry {} impl Global for GlobalRegistry {}
#[derive(Debug)] #[derive(Debug)]
pub enum RegistrySignal { pub enum RegistryEvent {
Open(WeakEntity<Room>), Open(WeakEntity<Room>),
Close(u64), Close(u64),
NewRequest(RoomKind), NewRequest(RoomKind),
@@ -41,19 +39,21 @@ pub struct Registry {
pub rooms: Vec<Entity<Room>>, pub rooms: Vec<Entity<Room>>,
/// Collection of all persons (user profiles) /// Collection of all persons (user profiles)
pub persons: BTreeMap<PublicKey, Entity<Profile>>, pub persons: HashMap<PublicKey, Entity<Profile>>,
/// Indicates if rooms are currently being loaded /// Indicates if rooms are currently being loaded
/// ///
/// Always equal to `true` when the app starts /// Always equal to `true` when the app starts
pub loading: bool, pub loading: bool,
/// Subscriptions for observing changes /// Public Key of the current user
#[allow(dead_code)] pub identity: Option<PublicKey>,
subscriptions: SmallVec<[Subscription; 2]>,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 1]>,
} }
impl EventEmitter<RegistrySignal> for Registry {} impl EventEmitter<RegistryEvent> for Registry {}
impl Registry { impl Registry {
/// Retrieve the Global Registry state /// Retrieve the Global Registry state
@@ -73,62 +73,13 @@ impl Registry {
/// Create a new Registry instance /// Create a new Registry instance
pub(crate) fn new(cx: &mut Context<Self>) -> Self { pub(crate) fn new(cx: &mut Context<Self>) -> Self {
let mut subscriptions = smallvec![]; let mut tasks = smallvec![];
// Load all user profiles from the database when the Registry is created let load_local_persons: Task<Result<Vec<Profile>, Error>> =
subscriptions.push(cx.observe_new::<Self>(|this, _window, cx| {
let task = this.load_local_person(cx);
this.set_persons_from_task(task, cx);
}));
// When any Room is created, load members metadata
subscriptions.push(cx.observe_new::<Room>(|this, _window, cx| {
let state = Self::global(cx);
let task = this.load_metadata(cx);
state.update(cx, |this, cx| {
this.set_persons_from_task(task, cx);
});
}));
Self {
rooms: vec![],
persons: BTreeMap::new(),
loading: true,
subscriptions,
}
}
pub fn reset(&mut self, cx: &mut Context<Self>) {
self.rooms = vec![];
self.loading = true;
cx.notify();
}
pub(crate) fn set_persons_from_task(
&mut self,
task: Task<Result<Vec<Profile>, Error>>,
cx: &mut Context<Self>,
) {
cx.spawn(async move |this, cx| {
if let Ok(profiles) = task.await {
this.update(cx, |this, cx| {
for profile in profiles {
this.persons
.insert(profile.public_key(), cx.new(|_| profile));
}
cx.notify();
})
.ok();
}
})
.detach();
}
pub(crate) fn load_local_person(&self, cx: &App) -> Task<Result<Vec<Profile>, Error>> {
cx.background_spawn(async move { cx.background_spawn(async move {
let filter = Filter::new().kind(Kind::Metadata).limit(100); let client = nostr_client();
let events = nostr_client().database().query(filter).await?; let filter = Filter::new().kind(Kind::Metadata).limit(200);
let events = client.database().query(filter).await?;
let mut profiles = vec![]; let mut profiles = vec![];
for event in events.into_iter() { for event in events.into_iter() {
@@ -138,9 +89,52 @@ impl Registry {
} }
Ok(profiles) Ok(profiles)
});
tasks.push(
// Load all user profiles from the database when the Registry is created
cx.spawn(async move |this, cx| {
if let Ok(profiles) = load_local_persons.await {
this.update(cx, |this, cx| {
this.set_persons(profiles, cx);
}) })
.ok();
}
}),
);
Self {
rooms: vec![],
persons: HashMap::new(),
identity: None,
loading: true,
_tasks: tasks,
}
} }
/// Returns the identity of the user.
///
/// WARNING: This method will panic if user is not logged in.
pub fn identity(&self, cx: &App) -> Profile {
self.get_person(&self.identity.unwrap(), cx)
}
/// Sets the identity of the user.
pub fn set_identity(&mut self, identity: PublicKey, cx: &mut Context<Self>) {
self.identity = Some(identity);
cx.notify();
}
/// Insert batch of persons
pub fn set_persons(&mut self, profiles: Vec<Profile>, cx: &mut Context<Self>) {
for profile in profiles.into_iter() {
self.persons
.insert(profile.public_key(), cx.new(|_| profile));
}
cx.notify();
}
/// Get single person
pub fn get_person(&self, public_key: &PublicKey, cx: &App) -> Profile { pub fn get_person(&self, public_key: &PublicKey, cx: &App) -> Profile {
self.persons self.persons
.get(public_key) .get(public_key)
@@ -149,6 +143,7 @@ impl Registry {
.unwrap_or(Profile::new(public_key.to_owned(), Metadata::default())) .unwrap_or(Profile::new(public_key.to_owned(), Metadata::default()))
} }
/// Get group of persons
pub fn get_group_person(&self, public_keys: &[PublicKey], cx: &App) -> Vec<Profile> { pub fn get_group_person(&self, public_keys: &[PublicKey], cx: &App) -> Vec<Profile> {
let mut profiles = vec![]; let mut profiles = vec![];
@@ -160,6 +155,7 @@ impl Registry {
profiles profiles
} }
/// Insert or update a person
pub fn insert_or_update_person(&mut self, event: Event, cx: &mut App) { pub fn insert_or_update_person(&mut self, event: Event, cx: &mut App) {
let public_key = event.pubkey; let public_key = event.pubkey;
let Ok(metadata) = Metadata::from_json(event.content) else { let Ok(metadata) = Metadata::from_json(event.content) else {
@@ -213,7 +209,7 @@ impl Registry {
/// Close a room. /// Close a room.
pub fn close_room(&mut self, id: u64, cx: &mut Context<Self>) { pub fn close_room(&mut self, id: u64, cx: &mut Context<Self>) {
if self.rooms.iter().any(|r| r.read(cx).id == id) { if self.rooms.iter().any(|r| r.read(cx).id == id) {
cx.emit(RegistrySignal::Close(id)); cx.emit(RegistryEvent::Close(id));
} }
} }
@@ -253,6 +249,14 @@ impl Registry {
cx.notify(); cx.notify();
} }
/// Reset the registry.
pub fn reset(&mut self, cx: &mut Context<Self>) {
self.rooms = vec![];
self.loading = true;
self.identity = None;
cx.notify();
}
/// Load all rooms from the database. /// Load all rooms from the database.
pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) {
log::info!("Starting to load chat rooms..."); log::info!("Starting to load chat rooms...");
@@ -260,7 +264,7 @@ impl Registry {
// Get the contact bypass setting // Get the contact bypass setting
let contact_bypass = AppSettings::get_contact_bypass(cx); let contact_bypass = AppSettings::get_contact_bypass(cx);
let task: Task<Result<BTreeSet<Room>, Error>> = cx.background_spawn(async move { let task: Task<Result<HashSet<Room>, Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
@@ -279,7 +283,7 @@ impl Registry {
let recv_events = client.database().query(recv).await?; let recv_events = client.database().query(recv).await?;
let events = send_events.merge(recv_events); let events = send_events.merge(recv_events);
let mut rooms: BTreeSet<Room> = BTreeSet::new(); let mut rooms: HashSet<Room> = HashSet::new();
// Process each event and group by room hash // Process each event and group by room hash
for event in events for event in events
@@ -343,7 +347,7 @@ impl Registry {
.detach(); .detach();
} }
pub(crate) fn extend_rooms(&mut self, rooms: BTreeSet<Room>, cx: &mut Context<Self>) { pub(crate) fn extend_rooms(&mut self, rooms: HashSet<Room>, cx: &mut Context<Self>) {
let mut room_map: HashMap<u64, usize> = HashMap::with_capacity(self.rooms.len()); let mut room_map: HashMap<u64, usize> = HashMap::with_capacity(self.rooms.len());
for (index, room) in self.rooms.iter().enumerate() { for (index, room) in self.rooms.iter().enumerate() {
@@ -380,7 +384,7 @@ impl Registry {
weak_room weak_room
}; };
cx.emit(RegistrySignal::Open(weak_room)); cx.emit(RegistryEvent::Open(weak_room));
} }
/// Refresh messages for a room in the global registry /// Refresh messages for a room in the global registry
@@ -400,7 +404,7 @@ impl Registry {
/// Updates room ordering based on the most recent messages. /// Updates room ordering based on the most recent messages.
pub fn event_to_message( pub fn event_to_message(
&mut self, &mut self,
identity: PublicKey, gift_wrap_id: EventId,
event: Event, event: Event,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@@ -408,6 +412,10 @@ impl Registry {
let id = event.uniq_id(); let id = event.uniq_id();
let author = event.pubkey; let author = event.pubkey;
let Some(identity) = self.identity else {
return;
};
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) { if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
// Update room // Update room
room.update(cx, |this, cx| { room.update(cx, |this, cx| {
@@ -420,7 +428,7 @@ impl Registry {
// Emit the new message to the room // Emit the new message to the room
cx.defer_in(window, move |this, _window, cx| { cx.defer_in(window, move |this, _window, cx| {
this.emit_message(event, cx); this.emit_message(gift_wrap_id, event, cx);
}); });
}); });
@@ -436,7 +444,7 @@ impl Registry {
// Notify the UI about the new room // Notify the UI about the new room
cx.defer_in(window, move |_this, _window, cx| { cx.defer_in(window, move |_this, _window, cx| {
cx.emit(RegistrySignal::NewRequest(RoomKind::default())); cx.emit(RegistryEvent::NewRequest(RoomKind::default()));
}); });
} }
} }

View File

@@ -1,7 +1,5 @@
use std::hash::Hash; use std::hash::Hash;
use chrono::{Local, TimeZone};
use gpui::SharedString;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -10,8 +8,8 @@ pub struct RenderedMessage {
/// Author's public key /// Author's public key
pub author: PublicKey, pub author: PublicKey,
/// The content/text of the message /// The content/text of the message
pub content: SharedString, pub content: String,
/// When the message was created /// Message created time as unix timestamp
pub created_at: Timestamp, pub created_at: Timestamp,
/// List of mentioned public keys in the message /// List of mentioned public keys in the message
pub mentions: Vec<PublicKey>, pub mentions: Vec<PublicKey>,
@@ -27,7 +25,7 @@ impl From<Event> for RenderedMessage {
Self { Self {
id: inner.id, id: inner.id,
author: inner.pubkey, author: inner.pubkey,
content: inner.content.into(), content: inner.content,
created_at: inner.created_at, created_at: inner.created_at,
mentions, mentions,
replies_to, replies_to,
@@ -44,7 +42,7 @@ impl From<UnsignedEvent> for RenderedMessage {
// Event ID must be known // Event ID must be known
id: inner.id.unwrap(), id: inner.id.unwrap(),
author: inner.pubkey, author: inner.pubkey,
content: inner.content.into(), content: inner.content,
created_at: inner.created_at, created_at: inner.created_at,
mentions, mentions,
replies_to, replies_to,
@@ -90,30 +88,6 @@ impl Hash for RenderedMessage {
} }
} }
impl RenderedMessage {
/// Returns a human-readable string representing how long ago the message was created
pub fn ago(&self) -> SharedString {
let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
chrono::LocalResult::Single(time) => time,
_ => return "Invalid timestamp".into(),
};
let now = Local::now();
let input_date = input_time.date_naive();
let now_date = now.date_naive();
let yesterday_date = (now - chrono::Duration::days(1)).date_naive();
let time_format = input_time.format("%H:%M %p");
match input_date {
date if date == now_date => format!("Today at {time_format}"),
date if date == yesterday_date => format!("Yesterday at {time_format}"),
_ => format!("{}, {time_format}", input_time.format("%d/%m/%y")),
}
.into()
}
}
fn extract_mentions(content: &str) -> Vec<PublicKey> { fn extract_mentions(content: &str) -> Vec<PublicKey> {
let parser = NostrParser::new(); let parser = NostrParser::new();
let tokens = parser.parse(content); let tokens = parser.parse(content);

View File

@@ -1,23 +1,16 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use std::hash::{Hash, Hasher};
use anyhow::{anyhow, Error}; use anyhow::Error;
use chrono::{Local, TimeZone}; use common::display::ReadableProfile;
use common::display::DisplayProfile;
use common::event::EventUtils; use common::event::EventUtils;
use global::nostr_client; use global::nostr_client;
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task}; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::SmallVec;
use crate::Registry; use crate::Registry;
pub(crate) const NOW: &str = "now";
pub(crate) const SECONDS_IN_MINUTE: i64 = 60;
pub(crate) const MINUTES_IN_HOUR: i64 = 60;
pub(crate) const HOURS_IN_DAY: i64 = 24;
pub(crate) const DAYS_IN_MONTH: i64 = 30;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SendReport { pub struct SendReport {
pub receiver: PublicKey, pub receiver: PublicKey,
@@ -69,7 +62,7 @@ impl SendReport {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum RoomSignal { pub enum RoomSignal {
NewMessage(Box<Event>), NewMessage((EventId, Box<Event>)),
Refresh, Refresh,
} }
@@ -85,11 +78,11 @@ pub struct Room {
pub id: u64, pub id: u64,
pub created_at: Timestamp, pub created_at: Timestamp,
/// Subject of the room /// Subject of the room
pub subject: Option<SharedString>, pub subject: Option<String>,
/// Picture of the room /// Picture of the room
pub picture: Option<SharedString>, pub picture: Option<String>,
/// All members of the room /// All members of the room
pub members: SmallVec<[PublicKey; 2]>, pub members: Vec<PublicKey>,
/// Kind /// Kind
pub kind: RoomKind, pub kind: RoomKind,
} }
@@ -112,6 +105,12 @@ impl PartialEq for Room {
} }
} }
impl Hash for Room {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl Eq for Room {} impl Eq for Room {}
impl EventEmitter<RoomSignal> for Room {} impl EventEmitter<RoomSignal> for Room {}
@@ -120,21 +119,25 @@ impl Room {
pub fn new(event: &Event) -> Self { pub fn new(event: &Event) -> Self {
let id = event.uniq_id(); let id = event.uniq_id();
let created_at = event.created_at; let created_at = event.created_at;
let public_keys = event.all_pubkeys();
// Convert pubkeys into members // Get the members from the event's tags and event's pubkey
let members = public_keys.into_iter().unique().sorted().collect(); let members = event
.all_pubkeys()
.into_iter()
.unique()
.sorted()
.collect_vec();
// Get the subject from the event's tags // Get the subject from the event's tags
let subject = if let Some(tag) = event.tags.find(TagKind::Subject) { let subject = if let Some(tag) = event.tags.find(TagKind::Subject) {
tag.content().map(|s| s.to_owned().into()) tag.content().map(|s| s.to_owned())
} else { } else {
None None
}; };
// Get the picture from the event's tags // Get the picture from the event's tags
let picture = if let Some(tag) = event.tags.find(TagKind::custom("picture")) { let picture = if let Some(tag) = event.tags.find(TagKind::custom("picture")) {
tag.content().map(|s| s.to_owned().into()) tag.content().map(|s| s.to_owned())
} else { } else {
None None
}; };
@@ -177,11 +180,9 @@ impl Room {
/// ///
/// The modified Room instance with the new member list after rearrangement /// The modified Room instance with the new member list after rearrangement
pub fn rearrange_by(mut self, rearrange_by: PublicKey) -> Self { pub fn rearrange_by(mut self, rearrange_by: PublicKey) -> Self {
let (not_match, matches): (Vec<PublicKey>, Vec<PublicKey>) = self let (not_match, matches): (Vec<PublicKey>, Vec<PublicKey>) =
.members self.members.iter().partition(|&key| key != &rearrange_by);
.into_iter() self.members = not_match;
.partition(|key| key != &rearrange_by);
self.members = not_match.into();
self.members.extend(matches); self.members.extend(matches);
self self
} }
@@ -224,8 +225,8 @@ impl Room {
/// ///
/// * `subject` - The new subject to set /// * `subject` - The new subject to set
/// * `cx` - The context to notify about the update /// * `cx` - The context to notify about the update
pub fn subject(&mut self, subject: impl Into<SharedString>, cx: &mut Context<Self>) { pub fn subject(&mut self, subject: String, cx: &mut Context<Self>) {
self.subject = Some(subject.into()); self.subject = Some(subject);
cx.notify(); cx.notify();
} }
@@ -235,42 +236,11 @@ impl Room {
/// ///
/// * `picture` - The new subject to set /// * `picture` - The new subject to set
/// * `cx` - The context to notify about the update /// * `cx` - The context to notify about the update
pub fn picture(&mut self, picture: impl Into<SharedString>, cx: &mut Context<Self>) { pub fn picture(&mut self, picture: String, cx: &mut Context<Self>) {
self.picture = Some(picture.into()); self.picture = Some(picture);
cx.notify(); cx.notify();
} }
/// Returns a human-readable string representing how long ago the room was created
///
/// The string will be formatted differently based on the time elapsed:
/// - Less than a minute: "now"
/// - Less than an hour: "Xm" (minutes)
/// - Less than a day: "Xh" (hours)
/// - Less than a month: "Xd" (days)
/// - More than a month: "MMM DD" (month abbreviation and day)
///
/// # Returns
///
/// A SharedString containing the formatted time representation
pub fn ago(&self) -> SharedString {
let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
chrono::LocalResult::Single(time) => time,
_ => return "1m".into(),
};
let now = Local::now();
let duration = now.signed_duration_since(input_time);
match duration {
d if d.num_seconds() < SECONDS_IN_MINUTE => NOW.into(),
d if d.num_minutes() < MINUTES_IN_HOUR => format!("{}m", d.num_minutes()),
d if d.num_hours() < HOURS_IN_DAY => format!("{}h", d.num_hours()),
d if d.num_days() < DAYS_IN_MONTH => format!("{}d", d.num_days()),
_ => input_time.format("%b %d").to_string(),
}
.into()
}
/// Gets the display name for the room /// Gets the display name for the room
/// ///
/// If the room has a subject set, that will be used as the display name. /// If the room has a subject set, that will be used as the display name.
@@ -282,8 +252,8 @@ impl Room {
/// ///
/// # Returns /// # Returns
/// ///
/// A SharedString containing the display name /// A string containing the display name
pub fn display_name(&self, cx: &App) -> SharedString { pub fn display_name(&self, cx: &App) -> String {
if let Some(subject) = self.subject.clone() { if let Some(subject) = self.subject.clone() {
subject subject
} else { } else {
@@ -305,8 +275,8 @@ impl Room {
/// ///
/// # Returns /// # Returns
/// ///
/// A SharedString containing the image path or URL /// A string containing the image path or URL
pub fn display_image(&self, proxy: bool, cx: &App) -> SharedString { pub fn display_image(&self, proxy: bool, cx: &App) -> String {
if let Some(picture) = self.picture.as_ref() { if let Some(picture) = self.picture.as_ref() {
picture.clone() picture.clone()
} else if !self.is_group() { } else if !self.is_group() {
@@ -325,7 +295,7 @@ impl Room {
} }
/// Merge the names of the first two members of the room. /// Merge the names of the first two members of the room.
pub(crate) fn merge_name(&self, cx: &App) -> SharedString { pub(crate) fn merge_name(&self, cx: &App) -> String {
let registry = Registry::read_global(cx); let registry = Registry::read_global(cx);
if self.is_group() { if self.is_group() {
@@ -346,37 +316,12 @@ impl Room {
name = format!("{}, +{}", name, profiles.len() - 2); name = format!("{}, +{}", name, profiles.len() - 2);
} }
name.into() name
} else { } else {
self.first_member(cx).display_name() self.first_member(cx).display_name()
} }
} }
/// Loads all profiles for this room members from the database
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// A Task that resolves to Result<Vec<Profile>, Error> containing all profiles for this room
pub fn load_metadata(&self, cx: &mut Context<Self>) -> Task<Result<Vec<Profile>, Error>> {
let public_keys = self.members.clone();
cx.background_spawn(async move {
let database = nostr_client().database();
let mut profiles = vec![];
for public_key in public_keys.into_iter() {
let metadata = database.metadata(public_key).await?.unwrap_or_default();
profiles.push(Profile::new(public_key, metadata));
}
Ok(profiles)
})
}
/// Loads all messages for this room from the database /// Loads all messages for this room from the database
/// ///
/// # Arguments /// # Arguments
@@ -397,22 +342,21 @@ impl Room {
.authors(members.clone()) .authors(members.clone())
.pubkeys(members.clone()); .pubkeys(members.clone());
let events = client let events: Vec<Event> = client
.database() .database()
.query(filter) .query(filter)
.await? .await?
.into_iter() .into_iter()
.sorted_by_key(|ev| ev.created_at)
.filter(|ev| ev.compare_pubkeys(&members)) .filter(|ev| ev.compare_pubkeys(&members))
.collect::<Vec<_>>(); .collect();
Ok(events) Ok(events)
}) })
} }
/// Emits a new message signal to the current room /// Emits a new message signal to the current room
pub fn emit_message(&self, event: Event, cx: &mut Context<Self>) { pub fn emit_message(&self, gift_wrap_id: EventId, event: Event, cx: &mut Context<Self>) {
cx.emit(RoomSignal::NewMessage(Box::new(event))); cx.emit(RoomSignal::NewMessage((gift_wrap_id, Box::new(event))));
} }
/// Emits a signal to refresh the current room's messages. /// Emits a signal to refresh the current room's messages.
@@ -473,7 +417,7 @@ impl Room {
let content = content.to_owned(); let content = content.to_owned();
let subject = self.subject.clone(); let subject = self.subject.clone();
let picture = self.picture.clone(); let picture = self.picture.clone();
let public_keys = self.members.clone(); let mut public_keys = self.members.clone();
cx.background_spawn(async move { cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
@@ -516,26 +460,25 @@ impl Room {
tags.push(Tag::custom(TagKind::custom("picture"), vec![picture])); tags.push(Tag::custom(TagKind::custom("picture"), vec![picture]));
} }
let Some((current_user, receivers)) = public_keys.split_last() else { // Remove the current public key from the list of receivers
return Err(anyhow!("Something is wrong. Cannot get receivers list.")); public_keys.retain(|&pk| pk != public_key);
};
// Stored all send errors // Stored all send errors
let mut reports = vec![]; let mut reports = vec![];
for receiver in receivers.iter() { for receiver in public_keys.into_iter() {
match client match client
.send_private_msg(*receiver, &content, tags.clone()) .send_private_msg(receiver, &content, tags.clone())
.await .await
{ {
Ok(output) => { Ok(output) => {
reports.push(SendReport::output(*receiver, output)); reports.push(SendReport::output(receiver, output));
} }
Err(e) => { Err(e) => {
if let nostr_sdk::client::Error::PrivateMsgRelaysNotFound = e { if let nostr_sdk::client::Error::PrivateMsgRelaysNotFound = e {
reports.push(SendReport::nip17_relays_not_found(*receiver)); reports.push(SendReport::nip17_relays_not_found(receiver));
} else { } else {
reports.push(SendReport::error(*receiver, e.to_string())); reports.push(SendReport::error(receiver, e.to_string()));
} }
} }
} }
@@ -544,17 +487,17 @@ impl Room {
// Only send a backup message to current user if sent successfully to others // Only send a backup message to current user if sent successfully to others
if reports.iter().all(|r| r.is_sent_success()) && backup { if reports.iter().all(|r| r.is_sent_success()) && backup {
match client match client
.send_private_msg(*current_user, &content, tags.clone()) .send_private_msg(public_key, &content, tags.clone())
.await .await
{ {
Ok(output) => { Ok(output) => {
reports.push(SendReport::output(*current_user, output)); reports.push(SendReport::output(public_key, output));
} }
Err(e) => { Err(e) => {
if let nostr_sdk::client::Error::PrivateMsgRelaysNotFound = e { if let nostr_sdk::client::Error::PrivateMsgRelaysNotFound = e {
reports.push(SendReport::nip17_relays_not_found(*current_user)); reports.push(SendReport::nip17_relays_not_found(public_key));
} else { } else {
reports.push(SendReport::error(*current_user, e.to_string())); reports.push(SendReport::error(public_key, e.to_string()));
} }
} }
} }

View File

@@ -1,15 +1,15 @@
use std::any::TypeId; use std::any::TypeId;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::sync::Arc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
blue, div, green, px, red, yellow, Animation, AnimationExt, App, AppContext, ClickEvent, div, px, Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context,
Context, DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement, DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, ParentElement as _, Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
Window, Subscription, Window,
}; };
use smol::Timer; use smol::Timer;
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -18,13 +18,30 @@ use crate::animation::cubic_bezier;
use crate::button::{Button, ButtonVariants as _}; use crate::button::{Button, ButtonVariants as _};
use crate::{h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt}; use crate::{h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt};
#[derive(Debug, Clone, Copy, Default)]
pub enum NotificationType { pub enum NotificationType {
#[default]
Info, Info,
Success, Success,
Warning, Warning,
Error, Error,
} }
impl NotificationType {
fn icon(&self, cx: &App) -> Icon {
match self {
Self::Info => Icon::new(IconName::Info).text_color(cx.theme().element_active),
Self::Warning => Icon::new(IconName::Report).text_color(cx.theme().warning_foreground),
Self::Success => {
Icon::new(IconName::CheckCircle).text_color(cx.theme().element_foreground)
}
Self::Error => {
Icon::new(IconName::CloseCircle).text_color(cx.theme().danger_foreground)
}
}
}
}
#[derive(Debug, PartialEq, Clone, Hash, Eq)] #[derive(Debug, PartialEq, Clone, Hash, Eq)]
pub(crate) enum NotificationId { pub(crate) enum NotificationId {
Id(TypeId), Id(TypeId),
@@ -43,8 +60,6 @@ impl From<(TypeId, ElementId)> for NotificationId {
} }
} }
type OnClick = Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>;
/// A notification element. /// A notification element.
pub struct Notification { pub struct Notification {
/// The id is used make the notification unique. /// The id is used make the notification unique.
@@ -52,48 +67,54 @@ pub struct Notification {
/// ///
/// None means the notification will be added to the end of the list. /// None means the notification will be added to the end of the list.
id: NotificationId, id: NotificationId,
kind: NotificationType, style: StyleRefinement,
type_: Option<NotificationType>,
title: Option<SharedString>, title: Option<SharedString>,
message: SharedString, message: Option<SharedString>,
icon: Option<Icon>, icon: Option<Icon>,
autohide: bool, autohide: bool,
on_click: OnClick, #[allow(clippy::type_complexity)]
action_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> Button>>,
#[allow(clippy::type_complexity)]
content_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>>,
#[allow(clippy::type_complexity)]
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
closing: bool, closing: bool,
} }
impl From<String> for Notification { impl From<String> for Notification {
fn from(s: String) -> Self { fn from(s: String) -> Self {
Self::new(s) Self::new().message(s)
} }
} }
impl From<Cow<'static, str>> for Notification { impl From<Cow<'static, str>> for Notification {
fn from(s: Cow<'static, str>) -> Self { fn from(s: Cow<'static, str>) -> Self {
Self::new(s) Self::new().message(s)
} }
} }
impl From<SharedString> for Notification { impl From<SharedString> for Notification {
fn from(s: SharedString) -> Self { fn from(s: SharedString) -> Self {
Self::new(s) Self::new().message(s)
} }
} }
impl From<&'static str> for Notification { impl From<&'static str> for Notification {
fn from(s: &'static str) -> Self { fn from(s: &'static str) -> Self {
Self::new(s) Self::new().message(s)
} }
} }
impl From<(NotificationType, &'static str)> for Notification { impl From<(NotificationType, &'static str)> for Notification {
fn from((type_, content): (NotificationType, &'static str)) -> Self { fn from((type_, content): (NotificationType, &'static str)) -> Self {
Self::new(content).with_type(type_) Self::new().message(content).with_type(type_)
} }
} }
impl From<(NotificationType, SharedString)> for Notification { impl From<(NotificationType, SharedString)> for Notification {
fn from((type_, content): (NotificationType, SharedString)) -> Self { fn from((type_, content): (NotificationType, SharedString)) -> Self {
Self::new(content).with_type(type_) Self::new().message(content).with_type(type_)
} }
} }
@@ -103,36 +124,52 @@ impl Notification {
/// Create a new notification with the given content. /// Create a new notification with the given content.
/// ///
/// default width is 320px. /// default width is 320px.
pub fn new(message: impl Into<SharedString>) -> Self { pub fn new() -> Self {
let id: SharedString = uuid::Uuid::new_v4().to_string().into(); let id: SharedString = uuid::Uuid::new_v4().to_string().into();
let id = (TypeId::of::<DefaultIdType>(), id.into()); let id = (TypeId::of::<DefaultIdType>(), id.into());
Self { Self {
id: id.into(), id: id.into(),
style: StyleRefinement::default(),
title: None, title: None,
message: message.into(), message: None,
kind: NotificationType::Info, type_: None,
icon: None, icon: None,
autohide: true, autohide: true,
action_builder: None,
content_builder: None,
on_click: None, on_click: None,
closing: false, closing: false,
} }
} }
pub fn message(mut self, message: impl Into<SharedString>) -> Self {
self.message = Some(message.into());
self
}
pub fn info(message: impl Into<SharedString>) -> Self { pub fn info(message: impl Into<SharedString>) -> Self {
Self::new(message).with_type(NotificationType::Info) Self::new()
.message(message)
.with_type(NotificationType::Info)
} }
pub fn success(message: impl Into<SharedString>) -> Self { pub fn success(message: impl Into<SharedString>) -> Self {
Self::new(message).with_type(NotificationType::Success) Self::new()
.message(message)
.with_type(NotificationType::Success)
} }
pub fn warning(message: impl Into<SharedString>) -> Self { pub fn warning(message: impl Into<SharedString>) -> Self {
Self::new(message).with_type(NotificationType::Warning) Self::new()
.message(message)
.with_type(NotificationType::Warning)
} }
pub fn error(message: impl Into<SharedString>) -> Self { pub fn error(message: impl Into<SharedString>) -> Self {
Self::new(message).with_type(NotificationType::Error) Self::new()
.message(message)
.with_type(NotificationType::Error)
} }
/// Set the type for unique identification of the notification. /// Set the type for unique identification of the notification.
@@ -147,8 +184,8 @@ impl Notification {
} }
/// Set the type and id of the notification, used to uniquely identify the notification. /// Set the type and id of the notification, used to uniquely identify the notification.
pub fn id1<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self { pub fn custom_id(mut self, key: impl Into<ElementId>) -> Self {
self.id = (TypeId::of::<T>(), key.into()).into(); self.id = (TypeId::of::<DefaultIdType>(), key.into()).into();
self self
} }
@@ -170,7 +207,7 @@ impl Notification {
/// Set the type of the notification, default is NotificationType::Info. /// Set the type of the notification, default is NotificationType::Info.
pub fn with_type(mut self, type_: NotificationType) -> Self { pub fn with_type(mut self, type_: NotificationType) -> Self {
self.kind = type_; self.type_ = Some(type_);
self self
} }
@@ -185,11 +222,21 @@ impl Notification {
mut self, mut self,
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
self.on_click = Some(Arc::new(on_click)); self.on_click = Some(Rc::new(on_click));
self self
} }
fn dismiss(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) { /// Set the action button of the notification.
pub fn action<F>(mut self, action: F) -> Self
where
F: Fn(&mut Window, &mut Context<Self>) -> Button + 'static,
{
self.action_builder = Some(Rc::new(action));
self
}
/// Dismiss the notification.
pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context<Self>) {
self.closing = true; self.closing = true;
cx.notify(); cx.notify();
@@ -207,31 +254,48 @@ impl Notification {
}) })
.detach() .detach()
} }
/// Set the content of the notification.
pub fn content(
mut self,
content: impl Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static,
) -> Self {
self.content_builder = Some(Rc::new(content));
self
}
}
impl Default for Notification {
fn default() -> Self {
Self::new()
}
} }
impl EventEmitter<DismissEvent> for Notification {} impl EventEmitter<DismissEvent> for Notification {}
impl FluentBuilder for Notification {} impl FluentBuilder for Notification {}
impl Styled for Notification {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl Render for Notification { impl Render for Notification {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let closing = self.closing; let closing = self.closing;
let icon = match self.icon.clone() { let icon = match self.type_ {
Some(icon) => icon, None => self.icon.clone(),
None => match self.kind { Some(type_) => Some(type_.icon(cx)),
NotificationType::Info => Icon::new(IconName::Info).text_color(blue()),
NotificationType::Warning => Icon::new(IconName::Info).text_color(yellow()),
NotificationType::Error => Icon::new(IconName::CloseCircle).text_color(red()),
NotificationType::Success => Icon::new(IconName::CheckCircle).text_color(green()),
},
}; };
div() h_flex()
.id("notification") .id("notification")
.refine_style(&self.style)
.group("") .group("")
.occlude() .occlude()
.relative() .relative()
.w_72() .w_96()
.border_1() .border_1()
.border_color(cx.theme().border) .border_color(cx.theme().border)
.bg(cx.theme().surface_background) .bg(cx.theme().surface_background)
@@ -239,30 +303,34 @@ impl Render for Notification {
.shadow_md() .shadow_md()
.p_2() .p_2()
.gap_3() .gap_3()
.child(div().absolute().top_2p5().left_2().child(icon)) .justify_start()
.items_start()
.when_some(icon, |this, icon| {
this.child(div().flex_shrink_0().pt_1().child(icon))
})
.child( .child(
v_flex() v_flex()
.pl_6() .flex_1()
.gap_1() .gap_1()
.when_some(self.title.clone(), |this, title| {
this.child(div().text_xs().font_semibold().child(title))
})
.overflow_hidden() .overflow_hidden()
.child(div().text_xs().child(self.message.clone())), .when_some(self.title.clone(), |this, title| {
) this.child(div().text_sm().font_semibold().child(title))
.when_some(self.on_click.clone(), |this, on_click| {
this.cursor_pointer()
.on_click(cx.listener(move |view, event, window, cx| {
view.dismiss(event, window, cx);
on_click(event, window, cx);
}))
}) })
.when(!self.autohide, |this| { .when_some(self.message.clone(), |this, message| {
this.child( this.child(div().text_sm().child(message))
h_flex() })
.when_some(self.content_builder.clone(), |this, child_builder| {
this.child(child_builder(window, cx))
})
.when_some(self.action_builder.clone(), |this, action_builder| {
this.child(action_builder(window, cx).small().w_full().my_2())
}),
)
.child(
div()
.absolute() .absolute()
.top_1() .top_2p5()
.right_1() .right_2p5()
.invisible() .invisible()
.group_hover("", |this| this.visible()) .group_hover("", |this| this.visible())
.child( .child(
@@ -270,21 +338,35 @@ impl Render for Notification {
.icon(IconName::Close) .icon(IconName::Close)
.ghost() .ghost()
.xsmall() .xsmall()
.on_click(cx.listener(Self::dismiss)), .on_click(cx.listener(|this, _, window, cx| {
this.dismiss(window, cx);
})),
), ),
) )
.when_some(self.on_click.clone(), |this, on_click| {
this.on_click(cx.listener(move |view, event, window, cx| {
view.dismiss(window, cx);
on_click(event, window, cx);
}))
}) })
.with_animation( .with_animation(
ElementId::NamedInteger("slide-down".into(), closing as u64), ElementId::NamedInteger("slide-down".into(), closing as u64),
Animation::new(Duration::from_secs_f64(0.15)) Animation::new(Duration::from_secs_f64(0.25))
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)), .with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
move |this, delta| { move |this, delta| {
if closing { if closing {
let x_offset = px(0.) + delta * px(45.); let x_offset = px(0.) + delta * px(45.);
this.left(px(0.) + x_offset).opacity(1. - delta) let opacity = 1. - delta;
this.left(px(0.) + x_offset)
.shadow_none()
.opacity(opacity)
.when(opacity < 0.85, |this| this.shadow_none())
} else { } else {
let y_offset = px(-45.) + delta * px(45.); let y_offset = px(-45.) + delta * px(45.);
this.top(px(0.) + y_offset).opacity(delta) let opacity = delta;
this.top(px(0.) + y_offset)
.opacity(opacity)
.when(opacity < 0.85, |this| this.shadow_none())
} }
}, },
) )
@@ -296,7 +378,7 @@ pub struct NotificationList {
/// Notifications that will be auto hidden. /// Notifications that will be auto hidden.
pub(crate) notifications: VecDeque<Entity<Notification>>, pub(crate) notifications: VecDeque<Entity<Notification>>,
expanded: bool, expanded: bool,
subscriptions: HashMap<NotificationId, Subscription>, _subscriptions: HashMap<NotificationId, Subscription>,
} }
impl NotificationList { impl NotificationList {
@@ -304,16 +386,14 @@ impl NotificationList {
Self { Self {
notifications: VecDeque::new(), notifications: VecDeque::new(),
expanded: false, expanded: false,
subscriptions: HashMap::new(), _subscriptions: HashMap::new(),
} }
} }
pub fn push( pub fn push<T>(&mut self, notification: T, window: &mut Window, cx: &mut Context<Self>)
&mut self, where
notification: impl Into<Notification>, T: Into<Notification>,
window: &mut Window, {
cx: &mut Context<Self>,
) {
let notification = notification.into(); let notification = notification.into();
let id = notification.id.clone(); let id = notification.id.clone();
let autohide = notification.autohide; let autohide = notification.autohide;
@@ -323,28 +403,47 @@ impl NotificationList {
let notification = cx.new(|_| notification); let notification = cx.new(|_| notification);
self.subscriptions.insert( self._subscriptions.insert(
id.clone(), id.clone(),
cx.subscribe(&notification, move |view, _, _: &DismissEvent, cx| { cx.subscribe(&notification, move |view, _, _: &DismissEvent, cx| {
view.notifications.retain(|note| id != note.read(cx).id); view.notifications.retain(|note| id != note.read(cx).id);
view.subscriptions.remove(&id); view._subscriptions.remove(&id);
}), }),
); );
self.notifications.push_back(notification.clone()); self.notifications.push_back(notification.clone());
if autohide { if autohide {
// Sleep for 3 seconds to autohide the notification // Sleep for 5 seconds to autohide the notification
cx.spawn_in(window, async move |_, cx| { cx.spawn_in(window, async move |_, cx| {
Timer::after(Duration::from_secs(3)).await; Timer::after(Duration::from_secs(5)).await;
_ = notification.update_in(cx, |note, window, cx| {
note.dismiss(&ClickEvent::default(), window, cx) if let Err(error) =
}); notification.update_in(cx, |note, window, cx| note.dismiss(window, cx))
{
log::error!("Failed to auto hide notification: {error}");
}
}) })
.detach(); .detach();
} }
cx.notify(); cx.notify();
} }
pub(crate) fn close<T>(&mut self, key: T, window: &mut Window, cx: &mut Context<Self>)
where
T: Into<ElementId>,
{
let id = (TypeId::of::<DefaultIdType>(), key.into()).into();
if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) {
n.update(cx, |note, cx| {
note.dismiss(window, cx);
});
}
cx.notify();
}
pub fn clear(&mut self, _window: &mut Window, cx: &mut Context<Self>) { pub fn clear(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.notifications.clear(); self.notifications.clear();
cx.notify(); cx.notify();
@@ -356,24 +455,25 @@ impl NotificationList {
} }
impl Render for NotificationList { impl Render for NotificationList {
fn render( fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
&mut self,
window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
let size = window.viewport_size(); let size = window.viewport_size();
let items = self.notifications.iter().rev().take(10).rev().cloned(); let items = self.notifications.iter().rev().take(10).rev().cloned();
div().absolute().top_4().right_4().child( div()
.id("notification-wrapper")
.absolute()
.top_4()
.right_4()
.child(
v_flex() v_flex()
.id("notification-list") .id("notification-list")
.h(size.height - px(8.)) .h(size.height - px(8.))
.gap_3()
.children(items)
.on_hover(cx.listener(|view, hovered, _, cx| { .on_hover(cx.listener(|view, hovered, _, cx| {
view.expanded = *hovered; view.expanded = *hovered;
cx.notify() cx.notify()
})) })),
.gap_3()
.children(items),
) )
} }
} }

View File

@@ -3,10 +3,11 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
actions, anchored, canvas, div, px, rems, Action, AnyElement, App, AppContext, Bounds, Context, actions, anchored, canvas, div, px, rems, Action, AnyElement, App, AppContext, AsKeystroke,
Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, KeyBinding, Keystroke, ParentElement, Pixels, Render, ScrollHandle, SharedString, InteractiveElement, IntoElement, KeyBinding, Keystroke, ParentElement, Pixels, Render,
StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Subscription, WeakEntity,
Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -472,7 +473,7 @@ impl PopupMenu {
keybinding keybinding
.keystrokes() .keystrokes()
.iter() .iter()
.map(|key| key_shortcut(key.clone())), .map(|key| key_shortcut(key.as_keystroke().clone())),
); );
return Some(el); return Some(el);

View File

@@ -3,7 +3,7 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, AnyView, App, AppContext, Context, Decorations, Entity, FocusHandle, InteractiveElement, div, AnyView, App, AppContext, Context, Decorations, Entity, FocusHandle, InteractiveElement,
IntoElement, ParentElement as _, Render, Styled, Window, IntoElement, ParentElement as _, Render, SharedString, Styled, Window,
}; };
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING}; use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
@@ -34,6 +34,9 @@ pub trait ContextModal: Sized {
/// Pushes a notification to the notification list. /// Pushes a notification to the notification list.
fn push_notification(&mut self, note: impl Into<Notification>, cx: &mut App); fn push_notification(&mut self, note: impl Into<Notification>, cx: &mut App);
/// Clears a notification by its ID.
fn clear_notification_by_id(&mut self, id: SharedString, cx: &mut App);
/// Clear all notifications /// Clear all notifications
fn clear_notifications(&mut self, cx: &mut App); fn clear_notifications(&mut self, cx: &mut App);
@@ -112,6 +115,15 @@ impl ContextModal for Window {
}) })
} }
fn clear_notification_by_id(&mut self, id: SharedString, cx: &mut App) {
Root::update(self, cx, move |root, window, cx| {
root.notification.update(cx, |view, cx| {
view.close(id.clone(), window, cx);
});
cx.notify();
})
}
fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>> { fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>> {
let entity = Root::read(self, cx).notification.clone(); let entity = Root::read(self, cx).notification.clone();
Rc::new(entity.read(cx).notifications()) Rc::new(entity.read(cx).notifications())

View File

@@ -1,21 +1,23 @@
use std::time::Duration; use std::time::Duration;
use gpui::{ use gpui::{
bounce, div, ease_in_out, Animation, AnimationExt, Div, IntoElement, ParentElement as _, bounce, div, ease_in_out, Animation, AnimationExt, IntoElement, RenderOnce, StyleRefinement,
RenderOnce, Styled, Styled,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::StyledExt;
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Skeleton { pub struct Skeleton {
base: Div, style: StyleRefinement,
secondary: bool, secondary: bool,
} }
impl Skeleton { impl Skeleton {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
base: div().w_full().h_4().rounded_md(), style: StyleRefinement::default(),
secondary: false, secondary: false,
} }
} }
@@ -34,7 +36,7 @@ impl Default for Skeleton {
impl Styled for Skeleton { impl Styled for Skeleton {
fn style(&mut self) -> &mut gpui::StyleRefinement { fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style() &mut self.style
} }
} }
@@ -46,8 +48,13 @@ impl RenderOnce for Skeleton {
cx.theme().ghost_element_active cx.theme().ghost_element_active
}; };
div().child( div()
self.base.bg(color).with_animation( .w_full()
.h_4()
.rounded_md()
.refine_style(&self.style)
.bg(color)
.with_animation(
"skeleton", "skeleton",
Animation::new(Duration::from_secs(2)) Animation::new(Duration::from_secs(2))
.repeat() .repeat()
@@ -56,7 +63,6 @@ impl RenderOnce for Skeleton {
let v = 1.0 - delta * 0.5; let v = 1.0 - delta * 0.5;
this.opacity(v) this.opacity(v)
}, },
),
) )
} }
} }

View File

@@ -1,7 +1,7 @@
use std::ops::Range; use std::ops::Range;
use std::sync::Arc; use std::sync::Arc;
use common::display::DisplayProfile; use common::display::ReadableProfile;
use gpui::{ use gpui::{
AnyElement, AnyView, App, ElementId, HighlightStyle, InteractiveText, IntoElement, AnyElement, AnyView, App, ElementId, HighlightStyle, InteractiveText, IntoElement,
SharedString, StyledText, UnderlineStyle, Window, SharedString, StyledText, UnderlineStyle, Window,

View File

@@ -35,6 +35,16 @@ common:
en: "Open Browser" en: "Open Browser"
refreshed: refreshed:
en: "Refreshed" en: "Refreshed"
quit:
en: "Quit"
restart:
en: "Restart"
approve:
en: "Approve"
ignore:
en: "Ignore"
relay:
en: "Relay"
auto_update: auto_update:
updating: updating:
@@ -82,6 +92,14 @@ proxy:
description: description:
en: "Open your default browser and approve the connection request in your Nostr Signer extension" en: "Open your default browser and approve the connection request in your Nostr Signer extension"
auth:
label:
en: "Authentication Required"
message:
en: "Approve the authentication request to allow Coop to continue getting your messages."
requests:
en: "You have %{u} total pending authentication requests"
startup: startup:
client_keys_warning: client_keys_warning:
en: "Warning" en: "Warning"