chore: rewrite the backend (not tested) (#203)

* wip: refactor

* refactor

* clean up

* .

* rename

* add relay auth

* .

* .

* optimize

* .

* clean up

* add encryption crate

* .

* .

* .

* .

* .

* add encryption crate

* .

* refactor nip4e

* .

* fix endless loop

* fix metadata fetching
This commit is contained in:
reya
2025-11-11 09:09:33 +07:00
committed by GitHub
parent a1a0a7ecd4
commit 512834b640
68 changed files with 3503 additions and 3194 deletions

321
Cargo.lock generated
View File

@@ -7,13 +7,18 @@ name = "account"
version = "0.2.11"
dependencies = [
"anyhow",
"common",
"gpui",
"log",
"nostr",
"nostr-sdk",
"serde",
"serde_json",
"settings",
"smallvec",
"smol",
"states",
"state",
"theme",
"ui",
]
[[package]]
@@ -125,7 +130,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -369,7 +374,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -445,7 +450,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -524,7 +529,7 @@ dependencies = [
"semver",
"smallvec",
"smol",
"states",
"state",
"tempfile",
]
@@ -621,7 +626,7 @@ dependencies = [
"regex",
"rustc-hash 2.1.1",
"shlex",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -641,7 +646,7 @@ dependencies = [
"regex",
"rustc-hash 2.1.1",
"shlex",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -773,7 +778,7 @@ checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -873,7 +878,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -942,7 +947,7 @@ dependencies = [
"quote",
"serde",
"serde_json",
"syn 2.0.108",
"syn 2.0.109",
"tempfile",
"toml 0.8.23",
]
@@ -1036,12 +1041,13 @@ dependencies = [
"account",
"anyhow",
"common",
"encryption",
"flume",
"futures",
"fuzzy-matcher",
"gpui",
"itertools 0.13.0",
"log",
"nostr",
"nostr-sdk",
"person",
"serde",
@@ -1049,7 +1055,7 @@ dependencies = [
"settings",
"smallvec",
"smol",
"states",
"state",
]
[[package]]
@@ -1061,12 +1067,12 @@ dependencies = [
"chat",
"common",
"emojis",
"encryption",
"gpui",
"gpui_tokio",
"indexset",
"itertools 0.13.0",
"log",
"nostr",
"nostr-sdk",
"once_cell",
"person",
@@ -1076,7 +1082,7 @@ dependencies = [
"settings",
"smallvec",
"smol",
"states",
"state",
"theme",
"ui",
]
@@ -1199,7 +1205,7 @@ dependencies = [
[[package]]
name = "collections"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"indexmap",
"rustc-hash 2.1.1",
@@ -1237,19 +1243,18 @@ version = "0.2.11"
dependencies = [
"anyhow",
"chrono",
"dirs 5.0.1",
"futures",
"gpui",
"itertools 0.13.0",
"log",
"nostr",
"nostr-connect",
"nostr-sdk",
"qrcode",
"reqwest",
"smallvec",
"smol",
"states",
"webbrowser",
"whoami",
]
[[package]]
@@ -1316,8 +1321,8 @@ dependencies = [
"chat",
"chat_ui",
"common",
"dirs 5.0.1",
"flume",
"encryption",
"encryption_ui",
"futures",
"gpui",
"gpui_tokio",
@@ -1326,11 +1331,11 @@ dependencies = [
"itertools 0.13.0",
"key_store",
"log",
"nostr",
"nostr-connect",
"nostr-sdk",
"oneshot",
"person",
"relay_auth",
"reqwest_client",
"rust-i18n",
"serde",
@@ -1338,7 +1343,7 @@ dependencies = [
"settings",
"smallvec",
"smol",
"states",
"state",
"theme",
"title_bar",
"tracing-subscriber",
@@ -1617,17 +1622,17 @@ dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
name = "derive_refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -1706,7 +1711,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -1819,6 +1824,49 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "encryption"
version = "0.2.11"
dependencies = [
"account",
"anyhow",
"common",
"flume",
"futures",
"gpui",
"log",
"nostr-sdk",
"serde",
"serde_json",
"smallvec",
"smol",
"state",
]
[[package]]
name = "encryption_ui"
version = "0.2.11"
dependencies = [
"account",
"anyhow",
"common",
"encryption",
"futures",
"gpui",
"itertools 0.13.0",
"log",
"nostr-sdk",
"person",
"serde",
"serde_json",
"settings",
"smallvec",
"smol",
"state",
"theme",
"ui",
]
[[package]]
name = "endi"
version = "1.1.0"
@@ -1843,7 +1891,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -1863,7 +1911,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -1986,7 +2034,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -2180,7 +2228,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -2334,7 +2382,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -2520,7 +2568,7 @@ dependencies = [
[[package]]
name = "gpui"
version = "0.2.2"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"anyhow",
"as-raw-xcb-connection",
@@ -2617,18 +2665,18 @@ dependencies = [
[[package]]
name = "gpui_macros"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
name = "gpui_tokio"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"anyhow",
"gpui",
@@ -2857,7 +2905,7 @@ dependencies = [
[[package]]
name = "http_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"anyhow",
"async-compression",
@@ -2882,7 +2930,7 @@ dependencies = [
[[package]]
name = "http_client_tls"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"rustls",
"rustls-platform-verifier",
@@ -3221,7 +3269,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -3253,9 +3301,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "iri-string"
version = "0.7.8"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
dependencies = [
"memchr",
"serde",
@@ -3369,16 +3417,15 @@ name = "key_store"
version = "0.2.11"
dependencies = [
"anyhow",
"common",
"futures",
"gpui",
"log",
"nostr",
"nostr-sdk",
"serde",
"serde_json",
"smallvec",
"smol",
"states",
]
[[package]]
@@ -3647,6 +3694,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mach2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -3679,7 +3735,7 @@ dependencies = [
[[package]]
name = "media"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"anyhow",
"bindgen 0.71.1",
@@ -3928,8 +3984,8 @@ dependencies = [
[[package]]
name = "nostr"
version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
dependencies = [
"aes",
"base64",
@@ -3952,8 +4008,8 @@ dependencies = [
[[package]]
name = "nostr-connect"
version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
dependencies = [
"async-utility",
"nostr",
@@ -3964,8 +4020,8 @@ dependencies = [
[[package]]
name = "nostr-database"
version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
dependencies = [
"flatbuffers",
"lru",
@@ -3975,16 +4031,16 @@ dependencies = [
[[package]]
name = "nostr-gossip"
version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
dependencies = [
"nostr",
]
[[package]]
name = "nostr-gossip-memory"
version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
dependencies = [
"indexmap",
"lru",
@@ -3995,8 +4051,8 @@ dependencies = [
[[package]]
name = "nostr-lmdb"
version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
dependencies = [
"async-utility",
"flume",
@@ -4009,8 +4065,8 @@ dependencies = [
[[package]]
name = "nostr-relay-pool"
version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
dependencies = [
"async-utility",
"async-wsocket",
@@ -4026,8 +4082,8 @@ dependencies = [
[[package]]
name = "nostr-sdk"
version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
dependencies = [
"async-utility",
"nostr",
@@ -4114,7 +4170,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -4391,7 +4447,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -4538,7 +4594,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "perf"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"collections",
"serde",
@@ -4554,11 +4610,10 @@ dependencies = [
"gpui",
"itertools 0.13.0",
"log",
"nostr",
"nostr-sdk",
"smallvec",
"smol",
"states",
"state",
]
[[package]]
@@ -4591,7 +4646,7 @@ dependencies = [
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -4626,7 +4681,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -4757,7 +4812,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -4788,7 +4843,7 @@ dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -4816,7 +4871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
dependencies = [
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -4937,9 +4992,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.41"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
@@ -5159,13 +5214,13 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
name = "refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"derive_refineable",
]
@@ -5199,6 +5254,23 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "relay_auth"
version = "0.2.11"
dependencies = [
"anyhow",
"common",
"gpui",
"log",
"nostr-sdk",
"settings",
"smallvec",
"smol",
"state",
"theme",
"ui",
]
[[package]]
name = "reqwest"
version = "0.12.24"
@@ -5246,7 +5318,7 @@ dependencies = [
[[package]]
name = "reqwest_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"anyhow",
"bytes",
@@ -5300,11 +5372,11 @@ dependencies = [
[[package]]
name = "rope"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"arrayvec",
"gpui",
"log",
"rayon",
"sum_tree",
"unicode-segmentation",
"util",
@@ -5336,7 +5408,7 @@ dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.108",
"syn 2.0.109",
"walkdir",
]
@@ -5379,7 +5451,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -5464,9 +5536,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.34"
version = "0.23.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
dependencies = [
"aws-lc-rs",
"log",
@@ -5624,9 +5696,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "1.0.5"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1317c3bf3e7df961da95b0a56a172a02abead31276215a0497241a7624b487ce"
checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
dependencies = [
"dyn-clone",
"indexmap",
@@ -5638,14 +5710,14 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "1.0.5"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f760a6150d45dd66ec044983c124595ae76912e77ed0b44124cb3e415cce5d9"
checksum = "301858a4023d78debd2353c7426dc486001bddc91ae31a76fb1f55132f7e2633"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -5766,7 +5838,7 @@ checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33"
[[package]]
name = "semantic_version"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"anyhow",
"serde",
@@ -5805,7 +5877,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -5816,7 +5888,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -5863,7 +5935,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -5921,7 +5993,7 @@ dependencies = [
"serde",
"serde_json",
"smallvec",
"states",
"state",
]
[[package]]
@@ -6125,16 +6197,17 @@ checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69"
dependencies = [
"proc-macro-error2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
name = "states"
name = "state"
version = "0.2.11"
dependencies = [
"anyhow",
"dirs 5.0.1",
"flume",
"common",
"event-listener 5.4.1",
"gpui",
"log",
"nostr-gossip-memory",
"nostr-lmdb",
@@ -6142,8 +6215,8 @@ dependencies = [
"rustls",
"serde",
"serde_json",
"smallvec",
"smol",
"whoami",
]
[[package]]
@@ -6189,7 +6262,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -6201,7 +6274,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -6213,12 +6286,11 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "sum_tree"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"arrayvec",
"futures",
"futures-lite 1.13.0",
"log",
"rayon",
]
[[package]]
@@ -6339,9 +6411,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.108"
version = "2.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f"
dependencies = [
"proc-macro2",
"quote",
@@ -6374,7 +6446,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -6538,7 +6610,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -6549,7 +6621,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -6677,7 +6749,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -6897,7 +6969,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -7201,7 +7273,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "util"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"anyhow",
"async-fs",
@@ -7216,6 +7288,7 @@ dependencies = [
"itertools 0.14.0",
"libc",
"log",
"mach2",
"nix 0.29.0",
"regex",
"rust-embed",
@@ -7236,11 +7309,11 @@ dependencies = [
[[package]]
name = "util_macros"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
dependencies = [
"perf",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -7438,7 +7511,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
"wasm-bindgen-shared",
]
@@ -7808,7 +7881,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -7819,7 +7892,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -7830,7 +7903,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -7841,7 +7914,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -8437,7 +8510,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
"synstructure",
]
@@ -8484,7 +8557,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
"zbus_names",
"zvariant",
"zvariant_utils",
@@ -8632,7 +8705,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -8652,7 +8725,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
"synstructure",
]
@@ -8673,7 +8746,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -8706,7 +8779,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
]
[[package]]
@@ -8757,7 +8830,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.109",
"zvariant_utils",
]
@@ -8770,6 +8843,6 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.108",
"syn 2.0.109",
"winnow",
]

View File

@@ -22,7 +22,6 @@ gpui_tokio = { git = "https://github.com/zed-industries/zed" }
reqwest_client = { git = "https://github.com/zed-industries/zed" }
# Nostr
nostr = { git = "https://github.com/rust-nostr/nostr" }
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" }
@@ -31,7 +30,6 @@ nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96",
# Others
anyhow = "1.0.44"
chrono = "0.4.38"
dirs = "5.0"
futures = "0.3"
itertools = "0.13.0"
log = "0.4"

View File

View File

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

View File

@@ -1,9 +1,15 @@
use std::sync::Arc;
use std::time::Duration;
use anyhow::Error;
use common::BOOTSTRAP_RELAYS;
use gpui::{App, AppContext, Context, Entity, Global, Task};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
pub fn init(public_key: PublicKey, cx: &mut App) {
Account::set_global(cx.new(|cx| Account::new(public_key, cx)), cx);
pub fn init(cx: &mut App) {
Account::set_global(cx.new(Account::new), cx);
}
struct GlobalAccount(Entity<Account>);
@@ -12,10 +18,24 @@ impl Global for GlobalAccount {}
pub struct Account {
/// The public key of the account
public_key: PublicKey,
public_key: Option<PublicKey>,
/// Status of the current user NIP-65 relays
pub nip65_status: RelayStatus,
/// Status of the current user NIP-17 relays
pub nip17_status: RelayStatus,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 1]>,
_tasks: SmallVec<[Task<()>; 2]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RelayStatus {
#[default]
Initial,
NotSet,
Set,
}
impl Account {
@@ -35,20 +55,152 @@ impl Account {
}
/// Set the global account instance
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalAccount(state));
}
/// Create a new account instance
pub(crate) fn new(public_key: PublicKey, _cx: &mut Context<Self>) -> Self {
fn new(cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let mut tasks = smallvec![];
tasks.push(
// Observe the nostr signer and set the public key when it sets
cx.spawn({
let client = Arc::clone(&client);
async move |this, cx| {
let result = cx
.background_spawn(async move { Self::observe_signer(&client).await })
.await;
if let Some(public_key) = result {
this.update(cx, |this, cx| {
this.set_account(public_key, cx);
})
.expect("Entity has been released")
}
}
}),
);
Self {
public_key,
_tasks: smallvec![],
public_key: None,
nip65_status: RelayStatus::default(),
nip17_status: RelayStatus::default(),
_tasks: tasks,
}
}
/// Observe the signer and return the public key when it sets
async fn observe_signer(client: &Client) -> Option<PublicKey> {
let loop_duration = Duration::from_millis(800);
loop {
if let Ok(signer) = client.signer().await {
if let Ok(public_key) = signer.get_public_key().await {
// Get current user's gossip relays
Self::get_gossip_relays(client, public_key).await.ok()?;
return Some(public_key);
}
}
smol::Timer::after(loop_duration).await;
}
}
/// Get gossip relays for a given public key
async fn get_gossip_relays(client: &Client, public_key: PublicKey) -> Result<(), Error> {
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let filter = Filter::new()
.kind(Kind::RelayList)
.author(public_key)
.limit(1);
// Subscribe to events from the bootstrapping relays
client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await?;
log::info!("Getting user's gossip relays...");
Ok(())
}
/// Ensure the user has NIP-65 relays
async fn ensure_nip65_relays(client: &Client, public_key: PublicKey) -> Result<bool, Error> {
let filter = Filter::new()
.kind(Kind::RelayList)
.author(public_key)
.limit(1);
// Count the number of nip65 relays event in the database
let total = client.database().count(filter).await.unwrap_or(0);
Ok(total > 0)
}
/// Ensure the user has NIP-17 relays
async fn ensure_nip17_relays(client: &Client, public_key: PublicKey) -> Result<bool, Error> {
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
// Count the number of nip17 relays event in the database
let total = client.database().count(filter).await.unwrap_or(0);
Ok(total > 0)
}
/// Set the public key of the account
pub fn set_account(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Update account's public key
self.public_key = Some(public_key);
// Add background task
self._tasks.push(
// Verify user's nip65 and nip17 relays
cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(Duration::from_secs(10))
.await;
let ensure_nip65 = Self::ensure_nip65_relays(&client, public_key).await;
let ensure_nip17 = Self::ensure_nip17_relays(&client, public_key).await;
this.update(cx, |this, cx| {
this.nip65_status = match ensure_nip65 {
Ok(true) => RelayStatus::Set,
_ => RelayStatus::NotSet,
};
this.nip17_status = match ensure_nip17 {
Ok(true) => RelayStatus::Set,
_ => RelayStatus::NotSet,
};
cx.notify();
})
.expect("Entity has been released")
}),
);
cx.notify();
}
/// Check if the account entity has a public key
pub fn has_account(&self) -> bool {
self.public_key.is_some()
}
/// Get the public key of the account
pub fn public_key(&self) -> PublicKey {
self.public_key
// This method is only called when user is logged in, so unwrap safely
self.public_key.unwrap()
}
}

View File

@@ -5,7 +5,7 @@ use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "../../assets"]
#[include = "fonts/**/*"]
#[include = "brand/*"]
#[include = "brand/**/*"]
#[include = "icons/**/*"]
#[exclude = "*.DS_Store"]
pub struct Assets;

View File

@@ -6,7 +6,7 @@ publish.workspace = true
[dependencies]
common = { path = "../common" }
states = { path = "../states" }
state = { path = "../state" }
gpui.workspace = true
gpui_tokio.workspace = true

View File

@@ -4,6 +4,7 @@ use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use common::BOOTSTRAP_RELAYS;
use gpui::http_client::{AsyncBody, HttpClient};
use gpui::{
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task,
@@ -13,7 +14,7 @@ use semver::Version;
use smallvec::{smallvec, SmallVec};
use smol::fs::File;
use smol::process::Command;
use states::{app_state, BOOTSTRAP_RELAYS};
use state::NostrRegistry;
const APP_PUBKEY: &str = "npub1y9jvl5vznq49eh9f2gj7679v4042kj80lp7p8fte3ql2cr7hty7qsyca8q";
@@ -230,8 +231,10 @@ impl AutoUpdater {
}
fn subscribe_to_updates(cx: &App) -> Task<()> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
cx.background_spawn(async move {
let client = app_state().client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let app_pubkey = PublicKey::parse(APP_PUBKEY).unwrap();
@@ -250,8 +253,14 @@ impl AutoUpdater {
}
fn check_for_updates(version: Version, cx: &AsyncApp) -> Task<Result<Vec<EventId>, Error>> {
let Ok(client) = cx.update(|cx| {
let nostr = NostrRegistry::global(cx);
nostr.read(cx).client()
}) else {
return Task::ready(Err(anyhow!("Entity has been released")));
};
cx.background_spawn(async move {
let client = app_state().client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let app_pubkey = PublicKey::parse(APP_PUBKEY).unwrap();
@@ -294,11 +303,12 @@ impl AutoUpdater {
}
fn get_latest_release(&mut self, ids: &[EventId], cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let http_client = cx.http_client();
let ids = ids.to_vec();
let task: Task<Result<(InstallerDir, PathBuf), Error>> = cx.background_spawn(async move {
let client = app_state().client();
let app_pubkey = PublicKey::parse(APP_PUBKEY).unwrap();
let os = std::env::consts::OS;

View File

@@ -6,20 +6,22 @@ publish.workspace = true
[dependencies]
common = { path = "../common" }
states = { path = "../states" }
state = { path = "../state" }
account = { path = "../account" }
encryption = { path = "../encryption" }
person = { path = "../person" }
settings = { path = "../settings" }
gpui.workspace = true
nostr.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
itertools.workspace = true
smallvec.workspace = true
smol.workspace = true
log.workspace = true
futures.workspace = true
flume.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -1,27 +1,53 @@
use std::cmp::Reverse;
use std::collections::{HashMap, HashSet};
use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use account::Account;
use anyhow::Error;
use common::event::EventUtils;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use common::{EventUtils, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT};
use encryption::Encryption;
use flume::Sender;
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, Window};
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task};
pub use message::*;
use nostr_sdk::prelude::*;
use room::RoomKind;
pub use room::*;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use states::{app_state, NewMessage};
use smol::lock::RwLock;
use state::{initialized_at, EventTracker, NostrRegistry, GIFTWRAP_SUBSCRIPTION};
use crate::room::Room;
pub mod message;
pub mod room;
mod message;
mod room;
pub fn init(cx: &mut App) {
ChatRegistry::set_global(cx.new(ChatRegistry::new), cx);
}
struct GlobalChatRegistry(Entity<ChatRegistry>);
impl Global for GlobalChatRegistry {}
/// Chat Registry
#[derive(Debug)]
pub struct ChatRegistry {
/// Collection of all chat rooms
pub rooms: Vec<Entity<Room>>,
/// Loading status of the registry
pub loading: bool,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 4]>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChatEvent {
OpenRoom(u64),
@@ -29,43 +55,271 @@ pub enum ChatEvent {
NewChatRequest(RoomKind),
}
struct GlobalChatRegistry(Entity<ChatRegistry>);
impl Global for GlobalChatRegistry {}
pub struct ChatRegistry {
/// Collection of all chat rooms
pub rooms: Vec<Entity<Room>>,
/// Loading status of the registry
pub loading: bool,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 2]>,
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Signal {
Loading(bool),
Message(NewMessage),
Eose,
}
impl EventEmitter<ChatEvent> for ChatRegistry {}
impl ChatRegistry {
/// Retrieve the global registry state
/// Retrieve the global chat registry state
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalChatRegistry>().0.clone()
}
/// Set the global registry instance
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
/// Set the global chat registry instance
fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalChatRegistry(state));
}
/// Create a new registry instance
pub(crate) fn new(_cx: &mut Context<Self>) -> Self {
/// Create a new chat registry instance
fn new(cx: &mut Context<Self>) -> Self {
let encryption = Encryption::global(cx);
let encryption_key = encryption.read(cx).encryption.clone();
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let tracker = nostr.read(cx).tracker();
let status = Arc::new(AtomicBool::new(true));
let (tx, rx) = flume::bounded::<Signal>(2048);
let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
subscriptions.push(
// Observe the encryption global state
cx.observe(&encryption_key, {
let status = Arc::clone(&status);
let tx = tx.clone();
move |this, state, cx| {
if let Some(signer) = state.read(cx).clone() {
this.retry_failed_events(&signer, &tx, &status, cx);
}
}
}),
);
tasks.push(
// Handle notifications
cx.background_spawn({
let client = Arc::clone(&client);
let status = Arc::clone(&status);
let tx = tx.clone();
async move { Self::handle_notifications(&client, &tracker, &tx, &status).await }
}),
);
tasks.push(
// Handle unwrapping status
cx.background_spawn({
let client = Arc::clone(&client);
async move { Self::handle_unwrapping(&client, &status, &tx).await }
}),
);
tasks.push(
// Handle new messages
cx.spawn(async move |this, cx| {
while let Ok(message) = rx.recv_async().await {
match message {
Signal::Message(message) => {
this.update(cx, |this, cx| {
this.new_message(message, cx);
})
.expect("Entity has been released");
}
Signal::Eose => {
this.update(cx, |this, cx| {
this.get_rooms(cx);
})
.expect("Entity has been released");
}
Signal::Loading(status) => {
this.update(cx, |this, cx| {
this.set_loading(status, cx);
this.get_rooms(cx);
})
.expect("Entity has been released");
}
};
}
}),
);
Self {
rooms: vec![],
loading: true,
_tasks: smallvec![],
_subscriptions: subscriptions,
_tasks: tasks,
}
}
async fn handle_notifications(
client: &Client,
tracker: &Arc<RwLock<EventTracker>>,
tx: &Sender<Signal>,
status: &Arc<AtomicBool>,
) {
let mut notifications = client.notifications();
log::info!("Listening for notifications");
let initialized_at = initialized_at();
let subscription_id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION);
let mut public_keys = HashSet::new();
let mut processed_events = HashSet::new();
while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, .. } = notification else {
// Skip non-message notifications
continue;
};
match message {
RelayMessage::Event { event, .. } => {
if !processed_events.insert(event.id) {
// Skip if the event has already been processed
continue;
}
if event.kind != Kind::GiftWrap {
// Skip non-gift wrap events
continue;
}
// Extract the rumor from the gift wrap event
match Self::extract_rumor(client, event.as_ref()).await {
Ok(rumor) => {
// Get all public keys
public_keys.extend(rumor.all_pubkeys());
let limit_reached = public_keys.len() >= METADATA_BATCH_LIMIT;
let done = !status.load(Ordering::Acquire) && !public_keys.is_empty();
// Get metadata for all public keys if the limit is reached
if limit_reached || done {
let public_keys = std::mem::take(&mut public_keys);
// Get metadata for the public keys
Self::get_metadata(client, public_keys).await.ok();
}
match &event.created_at >= initialized_at {
true => {
let new_message = NewMessage::new(event.id, rumor);
let signal = Signal::Message(new_message);
if let Err(e) = tx.send_async(signal).await {
log::error!("Failed to send signal: {}", e);
}
}
false => {
status.store(true, Ordering::Release);
}
}
}
Err(_e) => {
let mut tracker = tracker.write().await;
tracker.failed_unwrap_events.push(event.as_ref().clone());
drop(tracker);
}
}
}
RelayMessage::EndOfStoredEvents(id) => {
if id.as_ref() == &subscription_id {
if let Err(e) = tx.send_async(Signal::Eose).await {
log::error!("Failed to send signal: {}", e);
}
}
}
_ => {}
}
}
}
async fn handle_unwrapping(client: &Client, status: &Arc<AtomicBool>, tx: &Sender<Signal>) {
let loop_duration = Duration::from_secs(20);
let mut is_start_processing = false;
let mut total_loops = 0;
loop {
if client.has_signer().await {
total_loops += 1;
if status.load(Ordering::Acquire) {
is_start_processing = true;
// Reset gift wrap processing flag
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
// Send loading signal
if let Err(e) = tx.send_async(Signal::Loading(true)).await {
log::error!("Failed to send signal: {}", e);
}
} else {
// Only run further if we are already processing
// Wait until after 2 loops to prevent exiting early while events are still being processed
if is_start_processing && total_loops >= 2 {
// Send loading signal
if let Err(e) = tx.send_async(Signal::Loading(false)).await {
log::error!("Failed to send signal: {}", e);
}
// Reset the counter
is_start_processing = false;
total_loops = 0;
}
}
}
smol::Timer::after(loop_duration).await;
}
}
fn retry_failed_events(
&mut self,
signer: &Arc<dyn NostrSigner>,
tx: &Sender<Signal>,
status: &Arc<AtomicBool>,
cx: &mut Context<Self>,
) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let tracker = nostr.read(cx).tracker();
let signer = Arc::clone(signer);
let status = Arc::clone(status);
let tx = tx.clone();
let initialized_at = initialized_at();
self._tasks.push(cx.background_spawn(async move {
let tracker = tracker.read().await;
for event in tracker.failed_unwrap_events.iter() {
if let Ok(rumor) = Self::try_unwrap_custom(&client, &signer, event).await {
match &event.created_at >= initialized_at {
true => {
let new_message = NewMessage::new(event.id, rumor);
let signal = Signal::Message(new_message);
if let Err(e) = tx.send_async(signal).await {
log::error!("Failed to send signal: {}", e);
}
}
false => {
status.store(true, Ordering::Release);
}
}
}
}
}));
}
/// Set the loading status of the chat registry
pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
self.loading = loading;
@@ -147,97 +401,19 @@ impl ChatRegistry {
cx.notify();
}
/// Load all rooms from the database.
pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) {
log::info!("Starting to load chat rooms...");
/// Push a new room to the chat registry
pub fn push_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) {
let id = room.read(cx).id;
// Get the contact bypass setting
let bypass_setting = AppSettings::get_contact_bypass(cx);
if !self.rooms.iter().any(|r| r.read(cx).id == id) {
self.add_room(room, cx);
}
let task: Task<Result<HashSet<Room>, Error>> = cx.background_spawn(async move {
let client = app_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let contacts = client.database().contacts_public_keys(public_key).await?;
let authored_filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.custom_tag(SingleLetterTag::lowercase(Alphabet::A), public_key);
let addressed_filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.custom_tag(SingleLetterTag::lowercase(Alphabet::P), public_key);
let authored = client.database().query(authored_filter).await?;
let addressed = client.database().query(addressed_filter).await?;
let events = authored.merge(addressed);
let mut rooms: HashSet<Room> = HashSet::new();
let mut grouped: HashMap<u64, Vec<UnsignedEvent>> = HashMap::new();
// Process each event and group by room hash
for raw in events.into_iter() {
match UnsignedEvent::from_json(&raw.content) {
Ok(rumor) => {
if rumor.tags.public_keys().peekable().peek().is_some() {
grouped.entry(rumor.uniq_id()).or_default().push(rumor);
}
}
Err(e) => log::warn!("Failed to parse stored rumor: {e}"),
}
}
for (_room_id, mut messages) in grouped.into_iter() {
messages.sort_by_key(|m| Reverse(m.created_at));
let Some(latest) = messages.first() else {
continue;
};
let mut room = Room::from(latest);
if rooms.iter().any(|r| r.id == room.id) {
continue;
}
let mut public_keys: Vec<PublicKey> = room.members().to_vec();
public_keys.retain(|pk| pk != &public_key);
let user_sent = messages.iter().any(|m| m.pubkey == public_key);
let mut bypassed = false;
if bypass_setting {
bypassed = public_keys.iter().any(|k| contacts.contains(k));
}
if user_sent || bypassed {
room = room.kind(RoomKind::Ongoing);
}
rooms.insert(room);
}
Ok(rooms)
});
cx.spawn_in(window, async move |this, cx| {
match task.await {
Ok(rooms) => {
this.update_in(cx, move |this, _window, cx| {
this.extend_rooms(rooms, cx);
this.sort(cx);
})
.ok();
}
Err(e) => {
log::error!("Failed to load rooms: {e}")
}
};
})
.detach();
cx.emit(ChatEvent::OpenRoom(id));
}
pub(crate) fn extend_rooms(&mut self, rooms: HashSet<Room>, cx: &mut Context<Self>) {
/// Extend the registry with new rooms.
fn extend_rooms(&mut self, rooms: HashSet<Room>, cx: &mut Context<Self>) {
let mut room_map: HashMap<u64, usize> = self
.rooms
.iter()
@@ -264,18 +440,111 @@ impl ChatRegistry {
}
}
/// Push a new room to the chat registry
pub fn push_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) {
let id = room.read(cx).id;
/// Load all rooms from the database.
pub fn get_rooms(&mut self, cx: &mut Context<Self>) {
let task = self.create_get_rooms_task(cx);
if !self.rooms.iter().any(|r| r.read(cx).id == id) {
self.add_room(room, cx);
}
cx.emit(ChatEvent::OpenRoom(id));
self._tasks.push(
// Run and finished in the background
cx.spawn(async move |this, cx| {
match task.await {
Ok(rooms) => {
this.update(cx, move |this, cx| {
this.extend_rooms(rooms, cx);
this.sort(cx);
})
.expect("Entity has been released");
}
Err(e) => {
log::error!("Failed to load rooms: {e}")
}
};
}),
);
}
/// Refresh messages for a room in the global registry
/// Create a task to load rooms from the database
fn create_get_rooms_task(&self, cx: &App) -> Task<Result<HashSet<Room>, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Get the contact bypass setting
let bypass_setting = AppSettings::get_contact_bypass(cx);
cx.background_spawn(async move {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let contacts = client.database().contacts_public_keys(public_key).await?;
let authored_filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.custom_tag(SingleLetterTag::lowercase(Alphabet::A), public_key);
// Get all authored events
let authored = client.database().query(authored_filter).await?;
let addressed_filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.custom_tag(SingleLetterTag::lowercase(Alphabet::P), public_key);
// Get all addressed events
let addressed = client.database().query(addressed_filter).await?;
// Merge authored and addressed events
let events = authored.merge(addressed);
let mut rooms: HashSet<Room> = HashSet::new();
let mut grouped: HashMap<u64, Vec<UnsignedEvent>> = HashMap::new();
// Process each event and group by room hash
for raw in events.into_iter() {
if let Ok(rumor) = UnsignedEvent::from_json(&raw.content) {
if rumor.tags.public_keys().peekable().peek().is_some() {
grouped.entry(rumor.uniq_id()).or_default().push(rumor);
}
}
}
for (_id, mut messages) in grouped.into_iter() {
messages.sort_by_key(|m| Reverse(m.created_at));
let Some(latest) = messages.first() else {
continue;
};
let mut room = Room::from(latest);
if rooms.iter().any(|r| r.id == room.id) {
continue;
}
let mut public_keys = room.members();
public_keys.retain(|pk| pk != &public_key);
// Check if the user has responded to the room
let user_sent = messages.iter().any(|m| m.pubkey == public_key);
// Determine if the room is ongoing or not
let mut bypassed = false;
// Check if public keys are from the user's contacts
if bypass_setting {
bypassed = public_keys.iter().any(|k| contacts.contains(k));
}
// Set the room's kind based on status
if user_sent || bypassed {
room = room.kind(RoomKind::Ongoing);
}
rooms.insert(room);
}
Ok(rooms)
})
}
/// Trigger a refresh of the opened chat rooms by their IDs
pub fn refresh_rooms(&mut self, ids: Option<Vec<u64>>, cx: &mut Context<Self>) {
if let Some(ids) = ids {
for room in self.rooms.iter() {
@@ -292,15 +561,15 @@ impl ChatRegistry {
///
/// If the room doesn't exist, it will be created.
/// Updates room ordering based on the most recent messages.
pub fn new_message(&mut self, msg: NewMessage, window: &mut Window, cx: &mut Context<Self>) {
let id = msg.rumor.uniq_id();
let author = msg.rumor.pubkey;
pub fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
let id = message.rumor.uniq_id();
let author = message.rumor.pubkey;
let account = Account::global(cx);
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
let is_new_event = msg.rumor.created_at > room.read(cx).created_at;
let created_at = msg.rumor.created_at;
let event_for_emit = msg.rumor.clone();
let is_new_event = message.rumor.created_at > room.read(cx).created_at;
let created_at = message.rumor.created_at;
let event_for_emit = message.rumor.clone();
// Update room
room.update(cx, |this, cx| {
@@ -314,26 +583,170 @@ impl ChatRegistry {
}
// Emit the new message to the room
let event_to_emit = event_for_emit.clone();
cx.defer_in(window, move |this, _window, cx| {
this.emit_message(msg.gift_wrap, event_to_emit, cx);
});
this.emit_message(message.gift_wrap, event_for_emit.clone(), cx);
});
// Resort all rooms in the registry by their created at (after updated)
if is_new_event {
cx.defer_in(window, |this, _window, cx| {
this.sort(cx);
});
self.sort(cx);
}
} else {
// Push the new room to the front of the list
self.add_room(cx.new(|_| Room::from(&msg.rumor)), cx);
self.add_room(cx.new(|_| Room::from(&message.rumor)), cx);
// Notify the UI about the new room
cx.defer_in(window, move |_this, _window, cx| {
cx.emit(ChatEvent::NewChatRequest(RoomKind::default()));
});
cx.emit(ChatEvent::NewChatRequest(RoomKind::default()));
}
}
// Unwraps a gift-wrapped event and processes its contents.
async fn extract_rumor(client: &Client, gift_wrap: &Event) -> Result<UnsignedEvent, Error> {
// Try to get cached rumor first
if let Ok(event) = Self::get_rumor(client, gift_wrap.id).await {
return Ok(event);
}
// Try to unwrap with the available signer
let unwrapped = Self::try_unwrap(client, gift_wrap).await?;
let mut rumor_unsigned = unwrapped.rumor;
// Generate event id for the rumor if it doesn't have one
rumor_unsigned.ensure_id();
// Cache the rumor
Self::set_rumor(client, gift_wrap.id, &rumor_unsigned).await?;
Ok(rumor_unsigned)
}
// Helper method to try unwrapping with different signers
async fn try_unwrap(client: &Client, gift_wrap: &Event) -> Result<UnwrappedGift, Error> {
let signer = client.signer().await?;
let unwrapped = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await?;
Ok(unwrapped)
}
/// Helper method to try unwrapping with a custom signer
async fn try_unwrap_custom<T>(
client: &Client,
signer: &T,
gift_wrap: &Event,
) -> Result<UnsignedEvent, Error>
where
T: NostrSigner,
{
let unwrapped = UnwrappedGift::from_gift_wrap(signer, gift_wrap).await?;
let mut rumor_unsigned = unwrapped.rumor;
// Generate event id for the rumor if it doesn't have one
rumor_unsigned.ensure_id();
// Cache the rumor
Self::set_rumor(client, gift_wrap.id, &rumor_unsigned).await?;
Ok(rumor_unsigned)
}
/// Stores an unwrapped event in local database with reference to original
async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
let rumor_id = rumor.id.context("Rumor is missing an event id")?;
let author = rumor.pubkey;
let conversation = Self::conversation_id(rumor);
let mut tags = rumor.tags.clone().to_vec();
// Add a unique identifier
tags.push(Tag::identifier(id));
// Add a reference to the rumor's author
tags.push(Tag::custom(
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::A)),
[author],
));
// Add a conversation id
tags.push(Tag::custom(
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::C)),
[conversation.to_string()],
));
// Add a reference to the rumor's id
tags.push(Tag::event(rumor_id));
// Add references to the rumor's participants
for receiver in rumor.tags.public_keys().copied() {
tags.push(Tag::custom(
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)),
[receiver],
));
}
// Convert rumor to json
let content = rumor.as_json();
// Construct the event
let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
.tags(tags)
.sign(&Keys::generate())
.await?;
// Save the event to the database
client.database().save_event(&event).await?;
Ok(())
}
/// Retrieves a previously unwrapped event from local database
async fn get_rumor(client: &Client, gift_wrap: EventId) -> Result<UnsignedEvent, Error> {
let filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.identifier(gift_wrap)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() {
UnsignedEvent::from_json(event.content).map_err(|e| anyhow!(e))
} else {
Err(anyhow!("Event is not cached yet."))
}
}
/// Get metadata for a list of public keys
async fn get_metadata<I>(client: &Client, public_keys: I) -> Result<(), Error>
where
I: IntoIterator<Item = PublicKey>,
{
let authors: Vec<PublicKey> = public_keys.into_iter().collect();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
// Return if the list is empty
if authors.is_empty() {
return Err(anyhow!("You need at least one public key".to_string(),));
}
let filter = Filter::new()
.limit(authors.len() * kinds.len() + 10)
.authors(authors)
.kinds(kinds);
// Subscribe to filters to the bootstrap relays
client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await?;
Ok(())
}
/// Get the conversation ID for a given rumor (message).
fn conversation_id(rumor: &UnsignedEvent) -> u64 {
let mut hasher = DefaultHasher::new();
let mut pubkeys: Vec<PublicKey> = rumor.tags.public_keys().copied().collect();
pubkeys.push(rumor.pubkey);
pubkeys.sort();
pubkeys.dedup();
pubkeys.hash(&mut hasher);
hasher.finish()
}
}

View File

@@ -2,6 +2,18 @@ use std::hash::Hash;
use nostr_sdk::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct NewMessage {
pub gift_wrap: EventId,
pub rumor: UnsignedEvent,
}
impl NewMessage {
pub fn new(gift_wrap: EventId, rumor: UnsignedEvent) -> Self {
Self { gift_wrap, rumor }
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Message {
User(RenderedMessage),

View File

@@ -5,12 +5,14 @@ use std::time::Duration;
use account::Account;
use anyhow::{anyhow, Error};
use common::display::RenderedProfile;
use common::event::EventUtils;
use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task};
use common::{EventUtils, RenderedProfile};
use encryption::{Encryption, SignerKind};
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use states::{app_state, SignerKind, SEND_RETRY};
use state::NostrRegistry;
const SEND_RETRY: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SendOptions {
@@ -159,30 +161,6 @@ impl Eq for Room {}
impl EventEmitter<RoomSignal> for Room {}
impl From<&Event> for Room {
fn from(val: &Event) -> Self {
let id = val.uniq_id();
let created_at = val.created_at;
// Get the members from the event's tags and event's pubkey
let members = val.all_pubkeys();
// Get subject from tags
let subject = val
.tags
.find(TagKind::Subject)
.and_then(|tag| tag.content().map(|s| s.to_owned().into()));
Room {
id,
created_at,
subject,
members,
kind: RoomKind::default(),
}
}
}
impl From<&UnsignedEvent> for Room {
fn from(val: &UnsignedEvent) -> Self {
let id = val.uniq_id();
@@ -209,15 +187,7 @@ impl From<&UnsignedEvent> for Room {
impl Room {
/// Constructs a new room with the given receiver and tags.
pub async fn new(subject: Option<String>, receivers: Vec<PublicKey>) -> Result<Self, Error> {
let client = app_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
if receivers.is_empty() {
return Err(anyhow!("You need to add at least one receiver"));
};
pub fn new(subject: Option<String>, author: PublicKey, receivers: Vec<PublicKey>) -> Self {
// Convert receiver's public keys into tags
let mut tags: Tags = Tags::from_list(
receivers
@@ -235,12 +205,12 @@ impl Room {
let mut event = EventBuilder::new(Kind::PrivateDirectMessage, "")
.tags(tags)
.build(public_key);
.build(author);
// Generate event ID
event.ensure_id();
Ok(Room::from(&event))
Room::from(&event)
}
/// Sets the kind of the room and returns the modified room
@@ -292,11 +262,11 @@ impl Room {
}
/// Gets the display image for the room
pub fn display_image(&self, proxy: bool, cx: &App) -> SharedUri {
pub fn display_image(&self, proxy: bool, cx: &App) -> SharedString {
if !self.is_group() {
self.display_member(cx).avatar(proxy)
} else {
SharedUri::from("brand/group.png")
SharedString::from("brand/group.png")
}
}
@@ -358,10 +328,11 @@ impl Room {
/// Get messaging relays and encryption keys announcement for each member
pub fn connect(&self, cx: &App) -> Task<Result<(), Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let members = self.members();
cx.background_spawn(async move {
let client = app_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
@@ -396,10 +367,11 @@ impl Room {
/// Get all messages belonging to the room
pub fn get_messages(&self, cx: &App) -> Task<Result<Vec<UnsignedEvent>, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let conversation_id = self.id.to_string();
cx.background_spawn(async move {
let client = app_state().client();
let filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.custom_tag(SingleLetterTag::lowercase(Alphabet::C), conversation_id);
@@ -419,10 +391,6 @@ impl Room {
/// Create a new message event (unsigned)
pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent {
// Get the app state
let state = app_state();
let relay_cache = state.relay_cache.read_blocking();
// Get current user
let account = Account::global(cx);
let public_key = account.read(cx).public_key();
@@ -437,9 +405,7 @@ impl Room {
// NOTE: current user will be removed from the list of receivers
for member in self.members.iter() {
// Get relay hint if available
let relay_url = relay_cache
.get(member)
.and_then(|urls| urls.iter().nth(0).cloned());
let relay_url = None;
// Construct a public key tag with relay hint
let tag = TagStandard::PublicKey {
@@ -475,12 +441,12 @@ impl Room {
// Construct a direct message event
//
// WARNING: never send this event to relays
// WARNING: never sign and send this event to relays
let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content)
.tags(tags)
.build(public_key);
// Generate event ID
// Ensure the event id has been generated
event.ensure_id();
event
@@ -493,6 +459,14 @@ impl Room {
opts: &SendOptions,
cx: &App,
) -> Task<Result<Vec<SendReport>, Error>> {
let encryption = Encryption::global(cx);
let encryption_key = encryption.read(cx).encryption_key(cx);
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let cache_manager = nostr.read(cx).cache_manager();
let tracker = nostr.read(cx).tracker();
let rumor = rumor.to_owned();
let opts = opts.to_owned();
@@ -500,16 +474,12 @@ impl Room {
let mut members = self.members();
cx.background_spawn(async move {
let state = app_state();
let client = state.client();
let signer_kind = opts.signer_kind;
let cache = cache_manager.read().await;
let tracker = tracker.read().await;
let relay_cache = state.relay_cache.read().await;
let announcement_cache = state.announcement_cache.read().await;
let encryption = state.device.read().await.encryption.clone();
// Get the encryption public key
let encryption_pubkey = if let Some(signer) = encryption.as_ref() {
let encryption_pubkey = if let Some(signer) = encryption_key.as_ref() {
signer.get_public_key().await.ok()
} else {
None
@@ -523,14 +493,14 @@ impl Room {
members.retain(|&pk| pk != user_pubkey);
// Determine the signer will be used based on the provided options
let signer = Self::select_signer(&opts.signer_kind, user_signer, encryption)?;
let signer = Self::select_signer(&opts.signer_kind, user_signer, encryption_key)?;
// Collect the send reports
let mut reports: Vec<SendReport> = vec![];
for member in members.into_iter() {
// Get user's messaging relays
let urls = relay_cache.get(&member).cloned().unwrap_or_default();
let urls = cache.relay(&member).cloned().unwrap_or_default();
// Check if there are any relays to send the message to
if urls.is_empty() {
@@ -539,8 +509,8 @@ impl Room {
}
// Get user's encryption public key if available
let encryption = announcement_cache
.get(&member)
let encryption = cache
.announcement(&member)
.and_then(|a| a.to_owned().map(|a| a.public_key()));
// Skip sending if using encryption signer but receiver's encryption keys not found
@@ -551,10 +521,16 @@ impl Room {
let receiver = Self::select_receiver(&signer_kind, member, encryption)?;
let rumor = rumor.clone();
let tags = vec![Tag::public_key(member)];
// Construct the sealed event
let seal = EventBuilder::seal(&signer, &receiver, rumor.clone())
.await?
.build(member)
.sign(&signer)
.await?;
// Construct the gift wrap event
let event = EventBuilder::gift_wrap(&signer, &receiver, rumor, tags).await?;
let event = EventBuilder::gift_wrap_from_seal(&member, &seal, vec![])?;
// Send the gift wrap event to the messaging relays
match client.send_event_to(urls, &event).await {
@@ -566,8 +542,7 @@ impl Room {
if auth {
// Wait for authenticated and resent event successfully
for attempt in 0..=SEND_RETRY {
let retry_manager = state.tracker().read().await;
let ids = retry_manager.resent_ids();
let ids = tracker.resent_ids();
// Check if event was successfully resent
if let Some(output) = ids.iter().find(|e| e.id() == &id).cloned() {
@@ -596,14 +571,20 @@ impl Room {
let receiver = Self::select_receiver(&signer_kind, user_pubkey, encryption_pubkey)?;
let rumor = rumor.clone();
let tags = vec![Tag::public_key(user_pubkey)];
// Construct the sealed event
let seal = EventBuilder::seal(&signer, &receiver, rumor.clone())
.await?
.build(user_pubkey)
.sign(&signer)
.await?;
// Construct the gift-wrapped event
let event = EventBuilder::gift_wrap(&signer, &receiver, rumor, tags).await?;
let event = EventBuilder::gift_wrap_from_seal(&receiver, &seal, vec![])?;
// Only send a backup message to current user if sent successfully to others
if opts.backup() && reports.iter().all(|r| r.is_sent_success()) {
let urls = relay_cache.get(&user_pubkey).cloned().unwrap_or_default();
let urls = cache.relay(&user_pubkey).cloned().unwrap_or_default();
// Check if there are any relays to send the event to
if urls.is_empty() {
@@ -633,9 +614,12 @@ impl Room {
reports: Vec<SendReport>,
cx: &App,
) -> Task<Result<Vec<SendReport>, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let cache_manager = nostr.read(cx).cache_manager();
cx.background_spawn(async move {
let states = app_state();
let client = states.client();
let cache = cache_manager.read().await;
let mut resend_reports = vec![];
for report in reports.into_iter() {
@@ -664,7 +648,7 @@ impl Room {
// Process the on hold event if it exists
if let Some(event) = report.on_hold {
let urls = states.messaging_relays(receiver).await;
let urls = cache.relay(&receiver).cloned().unwrap_or_default();
// Check if there are any relays to send the event to
if urls.is_empty() {
@@ -702,15 +686,15 @@ impl Room {
fn select_receiver(
kind: &SignerKind,
user: PublicKey,
members: PublicKey,
encryption: Option<PublicKey>,
) -> Result<PublicKey, Error> {
match kind {
SignerKind::Encryption => {
Ok(encryption.ok_or_else(|| anyhow!("Receiver's encryption key not found"))?)
}
SignerKind::User => Ok(user),
SignerKind::Auto => Ok(encryption.unwrap_or(user)),
SignerKind::User => Ok(members),
SignerKind::Auto => Ok(encryption.unwrap_or(members)),
}
}
}

View File

@@ -5,11 +5,12 @@ edition.workspace = true
publish.workspace = true
[dependencies]
state = { path = "../state" }
ui = { path = "../ui" }
theme = { path = "../theme" }
common = { path = "../common" }
states = { path = "../states" }
account = { path = "../account" }
encryption = { path = "../encryption" }
person = { path = "../person" }
chat = { path = "../chat" }
settings = { path = "../settings" }
@@ -17,7 +18,6 @@ settings = { path = "../settings" }
gpui.workspace = true
gpui_tokio.workspace = true
nostr.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
itertools.workspace = true

View File

@@ -1,7 +1,7 @@
use encryption::SignerKind;
use gpui::Action;
use nostr_sdk::prelude::*;
use serde::Deserialize;
use states::SignerKind;
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = chat, no_json)]

View File

@@ -2,16 +2,15 @@ use std::collections::HashSet;
use std::time::Duration;
pub use actions::*;
use chat::message::{Message, RenderedMessage};
use chat::room::{Room, RoomKind, RoomSignal, SendOptions, SendReport};
use common::display::{RenderedProfile, RenderedTimestamp};
use common::nip96::nip96_upload;
use chat::{Message, RenderedMessage, Room, RoomKind, RoomSignal, SendOptions, SendReport};
use common::{nip96_upload, RenderedProfile, RenderedTimestamp};
use encryption::SignerKind;
use gpui::prelude::FluentBuilder;
use gpui::{
div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext,
ClipboardItem, Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable,
InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit,
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString, SharedUri,
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString,
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, Window,
};
use gpui_tokio::Tokio;
@@ -22,7 +21,7 @@ use person::PersonRegistry;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use smol::fs;
use states::{app_state, SignerKind};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
@@ -147,16 +146,16 @@ impl ChatPanel {
cx.subscribe_in(&room, window, move |this, _, signal, window, cx| {
match signal {
RoomSignal::NewMessage((gift_wrap_id, event)) => {
let nostr = NostrRegistry::global(cx);
let tracker = nostr.read(cx).tracker();
let gift_wrap_id = gift_wrap_id.to_owned();
let message = Message::user(event.clone());
cx.spawn_in(window, async move |this, cx| {
let states = app_state();
let event_tracker = states.tracker().read().await;
let sent_ids = event_tracker.sent_ids();
let tracker = tracker.read().await;
this.update_in(cx, |this, _window, cx| {
if !sent_ids.contains(&gift_wrap_id) {
if !tracker.sent_ids().contains(&gift_wrap_id) {
this.insert_message(message, false, cx);
}
})
@@ -488,6 +487,9 @@ impl ChatPanel {
}
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Get the user's configured NIP96 server
let nip96_server = AppSettings::get_media_server(cx);
@@ -503,9 +505,8 @@ impl ChatPanel {
let path = paths.pop()?;
let upload = Tokio::spawn(cx, async move {
let client = app_state().client();
let file = fs::read(path).await.ok()?;
let url = nip96_upload(client, &nip96_server, file).await.ok()?;
let url = nip96_upload(&client, &nip96_server, file).await.ok()?;
Some(url)
});
@@ -1071,7 +1072,7 @@ impl ChatPanel {
.relative()
.w_16()
.child(
img(SharedUri::from(url.to_string()))
img(url.as_str())
.size_16()
.shadow_lg()
.rounded(cx.theme().radius)
@@ -1238,11 +1239,12 @@ impl ChatPanel {
fn on_open_seen_on(&mut self, ev: &SeenOn, window: &mut Window, cx: &mut Context<Self>) {
let id = ev.0;
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let tracker = nostr.read(cx).tracker();
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
let states = app_state();
let client = states.client();
let event_tracker = states.tracker().read().await;
let tracker = tracker.read().await;
let mut relays: Vec<RelayUrl> = vec![];
let filter = Filter::new()
@@ -1252,7 +1254,7 @@ impl ChatPanel {
if let Some(event) = client.database().query(filter).await?.first_owned() {
if let Some(Ok(id)) = event.tags.identifier().map(EventId::parse) {
if let Some(urls) = event_tracker.seen_on_relays.get(&id).cloned() {
if let Some(urls) = tracker.seen_on_relays.get(&id).cloned() {
relays.extend(urls);
}
}

View File

@@ -1,7 +1,7 @@
use std::ops::Range;
use std::sync::Arc;
use common::display::RenderedProfile;
use common::RenderedProfile;
use gpui::{
AnyElement, App, ElementId, HighlightStyle, InteractiveText, IntoElement, SharedString,
StyledText, UnderlineStyle, Window,
@@ -129,8 +129,6 @@ fn render_plain_text_mut(
let range = link.start()..link.end();
let url = link.as_str().to_string();
log::info!("Found URL: {}", url);
url_matches.push((range, url));
}

View File

@@ -5,12 +5,9 @@ edition.workspace = true
publish.workspace = true
[dependencies]
states = { path = "../states" }
gpui.workspace = true
nostr-connect.workspace = true
nostr-sdk.workspace = true
nostr.workspace = true
anyhow.workspace = true
itertools.workspace = true
chrono.workspace = true
@@ -19,6 +16,8 @@ smol.workspace = true
futures.workspace = true
reqwest.workspace = true
log.workspace = true
webbrowser.workspace = true
dirs = "5.0"
qrcode = "0.14.1"
whoami = "1.6.1"
nostr = { git = "https://github.com/rust-nostr/nostr" }

View File

@@ -1,9 +1,6 @@
pub const CLIENT_NAME: &str = "Coop";
pub const APP_ID: &str = "su.reya.coop";
pub const SETTINGS_IDENTIFIER: &str = "coop:settings";
pub const INBOX_SUB_ID: &str = "inbox";
/// Bootstrap Relays.
pub const BOOTSTRAP_RELAYS: [&str; 5] = [
"wss://relay.damus.io",
@@ -31,14 +28,8 @@ pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
/// Default timeout (in seconds) for Nostr Connect (Bunker)
pub const BUNKER_TIMEOUT: u64 = 30;
/// Default timeout (in seconds) for fetching events
pub const QUERY_TIMEOUT: u64 = 3;
/// Total metadata requests will be grouped.
pub const METADATA_BATCH_LIMIT: usize = 100;
/// Maximum timeout for grouping metadata requests. (milliseconds)
pub const METADATA_BATCH_TIMEOUT: u64 = 300;
pub const METADATA_BATCH_LIMIT: usize = 20;
/// Default width of the sidebar.
pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.;

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use anyhow::{anyhow, Error};
use chrono::{Local, TimeZone};
use gpui::{Image, ImageFormat, SharedString, SharedUri};
use gpui::{Image, ImageFormat, SharedString};
use nostr_sdk::prelude::*;
use qrcode::render::svg;
use qrcode::QrCode;
@@ -16,12 +16,12 @@ const FALLBACK_IMG: &str = "https://image.nostr.build/c30703b48f511c293a9003be81
const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";
pub trait RenderedProfile {
fn avatar(&self, proxy: bool) -> SharedUri;
fn avatar(&self, proxy: bool) -> SharedString;
fn display_name(&self) -> SharedString;
}
impl RenderedProfile for Profile {
fn avatar(&self, proxy: bool) -> SharedUri {
fn avatar(&self, proxy: bool) -> SharedString {
self.metadata()
.picture
.as_ref()
@@ -32,12 +32,12 @@ impl RenderedProfile for Profile {
"{IMAGE_RESIZE_SERVICE}/?url={picture}&w=100&h=100&fit=cover&mask=circle&default={FALLBACK_IMG}&n=-1"
);
SharedUri::from(url)
url.into()
} else {
SharedUri::from(picture)
picture.into()
}
})
.unwrap_or_else(|| SharedUri::from("brand/avatar.png"))
.unwrap_or_else(|| "brand/avatar.png".into())
}
fn display_name(&self) -> SharedString {

View File

@@ -48,7 +48,6 @@ impl EventUtils for UnsignedEvent {
fn all_pubkeys(&self) -> Vec<PublicKey> {
let mut public_keys: Vec<PublicKey> = self.tags.public_keys().copied().collect();
public_keys.push(self.pubkey);
public_keys
public_keys.into_iter().unique().sorted().collect()
}
}

View File

@@ -1,5 +1,68 @@
pub mod debounced_delay;
pub mod display;
pub mod event;
pub mod nip05;
pub mod nip96;
use std::sync::OnceLock;
pub use constants::*;
pub use debounced_delay::*;
pub use display::*;
pub use event::*;
pub use nip05::*;
pub use nip96::*;
use nostr_sdk::prelude::*;
pub use paths::*;
mod constants;
mod debounced_delay;
mod display;
mod event;
mod nip05;
mod nip96;
mod paths;
static APP_NAME: OnceLock<String> = OnceLock::new();
static NIP65_RELAYS: OnceLock<Vec<(RelayUrl, Option<RelayMetadata>)>> = OnceLock::new();
static NIP17_RELAYS: OnceLock<Vec<RelayUrl>> = OnceLock::new();
/// Get the app name
pub fn app_name() -> &'static String {
APP_NAME.get_or_init(|| {
let devicename = whoami::devicename();
let platform = whoami::platform();
format!("{CLIENT_NAME} on {platform} ({devicename})")
})
}
/// Default NIP-65 Relays. Used for new account
pub fn default_nip65_relays() -> &'static Vec<(RelayUrl, Option<RelayMetadata>)> {
NIP65_RELAYS.get_or_init(|| {
vec![
(
RelayUrl::parse("wss://nostr.mom").unwrap(),
Some(RelayMetadata::Read),
),
(
RelayUrl::parse("wss://nostr.bitcoiner.social").unwrap(),
Some(RelayMetadata::Read),
),
(
RelayUrl::parse("wss://nostr.oxtr.dev").unwrap(),
Some(RelayMetadata::Write),
),
(
RelayUrl::parse("wss://nostr.fmt.wiz.biz").unwrap(),
Some(RelayMetadata::Write),
),
(RelayUrl::parse("wss://relay.primal.net").unwrap(), None),
(RelayUrl::parse("wss://relay.damus.io").unwrap(), None),
]
})
}
/// Default NIP-17 Relays. Used for new account
pub fn default_nip17_relays() -> &'static Vec<RelayUrl> {
NIP17_RELAYS.get_or_init(|| {
vec![
RelayUrl::parse("wss://nip17.com").unwrap(),
RelayUrl::parse("wss://auth.nostr1.com").unwrap(),
]
})
}

View File

@@ -32,14 +32,17 @@ ui = { path = "../ui" }
title_bar = { path = "../title_bar" }
theme = { path = "../theme" }
common = { path = "../common" }
states = { path = "../states" }
state = { path = "../state" }
key_store = { path = "../key_store" }
chat = { path = "../chat" }
chat_ui = { path = "../chat_ui" }
settings = { path = "../settings" }
auto_update = { path = "../auto_update" }
account = { path = "../account" }
encryption = { path = "../encryption" }
encryption_ui = { path = "../encryption_ui" }
person = { path = "../person" }
relay_auth = { path = "../relay_auth" }
rust-i18n.workspace = true
i18n.workspace = true
@@ -49,19 +52,16 @@ reqwest_client.workspace = true
nostr-connect.workspace = true
nostr-sdk.workspace = true
nostr.workspace = true
anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
itertools.workspace = true
dirs.workspace = true
log.workspace = true
smallvec.workspace = true
smol.workspace = true
futures.workspace = true
oneshot.workspace = true
flume.workspace = true
webbrowser.workspace = true
indexset = "0.12.3"

View File

@@ -1,12 +1,11 @@
use std::sync::Mutex;
use gpui::{actions, App};
use key_store::backend::KeyItem;
use key_store::KeyStore;
use key_store::{KeyItem, KeyStore};
use nostr_connect::prelude::*;
use states::app_state;
use state::NostrRegistry;
actions!(coop, [ReloadMetadata, DarkMode, Settings, Logout, Quit]);
actions!(coop, [KeyringPopup, DarkMode, Settings, Logout, Quit]);
actions!(sidebar, [Reload, RelayStatus]);
#[derive(Debug, Clone)]
@@ -49,10 +48,9 @@ pub fn load_embedded_fonts(cx: &App) {
pub fn reset(cx: &mut App) {
let backend = KeyStore::global(cx).read(cx).backend();
let client = NostrRegistry::global(cx).read(cx).client();
cx.spawn(async move |cx| {
let client = app_state().client();
// Remove the signer
client.unset_signer().await;

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
use std::sync::Arc;
use assets::Assets;
use common::{APP_ID, CLIENT_NAME};
use gpui::{
point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString,
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
WindowOptions,
};
use states::{app_state, APP_ID, BOOTSTRAP_RELAYS, CLIENT_NAME, SEARCH_RELAYS};
use ui::Root;
use crate::actions::{load_embedded_fonts, quit, Quit};
@@ -26,29 +26,6 @@ fn main() {
.with_assets(Assets)
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
// Initialize app state
let app_state = app_state();
// Connect to relays
app.background_executor()
.spawn(async move {
let client = app_state.client();
// Get all bootstrapping relays
let mut urls = vec![];
urls.extend(BOOTSTRAP_RELAYS);
urls.extend(SEARCH_RELAYS);
// Add relay to the relay pool
for url in urls.into_iter() {
client.add_relay(url).await.ok();
}
// Establish connection to relays
client.connect().await;
})
.detach();
// Run application
app.run(move |cx| {
// Load embedded fonts in assets/fonts
@@ -102,18 +79,30 @@ fn main() {
// Initialize components
ui::init(cx);
// Initialize app registry
chat::init(cx);
// Initialize backend for keys storage
key_store::init(cx);
// Initialize the nostr client
state::init(cx);
// Initialize person registry
person::init(cx);
// Initialize backend for keys storage
key_store::init(cx);
// Initialize settings
settings::init(cx);
// Initialize account state
account::init(cx);
// Initialize encryption state
encryption::init(cx);
// Initialize app registry
chat::init(cx);
// Initialize relay auth registry
relay_auth::init(window, cx);
// Initialize auto update
auto_update::init(cx);

View File

@@ -1,7 +1,7 @@
use std::fs;
use std::time::Duration;
use dirs::document_dir;
use common::home_dir;
use gpui::{
div, AppContext, ClipboardItem, Context, Entity, Flatten, IntoElement, ParentElement, Render,
SharedString, Styled, Task, Window,
@@ -46,9 +46,8 @@ impl BackupKeys {
}
pub fn backup(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Task<()>> {
let document_dir = document_dir().expect("Failed to get document directory");
let path = cx.prompt_for_new_path(&document_dir, Some("My Nostr Account"));
let dir = home_dir();
let path = cx.prompt_for_new_path(dir, Some("My Nostr Account"));
let nsec = self.secret_input.read(cx).value().to_string();
Some(cx.spawn_in(window, async move |this, cx| {

View File

@@ -1,11 +1,10 @@
use std::ops::Range;
use std::time::Duration;
use account::Account;
use anyhow::{anyhow, Error};
use chat::room::Room;
use chat::ChatRegistry;
use common::display::{RenderedProfile, TextUtils};
use common::nip05::nip05_profile;
use chat::{ChatRegistry, Room};
use common::{nip05_profile, RenderedProfile, TextUtils, BOOTSTRAP_RELAYS};
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement,
@@ -18,7 +17,7 @@ use nostr_sdk::prelude::*;
use person::PersonRegistry;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use states::{app_state, BOOTSTRAP_RELAYS};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
@@ -115,7 +114,10 @@ pub struct Compose {
}
impl Compose {
pub fn new(window: &mut Window, cx: &mut Context<'_, Self>) -> Self {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let contacts = cx.new(|_| vec![]);
let error_message = cx.new(|_| None);
@@ -129,7 +131,6 @@ impl Compose {
let mut tasks = smallvec![];
let get_contacts: Task<Result<Vec<Contact>, Error>> = cx.background_spawn(async move {
let client = app_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let profiles = client.database().contacts(public_key).await?;
@@ -194,10 +195,7 @@ impl Compose {
}
}
async fn request_metadata(public_key: PublicKey) -> Result<(), Error> {
let states = app_state();
let client = states.client();
async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> {
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
@@ -220,11 +218,13 @@ impl Compose {
}
fn push_contact(&mut self, contact: Contact, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let pk = contact.public_key;
if !self.contacts.read(cx).iter().any(|c| c.public_key == pk) {
self._tasks.push(cx.background_spawn(async move {
Self::request_metadata(pk).await.ok();
Self::request_metadata(&client, pk).await.ok();
}));
cx.defer_in(window, |this, window, cx| {
@@ -313,6 +313,10 @@ impl Compose {
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let chat = ChatRegistry::global(cx);
let account = Account::global(cx);
let public_key = account.read(cx).public_key();
let receivers: Vec<PublicKey> = self.selected(cx);
let subject_input = self.title_input.read(cx).value();
let subject = (!subject_input.is_empty()).then(|| subject_input.to_string());
@@ -322,25 +326,11 @@ impl Compose {
return;
};
cx.spawn_in(window, async move |this, cx| {
let result = Room::new(subject, receivers).await;
chat.update(cx, |this, cx| {
this.push_room(cx.new(|_| Room::new(subject, public_key, receivers)), cx);
});
this.update_in(cx, |this, window, cx| {
match result {
Ok(room) => {
chat.update(cx, |this, cx| {
this.push_room(cx.new(|_| room), cx);
});
window.close_modal(cx);
}
Err(e) => {
this.set_error(e.to_string(), cx);
}
};
})
.ok();
})
.detach();
window.close_modal(cx);
}
fn set_error(&mut self, error: impl Into<SharedString>, cx: &mut Context<Self>) {

View File

@@ -2,7 +2,7 @@ use std::str::FromStr;
use std::time::Duration;
use anyhow::Error;
use common::nip96::nip96_upload;
use common::nip96_upload;
use gpui::prelude::FluentBuilder;
use gpui::{
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
@@ -12,7 +12,7 @@ use i18n::{shared_t, t};
use nostr_sdk::prelude::*;
use settings::AppSettings;
use smol::fs;
use states::app_state;
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::input::{InputState, TextInput};
@@ -34,12 +34,18 @@ pub struct EditProfile {
impl EditProfile {
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let name_input =
cx.new(|cx| InputState::new(window, cx).placeholder(t!("profile.placeholder_name")));
let avatar_input =
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg"));
let website_input =
cx.new(|cx| InputState::new(window, cx).placeholder("https://your-website.com"));
let bio_input = cx.new(|cx| {
InputState::new(window, cx)
.multi_line()
@@ -58,7 +64,6 @@ impl EditProfile {
};
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
let client = app_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let metadata = client
@@ -104,8 +109,12 @@ impl EditProfile {
}
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let nip96 = AppSettings::get_media_server(cx);
let avatar_input = self.avatar_input.downgrade();
let paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: false,
@@ -125,9 +134,7 @@ impl EditProfile {
let (tx, rx) = oneshot::channel::<Url>();
nostr_sdk::async_utility::task::spawn(async move {
if let Ok(url) =
nip96_upload(app_state().client(), &nip96, file_data).await
{
if let Ok(url) = nip96_upload(&client, &nip96, file_data).await {
_ = tx.send(url);
}
});
@@ -168,6 +175,9 @@ impl EditProfile {
}
pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Profile, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let avatar = self.avatar_input.read(cx).value().to_string();
let name = self.name_input.read(cx).value().to_string();
let bio = self.bio_input.read(cx).value().to_string();
@@ -190,7 +200,6 @@ impl EditProfile {
}
cx.background_spawn(async move {
let client = app_state().client();
let signer = client.signer().await?;
// Sign the new metadata event

View File

@@ -1,17 +1,17 @@
use std::time::Duration;
use anyhow::anyhow;
use common::BUNKER_TIMEOUT;
use gpui::prelude::FluentBuilder;
use gpui::{
div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
};
use i18n::{shared_t, t};
use key_store::backend::KeyItem;
use key_store::KeyStore;
use key_store::{KeyItem, KeyStore};
use nostr_connect::prelude::*;
use smallvec::{smallvec, SmallVec};
use states::{app_state, BUNKER_TIMEOUT};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
@@ -213,8 +213,10 @@ impl Login {
}
fn connect(&mut self, signer: NostrConnect, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
cx.background_spawn(async move {
let client = app_state().client();
client.set_signer(signer).await;
})
.detach();
@@ -262,6 +264,10 @@ impl Login {
pub fn login_with_keys(&mut self, keys: Keys, cx: &mut Context<Self>) {
let keystore = KeyStore::global(cx).read(cx).backend();
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let username = keys.public_key().to_hex();
let secret = keys.secret_key().to_secret_hex().into_bytes();
@@ -281,7 +287,6 @@ impl Login {
// Update the signer
cx.background_spawn(async move {
let client = app_state().client();
client.set_signer(keys).await;
})
.detach();

View File

@@ -1,4 +1,3 @@
pub mod account;
pub mod backup_keys;
pub mod compose;
pub mod edit_profile;
@@ -9,5 +8,6 @@ pub mod preferences;
pub mod screening;
pub mod setup_relay;
pub mod sidebar;
pub mod startup;
pub mod user_profile;
pub mod welcome;

View File

@@ -1,5 +1,5 @@
use anyhow::{anyhow, Error};
use common::nip96::nip96_upload;
use common::{default_nip17_relays, default_nip65_relays, nip96_upload, BOOTSTRAP_RELAYS};
use gpui::{
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
@@ -7,12 +7,11 @@ use gpui::{
};
use gpui_tokio::Tokio;
use i18n::{shared_t, t};
use key_store::backend::KeyItem;
use key_store::KeyStore;
use key_store::{KeyItem, KeyStore};
use nostr_sdk::prelude::*;
use settings::AppSettings;
use smol::fs;
use states::{app_state, default_nip17_relays, default_nip65_relays, BOOTSTRAP_RELAYS};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
@@ -106,6 +105,9 @@ impl NewAccount {
pub fn set_signer(&mut self, cx: &mut Context<Self>) {
let keystore = KeyStore::global(cx).read(cx).backend();
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let keys = self.temp_keys.read(cx).clone();
let username = keys.public_key().to_hex();
let secret = keys.secret_key().to_secret_hex().into_bytes();
@@ -130,8 +132,6 @@ impl NewAccount {
// Update the signer
// Set the client's signer with the current keys
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = app_state().client();
// Set the client's signer with the current keys
client.set_signer(keys).await;
@@ -178,6 +178,9 @@ impl NewAccount {
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.uploading(true, cx);
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Get the user's configured NIP96 server
let nip96_server = AppSettings::get_media_server(cx);
@@ -194,7 +197,7 @@ impl NewAccount {
Ok(Some(mut paths)) => {
if let Some(path) = paths.pop() {
let file = fs::read(path).await?;
let url = nip96_upload(app_state().client(), &nip96_server, file).await?;
let url = nip96_upload(&client, &nip96_server, file).await?;
Ok(url)
} else {

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use std::time::Duration;
use common::display::TextUtils;
use common::{TextUtils, CLIENT_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
use gpui::prelude::FluentBuilder;
use gpui::{
div, img, px, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
@@ -9,11 +9,10 @@ use gpui::{
SharedString, StatefulInteractiveElement, Styled, Task, Window,
};
use i18n::{shared_t, t};
use key_store::backend::KeyItem;
use key_store::KeyStore;
use key_store::{KeyItem, KeyStore};
use nostr_connect::prelude::*;
use smallvec::{smallvec, SmallVec};
use states::{app_state, CLIENT_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
@@ -165,8 +164,10 @@ impl Onboarding {
}
fn connect(&mut self, signer: NostrConnect, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
cx.background_spawn(async move {
let client = app_state().client();
client.set_signer(signer).await;
})
.detach();
@@ -223,7 +224,7 @@ impl Focusable for Onboarding {
}
impl Render for Onboarding {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.size_full()
.child(

View File

@@ -1,5 +1,5 @@
use account::Account;
use common::display::RenderedProfile;
use common::RenderedProfile;
use gpui::http_client::Url;
use gpui::{
div, px, relative, rems, App, AppContext, Context, Entity, InteractiveElement, IntoElement,

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use std::time::Duration;
use common::display::{shorten_pubkey, RenderedProfile, RenderedTimestamp};
use common::nip05::nip05_verify;
use common::{nip05_verify, shorten_pubkey, RenderedProfile, RenderedTimestamp, BOOTSTRAP_RELAYS};
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity,
@@ -13,7 +13,7 @@ use nostr_sdk::prelude::*;
use person::PersonRegistry;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use states::{app_state, BOOTSTRAP_RELAYS};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
@@ -35,14 +35,17 @@ pub struct Screening {
impl Screening {
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get_person(&public_key, cx);
let mut tasks = smallvec![];
let contact_check: Task<Result<(bool, Vec<Profile>), Error>> =
cx.background_spawn(async move {
let client = app_state().client();
let contact_check: Task<Result<(bool, Vec<Profile>), Error>> = cx.background_spawn({
let client = Arc::clone(&client);
async move {
let signer = client.signer().await?;
let signer_pubkey = signer.get_public_key().await?;
@@ -64,10 +67,10 @@ impl Screening {
}
Ok((followed, mutual_contacts))
});
}
});
let activity_check = cx.background_spawn(async move {
let client = app_state().client();
let filter = Filter::new().author(public_key).limit(1);
let mut activity: Option<Timestamp> = None;
@@ -153,12 +156,12 @@ impl Screening {
}
fn report(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = self.profile.public_key();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = app_state().client();
let signer = client.signer().await?;
let tag = Tag::public_key_report(public_key, Report::Impersonation);
let event = EventBuilder::report(vec![tag], "").sign(&signer).await?;

View File

@@ -4,15 +4,14 @@ use std::time::Duration;
use anyhow::{anyhow, Error};
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, uniform_list, App, AppContext, AsyncWindowContext, Context, Entity,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, TextAlign, UniformList, Window,
div, px, uniform_list, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
ParentElement, Render, SharedString, Styled, Subscription, Task, TextAlign, UniformList,
Window,
};
use i18n::{shared_t, t};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use states::app_state;
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::input::{InputEvent, InputState, TextInput};
@@ -39,6 +38,9 @@ pub struct SetupRelay {
impl SetupRelay {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
let mut subscriptions = smallvec![];
@@ -47,7 +49,11 @@ impl SetupRelay {
tasks.push(
// Load user's relays in the local database
cx.spawn_in(window, async move |this, cx| {
if let Ok(relays) = Self::load(cx).await {
let result = cx
.background_spawn(async move { Self::load(&client).await })
.await;
if let Ok(relays) = result {
this.update(cx, |this, cx| {
this.relays.extend(relays);
cx.notify();
@@ -79,24 +85,21 @@ impl SetupRelay {
}
}
fn load(cx: &AsyncWindowContext) -> Task<Result<Vec<RelayUrl>, Error>> {
cx.background_spawn(async move {
let client = app_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
async fn load(client: &Client) -> Result<Vec<RelayUrl>, Error> {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() {
let urls = nip17::extract_owned_relay_list(event).collect();
Ok(urls)
} else {
Err(anyhow!("Not found."))
}
})
if let Some(event) = client.database().query(filter).await?.first_owned() {
let urls = nip17::extract_owned_relay_list(event).collect();
Ok(urls)
} else {
Err(anyhow!("Not found."))
}
}
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -150,13 +153,12 @@ impl SetupRelay {
return;
};
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let relays = self.relays.clone();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let states = app_state();
let client = states.client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let tags: Vec<Tag> = relays
.iter()
@@ -177,12 +179,6 @@ impl SetupRelay {
client.connect_relay(relay).await.ok();
}
// Fetch gift wrap events
states
.get_messages(public_key, &relays.into_iter().collect_vec())
.await
.ok();
Ok(())
});

View File

@@ -1,12 +1,11 @@
use std::rc::Rc;
use chat::room::RoomKind;
use chat::ChatRegistry;
use chat::{ChatRegistry, RoomKind};
use chat_ui::{CopyPublicKey, OpenPublicKey};
use gpui::prelude::FluentBuilder;
use gpui::{
div, rems, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce,
SharedString, SharedUri, StatefulInteractiveElement, Styled, Window,
SharedString, StatefulInteractiveElement, Styled, Window,
};
use i18n::t;
use nostr_sdk::prelude::*;
@@ -26,7 +25,7 @@ pub struct RoomListItem {
room_id: Option<u64>,
public_key: Option<PublicKey>,
name: Option<SharedString>,
avatar: Option<SharedUri>,
avatar: Option<SharedString>,
created_at: Option<SharedString>,
kind: Option<RoomKind>,
#[allow(clippy::type_complexity)]
@@ -62,7 +61,7 @@ impl RoomListItem {
self
}
pub fn avatar(mut self, avatar: impl Into<SharedUri>) -> Self {
pub fn avatar(mut self, avatar: impl Into<SharedString>) -> Self {
self.avatar = Some(avatar.into());
self
}

View File

@@ -3,10 +3,8 @@ use std::ops::Range;
use std::time::Duration;
use anyhow::{anyhow, Error};
use chat::room::{Room, RoomKind};
use chat::{ChatEvent, ChatRegistry};
use common::debounced_delay::DebouncedDelay;
use common::display::{RenderedTimestamp, TextUtils};
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use gpui::prelude::FluentBuilder;
use gpui::{
deferred, div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity,
@@ -20,7 +18,7 @@ use list_item::RoomListItem;
use nostr_sdk::prelude::*;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use states::{app_state, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
@@ -137,8 +135,7 @@ impl Sidebar {
}
}
async fn request_metadata(public_key: PublicKey) -> Result<(), Error> {
let client = app_state().client();
async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> {
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
@@ -152,18 +149,7 @@ impl Sidebar {
Ok(())
}
async fn create_temp_room(receiver: PublicKey) -> Result<Room, Error> {
// Request to get user's metadata
Self::request_metadata(receiver).await?;
// Create a temporary room
let room = Room::new(None, vec![receiver]).await?;
Ok(room)
}
async fn nip50(query: &str) -> Result<BTreeSet<Room>, Error> {
let client = app_state().client();
async fn nip50(client: &Client, query: &str) -> Result<BTreeSet<Room>, Error> {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
@@ -186,10 +172,13 @@ impl Sidebar {
continue;
}
// Return a temporary room
if let Ok(room) = Self::create_temp_room(event.pubkey).await {
rooms.insert(room);
}
// Request metadata event's author
Self::request_metadata(client, event.pubkey).await?;
// Construct room
let room = Room::new(None, public_key, vec![event.pubkey]);
rooms.insert(room);
}
}
@@ -212,11 +201,13 @@ impl Sidebar {
window: &mut Window,
cx: &mut Context<Self>,
) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let query = query.to_owned();
let query_cloned = query.clone();
let task = smol::future::or(
Tokio::spawn(cx, async move { Self::nip50(&query).await.ok() }),
Tokio::spawn(cx, async move { Self::nip50(&client, &query).await.ok() }),
Tokio::spawn(cx, async move {
let _ = rx.recv().await.is_ok();
None
@@ -263,13 +254,20 @@ impl Sidebar {
}
fn search_by_nip05(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let address = query.to_owned();
let task = Tokio::spawn(cx, async move {
if let Ok(profile) = common::nip05::nip05_profile(&address).await {
Self::create_temp_room(profile.public_key).await
} else {
Err(anyhow!(t!("sidebar.addr_error")))
match common::nip05_profile(&address).await {
Ok(profile) => {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let room = Room::new(None, public_key, vec![profile.public_key]);
Ok(room)
}
Err(e) => Err(anyhow!(e)),
}
});
@@ -310,6 +308,9 @@ impl Sidebar {
}
fn search_by_pubkey(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let Ok(public_key) = query.to_public_key() else {
window.push_notification(t!("common.pubkey_invalid"), cx);
self.set_finding(false, window, cx);
@@ -317,8 +318,13 @@ impl Sidebar {
};
let task: Task<Result<Room, Error>> = cx.background_spawn(async move {
// Create a gift wrap event to represent as room
Self::create_temp_room(public_key).await
let signer = client.signer().await?;
let author = signer.get_public_key().await?;
let room = Room::new(None, author, vec![public_key]);
Self::request_metadata(&client, public_key).await?;
Ok(room)
});
cx.spawn_in(window, async move |this, cx| {
@@ -521,14 +527,16 @@ impl Sidebar {
fn on_reload(&mut self, _ev: &Reload, window: &mut Window, cx: &mut Context<Self>) {
ChatRegistry::global(cx).update(cx, |this, cx| {
this.load_rooms(window, cx);
this.get_rooms(cx);
});
window.push_notification(t!("common.refreshed"), cx);
}
fn on_manage(&mut self, _ev: &RelayStatus, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let task: Task<Result<Vec<Relay>, Error>> = cx.background_spawn(async move {
let client = app_state().client();
let subscription = client.subscription(&SubscriptionId::new("inbox")).await;
let mut relays: Vec<Relay> = vec![];

View File

@@ -1,6 +1,6 @@
use std::time::Duration;
use common::display::RenderedProfile;
use common::{RenderedProfile, BUNKER_TIMEOUT};
use gpui::prelude::FluentBuilder;
use gpui::{
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
@@ -9,12 +9,11 @@ use gpui::{
Window,
};
use i18n::{shared_t, t};
use key_store::backend::KeyItem;
use key_store::KeyStore;
use key_store::{Credential, KeyItem, KeyStore};
use nostr_connect::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec};
use states::{app_state, BUNKER_TIMEOUT};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
@@ -24,18 +23,14 @@ use ui::{h_flex, v_flex, ContextModal, Sizable, StyledExt};
use crate::actions::{reset, CoopAuthUrlHandler};
pub fn init(
public_key: PublicKey,
secret: String,
window: &mut Window,
cx: &mut App,
) -> Entity<Account> {
cx.new(|cx| Account::new(public_key, secret, window, cx))
pub fn init(cre: Credential, window: &mut Window, cx: &mut App) -> Entity<Startup> {
cx.new(|cx| Startup::new(cre, window, cx))
}
pub struct Account {
public_key: PublicKey,
secret: String,
/// Startup
#[derive(Debug)]
pub struct Startup {
credential: Credential,
loading: bool,
name: SharedString,
@@ -49,13 +44,8 @@ pub struct Account {
_tasks: SmallVec<[Task<()>; 1]>,
}
impl Account {
fn new(
public_key: PublicKey,
secret: String,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
impl Startup {
fn new(credential: Credential, window: &mut Window, cx: &mut Context<Self>) -> Self {
let tasks = smallvec![];
let mut subscriptions = smallvec![];
@@ -69,8 +59,7 @@ impl Account {
);
Self {
public_key,
secret,
credential,
loading: false,
name: "Account".into(),
focus_handle: cx.focus_handle(),
@@ -83,9 +72,11 @@ impl Account {
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.set_loading(true, cx);
let secret = self.credential.secret();
// Try to login with bunker
if self.secret.starts_with("bunker://") {
match NostrConnectURI::parse(&self.secret) {
if secret.starts_with("bunker://") {
match NostrConnectURI::parse(secret) {
Ok(uri) => {
self.login_with_bunker(uri, window, cx);
}
@@ -98,7 +89,7 @@ impl Account {
};
// Fall back to login with keys
match SecretKey::parse(&self.secret) {
match SecretKey::parse(secret) {
Ok(secret) => {
self.login_with_keys(secret, cx);
}
@@ -115,6 +106,8 @@ impl Account {
window: &mut Window,
cx: &mut Context<Self>,
) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let keystore = KeyStore::global(cx).read(cx).backend();
// Handle connection in the background
@@ -138,8 +131,6 @@ impl Account {
this._tasks.push(
// Handle connection in the background
cx.spawn_in(window, async move |this, cx| {
let client = app_state().client();
match signer.bunker_uri().await {
Ok(_) => {
client.set_signer(signer).await;
@@ -171,11 +162,12 @@ impl Account {
}
fn login_with_keys(&mut self, secret: SecretKey, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let keys = Keys::new(secret);
// Update the signer
cx.background_spawn(async move {
let client = app_state().client();
client.set_signer(keys).await;
})
.detach();
@@ -187,7 +179,7 @@ impl Account {
}
}
impl Panel for Account {
impl Panel for Startup {
fn panel_id(&self) -> SharedString {
self.name.clone()
}
@@ -197,19 +189,21 @@ impl Panel for Account {
}
}
impl EventEmitter<PanelEvent> for Account {}
impl EventEmitter<PanelEvent> for Startup {}
impl Focusable for Account {
impl Focusable for Startup {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Account {
impl Render for Startup {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get_person(&self.public_key, cx);
let bunker = self.secret.starts_with("bunker://");
let bunker = self.credential.secret().starts_with("bunker://");
let profile = persons
.read(cx)
.get_person(&self.credential.public_key(), cx);
v_flex()
.image_cache(self.image_cache.clone())

View File

@@ -1,7 +1,6 @@
use std::time::Duration;
use common::display::RenderedProfile;
use common::nip05::nip05_verify;
use common::{nip05_verify, RenderedProfile};
use gpui::prelude::FluentBuilder;
use gpui::{
div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement,
@@ -13,7 +12,7 @@ use nostr_sdk::prelude::*;
use person::PersonRegistry;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use states::app_state;
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
@@ -33,13 +32,15 @@ pub struct UserProfile {
impl UserProfile {
pub fn new(target: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get_person(&target, cx);
let mut tasks = smallvec![];
let check_follow: Task<Result<bool, Error>> = cx.background_spawn(async move {
let client = app_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let contact_list = client.database().contacts_public_keys(public_key).await?;

View File

@@ -0,0 +1,22 @@
[package]
name = "encryption"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
state = { path = "../state" }
common = { path = "../common" }
account = { path = "../account" }
gpui.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
smallvec.workspace = true
smol.workspace = true
futures.workspace = true
flume.workspace = true
log.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -0,0 +1,631 @@
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use account::Account;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use common::app_name;
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
use nostr_sdk::prelude::*;
pub use signer::*;
use smallvec::{smallvec, SmallVec};
use state::{Announcement, NostrRegistry, Response};
mod signer;
pub fn init(cx: &mut App) {
Encryption::set_global(cx.new(Encryption::new), cx);
}
struct GlobalEncryption(Entity<Encryption>);
impl Global for GlobalEncryption {}
pub struct Encryption {
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
///
/// Client Signer that used for communication between devices
client_signer: Entity<Option<Arc<dyn NostrSigner>>>,
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
///
/// Encryption Key used for encryption and decryption instead of the user's identity
pub encryption: Entity<Option<Arc<dyn NostrSigner>>>,
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
///
/// Encryption Key announcement
announcement: Option<Arc<Announcement>>,
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
///
/// Requests for encryption keys from other devices
requests: Entity<HashSet<Announcement>>,
/// Async task for handling notifications
handle_notifications: Option<Task<()>>,
/// Async task for handling requests
handle_requests: Option<Task<()>>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 2]>,
}
impl Encryption {
/// Retrieve the global encryption state
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalEncryption>().0.clone()
}
/// Set the global encryption instance
fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalEncryption(state));
}
/// Create a new encryption instance
fn new(cx: &mut Context<Self>) -> Self {
let account = Account::global(cx);
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let requests = cx.new(|_| HashSet::default());
let encryption = cx.new(|_| None);
let client_signer = cx.new(|_| None);
let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
subscriptions.push(
// Observe the account state
cx.observe(&account, |this, state, cx| {
if state.read(cx).has_account() && !this.has_encryption(cx) {
this.get_announcement(cx);
}
}),
);
tasks.push(
// Get the client key
cx.spawn(async move |this, cx| {
match Self::get_keys(&client, "client").await {
Ok(keys) => {
this.update(cx, |this, cx| {
this.set_client(Arc::new(keys), cx);
})
.expect("Entity has been released");
}
Err(_) => {
let keys = Keys::generate();
let secret = keys.secret_key().to_secret_hex();
// Store the key in the database for future use
Self::set_keys(&client, "client", secret).await.ok();
// Update global state
this.update(cx, |this, cx| {
this.set_client(Arc::new(keys), cx);
})
.expect("Entity has been released");
}
}
}),
);
Self {
requests,
client_signer,
encryption,
announcement: None,
handle_notifications: None,
handle_requests: None,
_subscriptions: subscriptions,
_tasks: tasks,
}
}
/// Encrypt and store a key in the local database.
async fn set_keys<T>(client: &Client, kind: T, value: String) -> Result<(), Error>
where
T: Into<String>,
{
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
// Encrypt the value
let content = signer.nip44_encrypt(&public_key, value.as_ref()).await?;
// Construct the application data event
let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
.tag(Tag::identifier(format!("coop:{}", kind.into())))
.build(public_key)
.sign(&Keys::generate())
.await?;
// Save the event to the database
client.database().save_event(&event).await?;
Ok(())
}
/// Get and decrypt a key from the local database.
async fn get_keys<T>(client: &Client, kind: T) -> Result<Keys, Error>
where
T: Into<String>,
{
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.identifier(format!("coop:{}", kind.into()));
if let Some(event) = client.database().query(filter).await?.first() {
let content = signer.nip44_decrypt(&public_key, &event.content).await?;
let secret = SecretKey::parse(&content)?;
let keys = Keys::new(secret);
Ok(keys)
} else {
Err(anyhow!("Key not found"))
}
}
fn get_announcement(&mut self, cx: &mut Context<Self>) {
let task = self._get_announcement(cx);
self._tasks.push(cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(5)).await;
match task.await {
Ok(announcement) => {
this.update(cx, |this, cx| {
this.load_encryption(&announcement, cx);
// Set the announcement
this.announcement = Some(Arc::new(announcement));
cx.notify();
})
.expect("Entity has been released");
}
Err(err) => {
log::error!("Failed to get announcement: {}", err);
}
};
}));
}
fn _get_announcement(&self, cx: &App) -> Task<Result<Announcement, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
cx.background_spawn(async move {
let user_signer = client.signer().await?;
let public_key = user_signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::Custom(10044))
.author(public_key)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first() {
Ok(Self::extract_announcement(event)?)
} else {
Err(anyhow!("Announcement not found"))
}
})
}
/// Load the encryption key that stored in the database
///
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
fn load_encryption(&mut self, announcement: &Announcement, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let n = announcement.public_key();
cx.spawn(async move |this, cx| {
let result = Self::get_keys(&client, "encryption").await;
this.update(cx, |this, cx| {
if let Ok(keys) = result {
if keys.public_key() == n {
this.set_encryption(Arc::new(keys), cx);
this.listen_request(cx);
}
}
})
.expect("Entity has been released");
})
.detach();
}
/// Listen for the encryption key request from other devices
///
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
pub fn listen_request(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let (tx, rx) = flume::bounded::<Announcement>(50);
let task: Task<Result<(), Error>> = cx.background_spawn({
let client = Arc::clone(&client);
async move {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let id = SubscriptionId::new("listen-request");
let filter = Filter::new()
.author(public_key)
.kind(Kind::Custom(4454))
.since(Timestamp::now());
// Unsubscribe from the previous subscription
client.unsubscribe(&id).await;
// Subscribe to the new subscription
client.subscribe_with_id(id, filter, None).await?;
Ok(())
}
});
// Run this task and finish in the background
task.detach();
// Handle notifications
self.handle_notifications = Some(cx.background_spawn(async move {
let mut notifications = client.notifications();
let mut processed_events = HashSet::new();
while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, .. } = notification else {
// Skip if the notification is not a message
continue;
};
if let RelayMessage::Event { event, .. } = message {
if !processed_events.insert(event.id) {
// Skip if the event has already been processed
continue;
}
if event.kind != Kind::Custom(4454) {
// Skip if the event is not a encryption events
continue;
};
if Self::is_self_authored(&client, &event).await {
if let Ok(announcement) = Self::extract_announcement(&event) {
tx.send_async(announcement).await.ok();
}
}
}
}
}));
// Handle requests
self.handle_requests = Some(cx.spawn(async move |this, cx| {
while let Ok(request) = rx.recv_async().await {
this.update(cx, |this, cx| {
this.set_request(request, cx);
})
.expect("Entity has been released");
}
}));
}
/// Overwrite the encryption key
///
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
pub fn new_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let keys = Keys::generate();
let public_key = keys.public_key();
let secret = keys.secret_key().to_secret_hex();
// Create a task announce the encryption key
cx.background_spawn(async move {
// Store the encryption key to the database
Self::set_keys(&client, "encryption", secret).await?;
let signer = client.signer().await?;
// Construct the announcement event
let event = EventBuilder::new(Kind::Custom(10044), "")
.tags(vec![
Tag::client(app_name()),
Tag::custom(TagKind::custom("n"), vec![public_key]),
])
.sign(&signer)
.await?;
// Send the announcement event to user's relays
client.send_event(&event).await?;
Ok(keys)
})
}
/// Send a request for encryption key from other clients
///
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
pub fn send_request(&self, cx: &App) -> Task<Result<Option<Keys>, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Get the client signer
let Some(client_signer) = self.client_signer.read(cx).clone() else {
return Task::ready(Err(anyhow!("Client Signer is required")));
};
cx.background_spawn(async move {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let client_pubkey = client_signer.get_public_key().await?;
// Get the encryption key approval response from the database first
let filter = Filter::new()
.kind(Kind::Custom(4455))
.author(public_key)
.pubkey(client_pubkey)
.limit(1);
match client.database().query(filter).await?.first_owned() {
Some(event) => {
let root_device = event
.tags
.find(TagKind::custom("P"))
.and_then(|tag| tag.content())
.and_then(|content| PublicKey::parse(content).ok())
.context("Invalid event's tags")?;
let payload = event.content.as_str();
let decrypted = client_signer.nip44_decrypt(&root_device, payload).await?;
let secret = SecretKey::from_hex(&decrypted)?;
let keys = Keys::new(secret);
Ok(Some(keys))
}
None => {
// Construct encryption keys request event
let event = EventBuilder::new(Kind::Custom(4454), "")
.tags(vec![
Tag::client(app_name()),
Tag::custom(TagKind::custom("pubkey"), vec![client_pubkey]),
])
.sign(&signer)
.await?;
// Send a request for encryption keys from other devices
client.send_event(&event).await?;
// Create a unique ID to control the subscription later
let subscription_id = SubscriptionId::new("listen-response");
let filter = Filter::new()
.kind(Kind::Custom(4455))
.author(public_key)
.pubkey(client_pubkey)
.since(Timestamp::now());
// Subscribe to the approval response event
client
.subscribe_with_id(subscription_id, filter, None)
.await?;
Ok(None)
}
}
})
}
/// Send the approval response event
///
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
pub fn send_response(&self, target: PublicKey, cx: &App) -> Task<Result<(), Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Get the client signer
let Some(client_signer) = self.client_signer.read(cx).clone() else {
return Task::ready(Err(anyhow!("Client Signer is required")));
};
cx.background_spawn(async move {
let encryption = Self::get_keys(&client, "encryption").await?;
let client_pubkey = client_signer.get_public_key().await?;
// Encrypt the encryption keys with the client's signer
let payload = client_signer
.nip44_encrypt(&target, &encryption.secret_key().to_secret_hex())
.await?;
// Construct the response event
//
// P tag: the current client's public key
// p tag: the requester's public key
let event = EventBuilder::new(Kind::Custom(4455), payload)
.tags(vec![
Tag::custom(TagKind::custom("P"), vec![client_pubkey]),
Tag::public_key(target),
])
.sign(&client_signer)
.await?;
// Get the current user's signer and public key
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
// Get the current user's relay list
let urls: Vec<RelayUrl> = client
.database()
.relay_list(public_key)
.await?
.into_iter()
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == Some(RelayMetadata::Read) {
Some(url)
} else {
None
}
})
.collect();
// Send the response event to the user's relay list
client.send_event_to(urls, &event).await?;
Ok(())
})
}
/// Wait for the approval response event
///
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
pub fn wait_for_approval(&self, cx: &App) -> Task<Result<Keys, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let client_signer = self.client_signer.read(cx).clone().unwrap();
let mut processed_events = HashSet::new();
cx.background_spawn(async move {
let mut notifications = client.notifications();
log::info!("Listening for notifications");
while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, .. } = notification else {
// Skip non-message notifications
continue;
};
if let RelayMessage::Event { event, .. } = message {
if !processed_events.insert(event.id) {
// Skip if the event has already been processed
continue;
}
if event.kind != Kind::Custom(4455) {
// Skip non-gift wrap events
continue;
}
if let Ok(response) = Self::extract_response(&client, &event).await {
let public_key = response.public_key();
let payload = response.payload();
// Decrypt the payload using the client signer
let decrypted = client_signer.nip44_decrypt(&public_key, payload).await?;
let secret = SecretKey::parse(&decrypted)?;
// Construct the encryption keys
let keys = Keys::new(secret);
return Ok(keys);
}
}
}
Err(anyhow!("Failed to handle Encryption Key approval response"))
})
}
/// Set the client signer for the account
pub fn set_client(&mut self, signer: Arc<dyn NostrSigner>, cx: &mut Context<Self>) {
self.client_signer.update(cx, |this, cx| {
*this = Some(signer);
cx.notify();
});
}
/// Set the encryption signer for the account
pub fn set_encryption(&mut self, signer: Arc<dyn NostrSigner>, cx: &mut Context<Self>) {
self.encryption.update(cx, |this, cx| {
*this = Some(signer);
cx.notify();
});
}
/// Check if the account entity has an encryption key
pub fn has_encryption(&self, cx: &App) -> bool {
self.encryption.read(cx).is_some()
}
/// Returns the encryption key
pub fn encryption_key(&self, cx: &App) -> Option<Arc<dyn NostrSigner>> {
self.encryption.read(cx).clone()
}
/// Returns the encryption announcement
pub fn announcement(&self) -> Option<Arc<Announcement>> {
self.announcement.clone()
}
/// Returns the encryption requests
pub fn requests(&self) -> Entity<HashSet<Announcement>> {
self.requests.clone()
}
/// Push the encryption request
pub fn set_request(&mut self, request: Announcement, cx: &mut Context<Self>) {
self.requests.update(cx, |this, cx| {
this.insert(request);
cx.notify();
});
}
/// Extract an encryption keys announcement from an event.
fn extract_announcement(event: &Event) -> Result<Announcement, Error> {
let public_key = event
.tags
.iter()
.find(|tag| tag.kind().as_str() == "n" || tag.kind().as_str() == "pubkey")
.and_then(|tag| tag.content())
.and_then(|c| PublicKey::parse(c).ok())
.context("Cannot parse public key from the event's tags")?;
let client_name = event
.tags
.find(TagKind::Client)
.and_then(|tag| tag.content())
.map(|c| c.to_string())
.context("Cannot parse client name from the event's tags")?;
Ok(Announcement::new(event.id, client_name, public_key))
}
/// Extract an encryption keys response from an event.
async fn extract_response(client: &Client, event: &Event) -> Result<Response, Error> {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
if event.pubkey != public_key {
return Err(anyhow!("Event does not belong to current user"));
}
let client_pubkey = event
.tags
.find(TagKind::custom("P"))
.and_then(|tag| tag.content())
.and_then(|c| PublicKey::parse(c).ok())
.context("Cannot parse public key from the event's tags")?;
Ok(Response::new(event.content.clone(), client_pubkey))
}
/// Check if event is published by current user
async fn is_self_authored(client: &Client, event: &Event) -> bool {
if let Ok(signer) = client.signer().await {
if let Ok(public_key) = signer.get_public_key().await {
return public_key == event.pubkey;
}
}
false
}
}

View File

@@ -0,0 +1,9 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Deserialize, Serialize)]
pub enum SignerKind {
Encryption,
#[default]
User,
Auto,
}

View File

@@ -0,0 +1,27 @@
[package]
name = "encryption_ui"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
state = { path = "../state" }
ui = { path = "../ui" }
theme = { path = "../theme" }
common = { path = "../common" }
account = { path = "../account" }
encryption = { path = "../encryption" }
person = { path = "../person" }
settings = { path = "../settings" }
gpui.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
itertools.workspace = true
smallvec.workspace = true
smol.workspace = true
log.workspace = true
futures.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -0,0 +1,407 @@
use std::sync::Arc;
use std::time::Duration;
use anyhow::anyhow;
use common::shorten_pubkey;
use encryption::Encryption;
use futures::FutureExt;
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString,
Styled, Subscription, Window,
};
use smallvec::{smallvec, SmallVec};
use state::Announcement;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::notification::Notification;
use ui::{h_flex, v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<EncryptionPanel> {
cx.new(|cx| EncryptionPanel::new(window, cx))
}
#[derive(Debug)]
pub struct EncryptionPanel {
/// Whether the panel is currently requesting encryption.
requesting: bool,
/// Whether the panel is currently creating encryption.
creating: bool,
/// Whether the panel is currently showing an error.
error: Entity<Option<SharedString>>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
}
impl EncryptionPanel {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let error = cx.new(|_| None);
let encryption = Encryption::global(cx);
let requests = encryption.read(cx).requests();
let mut subscriptions = smallvec![];
subscriptions.push(
// Observe encryption request
cx.observe_in(&requests, window, |this, state, window, cx| {
for req in state.read(cx).clone().into_iter() {
this.ask_for_approval(req, window, cx);
}
}),
);
Self {
requesting: false,
creating: false,
error,
_subscriptions: subscriptions,
}
}
fn set_requesting(&mut self, status: bool, cx: &mut Context<Self>) {
self.requesting = status;
cx.notify();
}
fn set_creating(&mut self, status: bool, cx: &mut Context<Self>) {
self.creating = status;
cx.notify();
}
fn set_error(&mut self, error: impl Into<SharedString>, cx: &mut Context<Self>) {
self.error.update(cx, |this, cx| {
*this = Some(error.into());
cx.notify();
});
}
fn request(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let encryption = Encryption::global(cx);
let send_request = encryption.read(cx).send_request(cx);
// Ensure the user has not sent multiple requests
if self.requesting {
return;
}
self.set_requesting(true, cx);
cx.spawn_in(window, async move |this, cx| {
match send_request.await {
Ok(Some(keys)) => {
this.update(cx, |this, cx| {
this.set_requesting(false, cx);
// Set the encryption key
encryption.update(cx, |this, cx| {
this.set_encryption(Arc::new(keys), cx);
});
})
.expect("Entity has been released");
}
Ok(None) => {
this.update_in(cx, |this, window, cx| {
this.wait_for_approval(window, cx);
})
.expect("Entity has been released");
}
Err(e) => {
this.update(cx, |this, cx| {
this.set_requesting(false, cx);
this.set_error(e.to_string(), cx);
})
.expect("Entity has been released");
}
}
})
.detach();
}
fn new_encryption(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let encryption = Encryption::global(cx);
let reset = encryption.read(cx).new_encryption(cx);
// Ensure the user has not sent multiple requests
if self.requesting {
return;
}
self.set_creating(true, cx);
cx.spawn_in(window, async move |this, cx| {
match reset.await {
Ok(keys) => {
this.update(cx, |this, cx| {
this.set_creating(false, cx);
// Set the encryption key
encryption.update(cx, |this, cx| {
this.set_encryption(Arc::new(keys), cx);
this.listen_request(cx);
});
})
.expect("Entity has been released");
}
Err(e) => {
this.update(cx, |this, cx| {
this.set_creating(false, cx);
this.set_error(e.to_string(), cx);
})
.expect("Entity has been released");
}
}
})
.detach();
}
fn wait_for_approval(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let encryption = Encryption::global(cx);
let wait_for_approval = encryption.read(cx).wait_for_approval(cx);
cx.spawn_in(window, async move |this, cx| {
let timeout = cx.background_executor().timer(Duration::from_secs(30));
let result = futures::select! {
result = wait_for_approval.fuse() => {
// Ok(keys)
result
},
_ = timeout.fuse() => {
Err(anyhow!("Timeout"))
}
};
this.update(cx, |this, cx| {
match result {
Ok(keys) => {
this.set_requesting(false, cx);
// Set the encryption key
encryption.update(cx, |this, cx| {
this.set_encryption(Arc::new(keys), cx);
});
}
Err(e) => {
this.set_error(e.to_string(), cx);
}
};
})
.expect("Entity has been released");
})
.detach();
}
fn ask_for_approval(&mut self, req: Announcement, window: &mut Window, cx: &mut Context<Self>) {
let client_name = SharedString::from(req.client().to_string());
let target = req.public_key();
let note = Notification::new()
.custom_id(SharedString::from(req.id().to_hex()))
.autohide(false)
.icon(IconName::Info)
.title(SharedString::from("Encryption Key Request"))
.content(move |_window, cx| {
v_flex()
.gap_2()
.text_sm()
.child(SharedString::from(
"You've requested for the Encryption Key from:",
))
.child(
v_flex()
.py_1()
.px_1p5()
.rounded_sm()
.text_xs()
.bg(cx.theme().warning_background)
.text_color(cx.theme().warning_foreground)
.child(client_name.clone()),
)
.into_any_element()
})
.action(move |_window, _cx| {
Button::new("approve")
.label("Approve")
.small()
.primary()
.loading(false)
.disabled(false)
.on_click(move |_ev, _window, cx| {
let encryption = Encryption::global(cx);
let send_response = encryption.read(cx).send_response(target, cx);
send_response.detach();
})
});
// Push the notification to the current window
window.push_notification(note, cx);
// Focus the window if it's not active
if !window.is_window_hovered() {
window.activate_window();
}
}
}
impl Render for EncryptionPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
const NOTICE: &str = "Found an Encryption Announcement";
const SUGGEST: &str = "Please request the Encryption Key to continue using.";
const DESCRIPTION: &str = "Encryption Key is used to replace the User's Identity in encryption and decryption messages. Coop will automatically fallback to User's Identity if needed.";
const WARNING: &str = "Encryption Key is still in the alpha stage. Please be cautious.";
let encryption = Encryption::global(cx);
let has_encryption = encryption.read(cx).has_encryption(cx);
v_flex()
.p_2()
.max_w(px(340.))
.w(px(340.))
.text_sm()
.when(has_encryption, |this| {
this.child(
h_flex()
.gap_2()
.w_full()
.text_xs()
.font_semibold()
.child(
Icon::new(IconName::CheckCircleFill)
.small()
.text_color(cx.theme().element_active),
)
.child(SharedString::from("Encryption Key has been set")),
)
})
.when(!has_encryption, |this| {
if let Some(announcement) = encryption.read(cx).announcement().as_ref() {
let pubkey = shorten_pubkey(announcement.public_key(), 16);
let name = announcement.client();
this.child(
v_flex()
.gap_2()
.child(div().font_semibold().child(SharedString::from(NOTICE)))
.child(
v_flex()
.h_12()
.items_center()
.justify_center()
.rounded(cx.theme().radius)
.bg(cx.theme().warning_background)
.text_color(cx.theme().warning_foreground)
.child(name),
)
.child(
v_flex()
.gap_1()
.child(
div()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Client Public Key:")),
)
.child(
h_flex()
.h_7()
.w_full()
.px_2()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.child(SharedString::from(pubkey)),
),
)
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from(SUGGEST)),
)
.child(
h_flex()
.mt_2()
.gap_1()
.when(!self.requesting, |this| {
this.child(
Button::new("reset")
.label("Reset")
.flex_1()
.small()
.ghost_alt()
.loading(self.creating)
.disabled(self.creating)
.on_click(cx.listener(
move |this, _ev, window, cx| {
this.new_encryption(window, cx);
},
)),
)
})
.when(!self.creating, |this| {
this.child(
Button::new("request")
.label({
if self.requesting {
"Wait for approval"
} else {
"Request"
}
})
.flex_1()
.small()
.primary()
.loading(self.requesting)
.disabled(self.requesting)
.on_click(cx.listener(
move |this, _ev, window, cx| {
this.request(window, cx);
},
)),
)
}),
)
.when_some(self.error.read(cx).as_ref(), |this, error| {
this.child(
div()
.text_xs()
.text_center()
.text_color(cx.theme().danger_foreground)
.child(error.clone()),
)
}),
)
} else {
this.child(
v_flex()
.gap_2()
.child(
div()
.font_semibold()
.child(SharedString::from("Set up Encryption Key")),
)
.child(SharedString::from(DESCRIPTION))
.child(
div()
.text_xs()
.text_color(cx.theme().warning_foreground)
.child(SharedString::from(WARNING)),
)
.child(
Button::new("create")
.label("Setup")
.flex_1()
.small()
.primary()
.loading(self.creating)
.disabled(self.creating)
.on_click(cx.listener(move |this, _ev, window, cx| {
this.new_encryption(window, cx);
})),
),
)
}
})
}
}

View File

@@ -5,11 +5,9 @@ edition.workspace = true
publish.workspace = true
[dependencies]
states = { path = "../states" }
common = { path = "../common" }
gpui.workspace = true
nostr.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true

View File

@@ -6,9 +6,33 @@ use std::path::PathBuf;
use std::pin::Pin;
use anyhow::Result;
use common::config_dir;
use futures::FutureExt as _;
use gpui::AsyncApp;
use states::config_dir;
use nostr_sdk::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Credential {
public_key: PublicKey,
secret: String,
}
impl Credential {
pub fn new(user: String, secret: Vec<u8>) -> Self {
Self {
public_key: PublicKey::parse(&user).unwrap(),
secret: String::from_utf8(secret).unwrap(),
}
}
pub fn public_key(&self) -> PublicKey {
self.public_key
}
pub fn secret(&self) -> &str {
&self.secret
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyItem {

View File

@@ -1,11 +1,10 @@
use std::sync::{Arc, LazyLock};
pub use backend::*;
use gpui::{App, AppContext, Context, Entity, Global, Task};
use smallvec::{smallvec, SmallVec};
use crate::backend::{FileProvider, KeyBackend, KeyringProvider};
pub mod backend;
mod backend;
static DISABLE_KEYRING: LazyLock<bool> =
LazyLock::new(|| std::env::var("DISABLE_KEYRING").is_ok_and(|value| !value.is_empty()));

View File

@@ -6,10 +6,9 @@ publish.workspace = true
[dependencies]
common = { path = "../common" }
states = { path = "../states" }
state = { path = "../state" }
gpui.workspace = true
nostr.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
itertools.workspace = true

View File

@@ -1,9 +1,10 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use gpui::{App, AppContext, AsyncApp, Context, Entity, Global, Task};
use gpui::{App, AppContext, Context, Entity, Global, Task};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use states::app_state;
use state::NostrRegistry;
pub fn init(cx: &mut App) {
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx);
@@ -13,6 +14,8 @@ struct GlobalPersonRegistry(Entity<PersonRegistry>);
impl Global for GlobalPersonRegistry {}
/// Person Registry
#[derive(Debug)]
pub struct PersonRegistry {
/// Collection of all persons (user profiles)
pub persons: HashMap<PublicKey, Entity<Profile>>,
@@ -34,12 +37,58 @@ impl PersonRegistry {
/// Create a new person registry instance
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let mut tasks = smallvec![];
tasks.push(
// Handle notifications
cx.spawn({
let client = Arc::clone(&client);
async move |this, cx| {
let mut notifications = client.notifications();
log::info!("Listening for notifications");
let mut processed_events = HashSet::new();
while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, .. } = notification else {
// Skip if the notification is not a message
continue;
};
if let RelayMessage::Event { event, .. } = message {
if !processed_events.insert(event.id) {
// Skip if the event has already been processed
continue;
}
if event.kind != Kind::Metadata {
// Skip if the event is not a metadata event
continue;
};
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata);
this.update(cx, |this, cx| {
this.insert_or_update_person(profile, cx);
})
.expect("Entity has been released")
}
}
}
}),
);
tasks.push(
// Load all user profiles from the database
cx.spawn(async move |this, cx| {
match Self::load_persons(cx).await {
let result = cx
.background_spawn(async move { Self::load_persons(&client).await })
.await;
match result {
Ok(profiles) => {
this.update(cx, |this, cx| {
this.bulk_insert_persons(profiles, cx);
@@ -59,23 +108,20 @@ impl PersonRegistry {
}
}
/// Create a task to load all user profiles from the database
fn load_persons(cx: &AsyncApp) -> Task<Result<Vec<Profile>, Error>> {
cx.background_spawn(async move {
let client = app_state().client();
let filter = Filter::new().kind(Kind::Metadata).limit(200);
let events = client.database().query(filter).await?;
/// Load all user profiles from the database
async fn load_persons(client: &Client) -> Result<Vec<Profile>, Error> {
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() {
let metadata = Metadata::from_json(event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata);
profiles.push(profile);
}
for event in events.into_iter() {
let metadata = Metadata::from_json(event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata);
profiles.push(profile);
}
Ok(profiles)
})
Ok(profiles)
}
/// Insert batch of persons

View File

@@ -0,0 +1,20 @@
[package]
name = "relay_auth"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
state = { path = "../state" }
settings = { path = "../settings" }
common = { path = "../common" }
theme = { path = "../theme" }
ui = { path = "../ui" }
gpui.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
smallvec.workspace = true
smol.workspace = true
log.workspace = true

View File

@@ -0,0 +1,326 @@
use std::borrow::Cow;
use std::cell::Cell;
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use anyhow::{anyhow, Error};
use gpui::{
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
Subscription, Task, Window,
};
use nostr_sdk::prelude::*;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::notification::Notification;
use ui::{v_flex, ContextModal, Disableable, IconName, Sizable};
const AUTH_MESSAGE: &str =
"Approve the authentication request to allow Coop to continue sending or receiving events.";
pub fn init(window: &mut Window, cx: &mut App) {
RelayAuth::set_global(cx.new(|cx| RelayAuth::new(window, cx)), cx);
}
struct GlobalRelayAuth(Entity<RelayAuth>);
impl Global for GlobalRelayAuth {}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct AuthRequest {
pub url: RelayUrl,
pub challenge: String,
}
impl Hash for AuthRequest {
fn hash<H: Hasher>(&self, state: &mut H) {
self.challenge.hash(state);
}
}
impl AuthRequest {
pub fn new(challenge: impl Into<String>, url: RelayUrl) -> Self {
Self {
challenge: challenge.into(),
url,
}
}
}
#[derive(Debug)]
pub struct RelayAuth {
/// Entity for managing auth requests
requests: HashSet<AuthRequest>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 1]>,
}
impl RelayAuth {
/// Retrieve the global relay auth state
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalRelayAuth>().0.clone()
}
/// Set the global relay auth instance
fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalRelayAuth(state));
}
/// Create a new relay auth instance
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let entity = cx.entity();
let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
subscriptions.push(
// Observe the current state
cx.observe_in(&entity, window, |this, _, window, cx| {
let auto_auth = AppSettings::get_auto_auth(cx);
for req in this.requests.clone().into_iter() {
let is_authenticated = AppSettings::read_global(cx).is_authenticated(&req.url);
if auto_auth && is_authenticated {
// Automatically authenticate if the relay is authenticated before
this.response(req.to_owned(), window, cx);
} else {
// Otherwise open the auth request popup
this.ask_for_approval(req.to_owned(), window, cx);
}
}
}),
);
tasks.push(
// Handle notifications
cx.spawn(async move |this, cx| {
let mut notifications = client.notifications();
let mut challenges: HashSet<Cow<'_, str>> = HashSet::new();
while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, relay_url } = notification else {
// Skip if the notification is not a message
continue;
};
if let RelayMessage::Auth { challenge } = message {
if challenges.insert(challenge.clone()) {
this.update(cx, |this, cx| {
this.requests.insert(AuthRequest::new(challenge, relay_url));
cx.notify();
})
.expect("Entity has been released");
};
}
}
}),
);
Self {
requests: HashSet::new(),
_subscriptions: subscriptions,
_tasks: tasks,
}
}
/// Get the number of pending requests.
pub fn pending_requests(&self, _cx: &App) -> usize {
self.requests.len()
}
/// Reask for approval for all pending requests.
pub fn re_ask(&mut self, window: &mut Window, cx: &mut Context<Self>) {
for request in self.requests.clone().into_iter() {
self.ask_for_approval(request, window, cx);
}
}
/// Respond to an authentication request.
fn response(&mut self, req: AuthRequest, window: &mut Window, cx: &mut Context<Self>) {
let settings = AppSettings::global(cx);
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let tracker = nostr.read(cx).tracker();
let challenge = req.challenge.to_owned();
let url = req.url.to_owned();
let challenge_clone = challenge.clone();
let url_clone = url.clone();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let signer = client.signer().await?;
// Construct event
let event: Event = EventBuilder::auth(challenge_clone, url_clone.clone())
.sign(&signer)
.await?;
// Get the event ID
let id = event.id;
// Get the relay
let relay = client.pool().relay(url_clone).await?;
let relay_url = relay.url();
// Subscribe to notifications
let mut notifications = relay.notifications();
// Send the AUTH message
relay.send_msg(ClientMessage::Auth(Cow::Borrowed(&event)))?;
while let Ok(notification) = notifications.recv().await {
match notification {
RelayNotification::Message {
message: RelayMessage::Ok { event_id, .. },
} => {
if id == event_id {
// Re-subscribe to previous subscription
relay.resubscribe().await?;
// Get all failed events that need to be resent
let mut tracker = tracker.write().await;
let ids: Vec<EventId> = tracker
.resend_queue
.iter()
.filter(|(_, url)| relay_url == *url)
.map(|(id, _)| *id)
.collect();
for id in ids.into_iter() {
if let Some(relay_url) = tracker.resend_queue.remove(&id) {
if let Some(event) = client.database().event_by_id(&id).await? {
let event_id = relay.send_event(&event).await?;
let output = Output {
val: event_id,
failed: HashMap::new(),
success: HashSet::from([relay_url]),
};
tracker.sent_ids.insert(event_id);
tracker.resent_ids.push(output);
}
}
}
return Ok(());
}
}
RelayNotification::AuthenticationFailed => break,
RelayNotification::Shutdown => break,
_ => {}
}
}
Err(anyhow!("Authentication failed"))
});
self._tasks.push(
// Handle response in the background
cx.spawn_in(window, async move |this, cx| {
match task.await {
Ok(_) => {
this.update_in(cx, |this, window, cx| {
// Clear the current notification
window.clear_notification_by_id(SharedString::from(&challenge), cx);
// Push a new notification
window.push_notification(format!("{url} has been authenticated"), cx);
// Save the authenticated relay to automatically authenticate future requests
settings.update(cx, |this, cx| {
this.push_relay(&url, cx);
});
// Remove the challenge from the list of pending authentications
this.requests.remove(&req);
cx.notify();
})
.expect("Entity has been released");
}
Err(e) => {
this.update_in(cx, |_, window, cx| {
window.push_notification(Notification::error(e.to_string()), cx);
})
.expect("Entity has been released");
}
};
}),
);
}
/// Push a popup to approve the authentication request.
fn ask_for_approval(&mut self, req: AuthRequest, window: &mut Window, cx: &mut Context<Self>) {
let url = SharedString::from(req.url.clone().to_string());
let entity = cx.entity().downgrade();
let loading = Rc::new(Cell::new(false));
let note = Notification::new()
.custom_id(SharedString::from(&req.challenge))
.autohide(false)
.icon(IconName::Info)
.title(SharedString::from("Authentication Required"))
.content(move |_window, cx| {
v_flex()
.gap_2()
.text_sm()
.child(SharedString::from(AUTH_MESSAGE))
.child(
v_flex()
.py_1()
.px_1p5()
.rounded_sm()
.text_xs()
.bg(cx.theme().warning_background)
.text_color(cx.theme().warning_foreground)
.child(url.clone()),
)
.into_any_element()
})
.action(move |_window, _cx| {
let entity = entity.clone();
let req = req.clone();
Button::new("approve")
.label("Approve")
.small()
.primary()
.loading(loading.get())
.disabled(loading.get())
.on_click({
let loading = Rc::clone(&loading);
move |_ev, window, cx| {
// Set loading state to true
loading.set(true);
// Process to approve the request
entity
.update(cx, |this, cx| {
this.response(req.clone(), window, cx);
})
.expect("Entity has been released");
}
})
});
// Push the notification to the current window
window.push_notification(note, cx);
// Focus the window if it's not active
if !window.is_window_hovered() {
window.activate_window();
}
}
}

View File

@@ -5,7 +5,7 @@ edition.workspace = true
publish.workspace = true
[dependencies]
states = { path = "../states" }
state = { path = "../state" }
nostr-sdk.workspace = true
gpui.workspace = true

View File

@@ -3,7 +3,9 @@ use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use smallvec::{smallvec, SmallVec};
use states::{app_state, SETTINGS_IDENTIFIER};
use state::NostrRegistry;
const SETTINGS_IDENTIFIER: &str = "coop:settings";
pub fn init(cx: &mut App) {
let state = cx.new(AppSettings::new);
@@ -122,8 +124,10 @@ impl AppSettings {
}
pub fn load_settings(&self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let task: Task<Result<Settings, anyhow::Error>> = cx.background_spawn(async move {
let client = app_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
@@ -153,9 +157,11 @@ impl AppSettings {
}
pub fn set_settings(&self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
if let Ok(content) = serde_json::to_string(&self.setting_values) {
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
let client = app_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;

View File

@@ -1,21 +1,23 @@
[package]
name = "states"
name = "state"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
common = { path = "../common" }
nostr-sdk.workspace = true
nostr-lmdb.workspace = true
nostr-gossip-memory.workspace = true
dirs.workspace = true
gpui.workspace = true
smol.workspace = true
flume.workspace = true
smallvec.workspace = true
log.workspace = true
anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
whoami = "1.6.1"
rustls = "0.23.23"
event-listener = "5.4.1"

312
crates/state/src/lib.rs Normal file
View File

@@ -0,0 +1,312 @@
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Error};
use common::{config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use gpui::{App, AppContext, Context, Entity, Global, Task};
use nostr_gossip_memory::prelude::*;
use nostr_lmdb::NostrLMDB;
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use smol::lock::RwLock;
pub use storage::*;
pub use tracker::*;
mod storage;
mod tracker;
pub const GIFTWRAP_SUBSCRIPTION: &str = "default-inbox";
pub const ENCRYPTION_GIFTWARP_SUBSCRIPTION: &str = "encryption-inbox";
pub fn init(cx: &mut App) {
NostrRegistry::set_global(cx.new(NostrRegistry::new), cx);
}
struct GlobalNostrRegistry(Entity<NostrRegistry>);
impl Global for GlobalNostrRegistry {}
/// Nostr Registry
#[derive(Debug)]
pub struct NostrRegistry {
/// Nostr client instance
client: Arc<Client>,
/// Tracks activity related to Nostr events
tracker: Arc<RwLock<EventTracker>>,
/// Manages caching of nostr events
cache_manager: Arc<RwLock<CacheManager>>,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 1]>,
}
impl NostrRegistry {
/// Retrieve the global nostr state
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalNostrRegistry>().0.clone()
}
/// Set the global nostr instance
fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalNostrRegistry(state));
}
/// Create a new nostr instance
fn new(cx: &mut Context<Self>) -> Self {
// rustls uses the `aws_lc_rs` provider by default
// This only errors if the default provider has already
// been installed. We can ignore this `Result`.
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.ok();
let path = config_dir().join("nostr");
let lmdb = NostrLMDB::open(path).expect("Failed to initialize database");
let gossip = NostrGossipMemory::unbounded();
// Nostr client options
let opts = ClientOptions::new()
.automatic_authentication(false)
.verify_subscriptions(false)
.sleep_when_idle(SleepWhenIdle::Enabled {
timeout: Duration::from_secs(600),
});
// Construct the nostr client
let client = Arc::new(
ClientBuilder::default()
.gossip(gossip)
.database(lmdb)
.opts(opts)
.build(),
);
let tracker = Arc::new(RwLock::new(EventTracker::default()));
let cache_manager = Arc::new(RwLock::new(CacheManager::default()));
let mut tasks = smallvec![];
tasks.push(
// Establish connection to the bootstrap relays
//
// And handle notifications from the nostr relay pool channel
cx.background_spawn({
let client = Arc::clone(&client);
let cache_manager = Arc::clone(&cache_manager);
let tracker = Arc::clone(&tracker);
let _ = initialized_at();
async move {
// Connect to the bootstrap relays
Self::connect(&client).await;
// Handle notifications from the relay pool
Self::handle_notifications(&client, &cache_manager, &tracker).await;
}
}),
);
Self {
client,
tracker,
cache_manager,
_tasks: tasks,
}
}
/// Establish connection to the bootstrap relays
async fn connect(client: &Client) {
// Get all bootstrapping relays
let mut urls = vec![];
urls.extend(BOOTSTRAP_RELAYS);
urls.extend(SEARCH_RELAYS);
// Add relay to the relay pool
for url in urls.into_iter() {
client.add_relay(url).await.ok();
}
// Connect to all added relays
client.connect().await;
}
async fn handle_notifications(
client: &Client,
cache: &Arc<RwLock<CacheManager>>,
tracker: &Arc<RwLock<EventTracker>>,
) {
let mut notifications = client.notifications();
log::info!("Listening for notifications");
let mut processed_events = HashSet::new();
while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, relay_url } = notification else {
// Skip if the notification is not a message
continue;
};
match message {
RelayMessage::Event { event, .. } => {
if !processed_events.insert(event.id) {
// Skip if the event has already been processed
continue;
}
match event.kind {
Kind::RelayList => {
if Self::is_self_authored(client, &event).await {
log::info!("Found relay list event for the current user");
let author = event.pubkey;
let announcement = Kind::Custom(10044);
// Fetch user's messaging relays event
_ = Self::subscribe(client, author, Kind::InboxRelays).await;
// Fetch user's encryption announcement event
_ = Self::subscribe(client, author, announcement).await;
// Fetch user's metadata event
_ = Self::subscribe(client, author, Kind::Metadata).await;
// Fetch user's contact list event
_ = Self::subscribe(client, author, Kind::ContactList).await;
}
}
Kind::InboxRelays => {
// Extract up to 3 messaging relays
let urls: Vec<RelayUrl> =
nip17::extract_relay_list(&event).take(3).cloned().collect();
// Subscribe to gift wrap events if event is from current user
if Self::is_self_authored(client, &event).await {
log::info!("Found messaging list event for the current user");
if let Err(e) =
Self::get_messages(client, &urls, event.pubkey).await
{
log::error!("Failed to subscribe to gift wrap events: {e}");
}
}
// Cache the messaging relays
let mut cache = cache.write().await;
cache.insert_relay(event.pubkey, urls);
}
Kind::ContactList => {
if Self::is_self_authored(client, &event).await {
let pubkeys: Vec<_> = event.tags.public_keys().copied().collect();
if let Err(e) = Self::get_metadata_for_list(client, pubkeys).await {
log::error!("Failed to get metadata for list: {e}");
}
}
}
_ => {}
};
}
RelayMessage::Ok {
event_id, message, ..
} => {
let msg = MachineReadablePrefix::parse(&message);
let mut tracker = tracker.write().await;
// Message that need to be authenticated will be handled separately
if let Some(MachineReadablePrefix::AuthRequired) = msg {
// Keep track of events that need to be resent after authentication
tracker.resend_queue.insert(event_id, relay_url);
} else {
// Keep track of events sent by Coop
tracker.sent_ids.insert(event_id);
}
}
_ => {}
}
}
}
/// Check if event is published by current user
async fn is_self_authored(client: &Client, event: &Event) -> bool {
if let Ok(signer) = client.signer().await {
if let Ok(public_key) = signer.get_public_key().await {
return public_key == event.pubkey;
}
}
false
}
/// Subscribe for events that match the given kind for a given author
async fn subscribe(client: &Client, author: PublicKey, kind: Kind) -> Result<(), Error> {
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let filter = Filter::new().author(author).kind(kind).limit(1);
// Subscribe to filters from the user's write relays
client.subscribe(filter, Some(opts)).await?;
Ok(())
}
/// Get all gift wrap events in the messaging relays for a given public key
async fn get_messages(
client: &Client,
urls: &[RelayUrl],
public_key: PublicKey,
) -> Result<(), Error> {
let id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION);
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
// Verify that there are relays provided
if urls.is_empty() {
return Err(anyhow!("No relays provided"));
}
// Add and connect relays
for url in urls {
client.add_relay(url).await?;
client.connect_relay(url).await?;
}
// Subscribe to filters to user's messaging relays
client.subscribe_with_id_to(urls, id, filter, None).await?;
Ok(())
}
/// Get metadata for a list of public keys
async fn get_metadata_for_list(client: &Client, pubkeys: Vec<PublicKey>) -> Result<(), Error> {
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
// Return if the list is empty
if pubkeys.is_empty() {
return Err(anyhow!("You need at least one public key".to_string(),));
}
let filter = Filter::new()
.limit(pubkeys.len() * kinds.len() + 10)
.authors(pubkeys)
.kinds(kinds);
// Subscribe to filters to the bootstrap relays
client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await?;
Ok(())
}
/// Returns a reference to the nostr client.
pub fn client(&self) -> Arc<Client> {
Arc::clone(&self.client)
}
/// Returns a reference to the event tracker.
pub fn tracker(&self) -> Arc<RwLock<EventTracker>> {
Arc::clone(&self.tracker)
}
/// Returns a reference to the cache manager.
pub fn cache_manager(&self) -> Arc<RwLock<CacheManager>> {
Arc::clone(&self.cache_manager)
}
}

View File

@@ -0,0 +1,87 @@
use std::collections::{HashMap, HashSet};
use gpui::SharedString;
use nostr_sdk::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Announcement {
id: EventId,
client: String,
public_key: PublicKey,
}
impl Announcement {
pub fn new(id: EventId, client_name: String, public_key: PublicKey) -> Self {
Self {
id,
client: client_name,
public_key,
}
}
pub fn id(&self) -> EventId {
self.id
}
pub fn public_key(&self) -> PublicKey {
self.public_key
}
pub fn client(&self) -> SharedString {
SharedString::from(self.client.clone())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Response {
payload: String,
public_key: PublicKey,
}
impl Response {
pub fn new(payload: String, public_key: PublicKey) -> Self {
Self {
payload,
public_key,
}
}
pub fn public_key(&self) -> PublicKey {
self.public_key
}
pub fn payload(&self) -> &str {
self.payload.as_str()
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheManager {
/// Cache of messaging relays for each public key
relay: HashMap<PublicKey, HashSet<RelayUrl>>,
/// Cache of device announcement for each public key
announcement: HashMap<PublicKey, Option<Announcement>>,
}
impl CacheManager {
pub fn relay(&self, public_key: &PublicKey) -> Option<&HashSet<RelayUrl>> {
self.relay.get(public_key)
}
pub fn insert_relay(&mut self, public_key: PublicKey, urls: Vec<RelayUrl>) {
self.relay.entry(public_key).or_default().extend(urls);
}
pub fn announcement(&self, public_key: &PublicKey) -> Option<&Option<Announcement>> {
self.announcement.get(public_key)
}
pub fn insert_announcement(
&mut self,
public_key: PublicKey,
announcement: Option<Announcement>,
) {
self.announcement.insert(public_key, announcement);
}
}

View File

@@ -1,9 +1,19 @@
use std::collections::{HashMap, HashSet};
use std::sync::OnceLock;
use nostr_sdk::prelude::*;
static INITIALIZED_AT: OnceLock<Timestamp> = OnceLock::new();
pub fn initialized_at() -> &'static Timestamp {
INITIALIZED_AT.get_or_init(Timestamp::now)
}
#[derive(Debug, Clone, Default)]
pub struct EventTracker {
/// Tracking events that have failed to unwrap
pub failed_unwrap_events: Vec<Event>,
/// Tracking events that have been resent by Coop in the current session
pub resent_ids: Vec<Output<EventId>>,
@@ -18,6 +28,10 @@ pub struct EventTracker {
}
impl EventTracker {
pub fn failed_unwrap_events(&self) -> &Vec<Event> {
&self.failed_unwrap_events
}
pub fn resent_ids(&self) -> &Vec<Output<EventId>> {
&self.resent_ids
}

View File

@@ -1,67 +0,0 @@
use std::sync::OnceLock;
use nostr_sdk::prelude::*;
use whoami::{devicename, platform};
mod constants;
mod paths;
mod state;
pub use constants::*;
pub use paths::*;
pub use state::*;
static APP_STATE: OnceLock<AppState> = OnceLock::new();
static APP_NAME: OnceLock<String> = OnceLock::new();
static NIP65_RELAYS: OnceLock<Vec<(RelayUrl, Option<RelayMetadata>)>> = OnceLock::new();
static NIP17_RELAYS: OnceLock<Vec<RelayUrl>> = OnceLock::new();
/// Initialize the application state.
pub fn app_state() -> &'static AppState {
APP_STATE.get_or_init(AppState::new)
}
pub fn app_name() -> &'static String {
APP_NAME.get_or_init(|| {
let devicename = devicename();
let platform = platform();
format!("{CLIENT_NAME} on {platform} ({devicename})")
})
}
/// Default NIP-65 Relays. Used for new account
pub fn default_nip65_relays() -> &'static Vec<(RelayUrl, Option<RelayMetadata>)> {
NIP65_RELAYS.get_or_init(|| {
vec![
(
RelayUrl::parse("wss://nostr.mom").unwrap(),
Some(RelayMetadata::Read),
),
(
RelayUrl::parse("wss://nostr.bitcoiner.social").unwrap(),
Some(RelayMetadata::Read),
),
(
RelayUrl::parse("wss://nostr.oxtr.dev").unwrap(),
Some(RelayMetadata::Write),
),
(
RelayUrl::parse("wss://nostr.fmt.wiz.biz").unwrap(),
Some(RelayMetadata::Write),
),
(RelayUrl::parse("wss://relay.primal.net").unwrap(), None),
(RelayUrl::parse("wss://relay.damus.io").unwrap(), None),
]
})
}
/// Default NIP-17 Relays. Used for new account
pub fn default_nip17_relays() -> &'static Vec<RelayUrl> {
NIP17_RELAYS.get_or_init(|| {
vec![
RelayUrl::parse("wss://nip17.com").unwrap(),
RelayUrl::parse("wss://auth.nostr1.com").unwrap(),
]
})
}

View File

@@ -1,48 +0,0 @@
use std::sync::Arc;
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Deserialize, Serialize)]
pub enum SignerKind {
Encryption,
#[default]
User,
Auto,
}
#[derive(Debug, Clone, Default)]
pub struct Device {
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
///
/// Client Key that used for communication between devices
pub client: Option<Arc<dyn NostrSigner>>,
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
///
/// Encryption key used for encryption and decryption instead of the user's identity
pub encryption: Option<Arc<dyn NostrSigner>>,
}
impl Device {
pub fn new() -> Self {
Self {
client: None,
encryption: None,
}
}
pub fn set_client<T>(&mut self, keys: T)
where
T: NostrSigner,
{
self.client = Some(Arc::new(keys));
}
pub fn set_encryption<T>(&mut self, keys: T)
where
T: NostrSigner,
{
self.encryption = Some(Arc::new(keys));
}
}

View File

@@ -1,31 +0,0 @@
use flume::{Receiver, Sender};
use nostr_sdk::prelude::*;
#[derive(Debug, Clone)]
pub struct Ingester {
rx: Receiver<PublicKey>,
tx: Sender<PublicKey>,
}
impl Default for Ingester {
fn default() -> Self {
Self::new()
}
}
impl Ingester {
pub fn new() -> Self {
let (tx, rx) = flume::bounded::<PublicKey>(1024);
Self { rx, tx }
}
pub fn receiver(&self) -> &Receiver<PublicKey> {
&self.rx
}
pub async fn send(&self, public_key: PublicKey) {
if let Err(e) = self.tx.send_async(public_key).await {
log::error!("Failed to send public key: {e}");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,169 +0,0 @@
use flume::{Receiver, Sender};
use nostr_sdk::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct NewMessage {
pub gift_wrap: EventId,
pub rumor: UnsignedEvent,
}
impl NewMessage {
pub fn new(gift_wrap: EventId, rumor: UnsignedEvent) -> Self {
Self { gift_wrap, rumor }
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct AuthRequest {
pub url: RelayUrl,
pub challenge: String,
pub sending: bool,
}
impl AuthRequest {
pub fn new(challenge: impl Into<String>, url: RelayUrl) -> Self {
Self {
challenge: challenge.into(),
sending: false,
url,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum UnwrappingStatus {
#[default]
Initialized,
Processing,
Complete,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Announcement {
id: EventId,
client: String,
public_key: PublicKey,
}
impl Announcement {
pub fn new(id: EventId, client_name: String, public_key: PublicKey) -> Self {
Self {
id,
client: client_name,
public_key,
}
}
pub fn id(&self) -> EventId {
self.id
}
pub fn public_key(&self) -> PublicKey {
self.public_key
}
pub fn client(&self) -> &str {
self.client.as_str()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Response {
payload: String,
public_key: PublicKey,
}
impl Response {
pub fn new(payload: String, public_key: PublicKey) -> Self {
Self {
payload,
public_key,
}
}
pub fn public_key(&self) -> PublicKey {
self.public_key
}
pub fn payload(&self) -> &str {
self.payload.as_str()
}
}
/// Signals sent through the global event channel to notify UI
#[derive(Debug)]
pub enum SignalKind {
/// NIP-4e
///
/// A signal to notify UI that the user has not set encryption keys yet
EncryptionNotSet,
/// NIP-4e
///
/// A signal to notify UI that the user has set encryption keys
EncryptionSet(Announcement),
/// NIP-4e
///
/// A signal to notify UI that the user has responded to an encryption request
EncryptionResponse(Response),
/// NIP-4e
///
/// A signal to notify UI that the user has requested encryption keys from other devices
EncryptionRequest(Announcement),
/// A signal to notify UI that the client's signer has been set
SignerSet(PublicKey),
/// A signal to notify UI that the relay requires authentication
Auth(AuthRequest),
/// A signal to notify UI that a new profile has been received
NewProfile(Profile),
/// A signal to notify UI that a new gift wrap event has been received
NewMessage(NewMessage),
/// A signal to notify UI that no messaging relays for current user was found
MessagingRelaysNotFound,
/// A signal to notify UI that no gossip relays for current user was found
GossipRelaysNotFound,
/// A signal to notify UI that gift wrap status has changed
GiftWrapStatus(UnwrappingStatus),
}
#[derive(Debug, Clone)]
pub struct Signal {
rx: Receiver<SignalKind>,
tx: Sender<SignalKind>,
}
impl Default for Signal {
fn default() -> Self {
Self::new()
}
}
impl Signal {
pub fn new() -> Self {
let (tx, rx) = flume::bounded::<SignalKind>(2048);
Self { rx, tx }
}
pub fn receiver(&self) -> &Receiver<SignalKind> {
&self.rx
}
pub fn sender(&self) -> &Sender<SignalKind> {
&self.tx
}
pub async fn send(&self, kind: SignalKind) {
if let Err(e) = self.tx.send_async(kind).await {
log::error!("Failed to send signal: {e}");
}
}
}

View File

@@ -541,13 +541,10 @@ impl Element for TextElement {
let mut bounds = bounds;
let (display_text, text_color) = if is_empty {
(
Rope::from_str_small(placeholder.as_str()),
cx.theme().text_muted,
)
(Rope::from(placeholder.as_str()), cx.theme().text_muted)
} else if state.masked {
(
Rope::from_str_small("*".repeat(text.chars_count()).as_str()),
Rope::from("*".repeat(text.chars_count()).as_str()),
cx.theme().text,
)
} else {

View File

@@ -718,7 +718,7 @@ impl InputState {
/// Set the default value of the input field.
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
let text: SharedString = value.into();
self.text = Rope::from_str_small(text.as_str());
self.text = Rope::from(text.as_str());
self.text_wrapper.set_default_text(&self.text);
self
}
@@ -2099,9 +2099,7 @@ impl EntityInputHandler for InputState {
.unwrap_or(self.selected_range.into());
let old_text = self.text.clone();
let executor = cx.background_executor();
self.text.replace(range.clone(), new_text, executor);
self.text.replace(range.clone(), new_text);
let mut new_offset = (range.start + new_text.len()).min(self.text.len());
@@ -2115,7 +2113,7 @@ impl EntityInputHandler for InputState {
if !self.mask_pattern.is_none() {
let mask_text = self.mask_pattern.mask(&pending_text);
self.text = Rope::from_str_small(mask_text.as_str());
self.text = Rope::from(mask_text.as_str());
let new_text_len =
(new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
new_offset = (range.start + new_text_len).min(mask_text.len());
@@ -2123,13 +2121,8 @@ impl EntityInputHandler for InputState {
}
self.push_history(&old_text, &range, new_text);
self.text_wrapper.update(
&self.text,
&range,
&Rope::from_str_small(new_text),
false,
cx,
);
self.text_wrapper
.update(&self.text, &range, &Rope::from(new_text), false, cx);
self.selected_range = (new_offset..new_offset).into();
self.ime_marked_range.take();
self.update_preferred_column();
@@ -2161,9 +2154,7 @@ impl EntityInputHandler for InputState {
.unwrap_or(self.selected_range.into());
let old_text = self.text.clone();
let executor = cx.background_executor();
self.text.replace(range.clone(), new_text, executor);
self.text.replace(range.clone(), new_text);
if self.mode.is_single_line() {
let pending_text = self.text.to_string();
@@ -2174,13 +2165,9 @@ impl EntityInputHandler for InputState {
}
self.push_history(&old_text, &range, new_text);
self.text_wrapper.update(
&self.text,
&range,
&Rope::from_str_small(new_text),
false,
cx,
);
self.text_wrapper
.update(&self.text, &range, &Rope::from(new_text), false, cx);
if new_text.is_empty() {
// Cancel selection, when cancel IME input.
self.selected_range = (range.start..range.start).into();
@@ -2195,6 +2182,7 @@ impl EntityInputHandler for InputState {
.into();
}
self.mode.update_auto_grow(&self.text_wrapper);
cx.emit(InputEvent::Change);
cx.notify();
}

View File

@@ -88,7 +88,7 @@ impl RenderOnce for MenuItemElement {
h_flex()
.id(self.id)
.group(&self.group_name)
.gap_x_1()
.gap_x_2()
.py_1()
.px_2()
.text_base()

View File

@@ -861,8 +861,8 @@ impl PopupMenu {
fn render_icon(
has_icon: bool,
icon: Option<Icon>,
_: &mut Window,
_: &mut Context<Self>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
if !has_icon {
return None;
@@ -873,7 +873,7 @@ impl PopupMenu {
.h_3p5()
.justify_center()
.text_sm()
.when_some(icon, |this, icon| this.child(icon.clone().xsmall()));
.when_some(icon, |this, icon| this.child(icon.clone().small()));
Some(icon)
}

View File

@@ -68,15 +68,11 @@ keyring_disable:
label:
en: "Keyring is disabled"
body_1:
en: "Coop cannot access the Keyring Service on your system."
en: "Coop cannot access the Keyring Service on your system. By design, Coop uses Keyring to store your credentials."
body_2:
en: "By design, Coop uses Keyring to store your credentials."
body_3:
en: "Without access to Keyring, Coop will store your credentials as plain text."
body_4:
body_3:
en: "If you want to store your credentials in the Keyring, please enable Keyring and allow Coop to access it."
body_5:
en: "By clicking continue, you agree to store your credentials as plain text."
pending_encryption:
label:
@@ -94,14 +90,6 @@ request_encryption:
body:
en: "You've requested for the encryption Key from:"
encryption:
notice:
en: "Encryption Key are being generated"
success:
en: "Encryption Key have been successfully set up"
reinit:
en: "Encryption Key are being reinitialized"
auto_update:
updating:
en: "Installing the new update..."