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:
321
Cargo.lock
generated
321
Cargo.lock
generated
@@ -7,13 +7,18 @@ name = "account"
|
|||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"common",
|
||||||
"gpui",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"nostr",
|
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"states",
|
"state",
|
||||||
|
"theme",
|
||||||
|
"ui",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -125,7 +130,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -369,7 +374,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -445,7 +450,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -524,7 +529,7 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"states",
|
"state",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -621,7 +626,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"shlex",
|
"shlex",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -641,7 +646,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"shlex",
|
"shlex",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -773,7 +778,7 @@ checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -873,7 +878,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -942,7 +947,7 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"toml 0.8.23",
|
"toml 0.8.23",
|
||||||
]
|
]
|
||||||
@@ -1036,12 +1041,13 @@ dependencies = [
|
|||||||
"account",
|
"account",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"common",
|
"common",
|
||||||
|
"encryption",
|
||||||
|
"flume",
|
||||||
"futures",
|
"futures",
|
||||||
"fuzzy-matcher",
|
"fuzzy-matcher",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"nostr",
|
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"person",
|
"person",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1049,7 +1055,7 @@ dependencies = [
|
|||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"states",
|
"state",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1061,12 +1067,12 @@ dependencies = [
|
|||||||
"chat",
|
"chat",
|
||||||
"common",
|
"common",
|
||||||
"emojis",
|
"emojis",
|
||||||
|
"encryption",
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui_tokio",
|
"gpui_tokio",
|
||||||
"indexset",
|
"indexset",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"nostr",
|
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"person",
|
"person",
|
||||||
@@ -1076,7 +1082,7 @@ dependencies = [
|
|||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"states",
|
"state",
|
||||||
"theme",
|
"theme",
|
||||||
"ui",
|
"ui",
|
||||||
]
|
]
|
||||||
@@ -1199,7 +1205,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1237,19 +1243,18 @@ version = "0.2.11"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"dirs 5.0.1",
|
||||||
"futures",
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"nostr",
|
"nostr",
|
||||||
"nostr-connect",
|
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"qrcode",
|
"qrcode",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"states",
|
"whoami",
|
||||||
"webbrowser",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1316,8 +1321,8 @@ dependencies = [
|
|||||||
"chat",
|
"chat",
|
||||||
"chat_ui",
|
"chat_ui",
|
||||||
"common",
|
"common",
|
||||||
"dirs 5.0.1",
|
"encryption",
|
||||||
"flume",
|
"encryption_ui",
|
||||||
"futures",
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui_tokio",
|
"gpui_tokio",
|
||||||
@@ -1326,11 +1331,11 @@ dependencies = [
|
|||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"key_store",
|
"key_store",
|
||||||
"log",
|
"log",
|
||||||
"nostr",
|
|
||||||
"nostr-connect",
|
"nostr-connect",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"oneshot",
|
"oneshot",
|
||||||
"person",
|
"person",
|
||||||
|
"relay_auth",
|
||||||
"reqwest_client",
|
"reqwest_client",
|
||||||
"rust-i18n",
|
"rust-i18n",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1338,7 +1343,7 @@ dependencies = [
|
|||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"states",
|
"state",
|
||||||
"theme",
|
"theme",
|
||||||
"title_bar",
|
"title_bar",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -1617,17 +1622,17 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_refineable"
|
name = "derive_refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1706,7 +1711,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1819,6 +1824,49 @@ dependencies = [
|
|||||||
"cfg-if",
|
"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]]
|
[[package]]
|
||||||
name = "endi"
|
name = "endi"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -1843,7 +1891,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1863,7 +1911,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1986,7 +2034,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2180,7 +2228,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2334,7 +2382,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2520,7 +2568,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2617,18 +2665,18 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_macros"
|
name = "gpui_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_tokio"
|
name = "gpui_tokio"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -2857,7 +2905,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "http_client"
|
name = "http_client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
@@ -2882,7 +2930,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "http_client_tls"
|
name = "http_client_tls"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -3221,7 +3269,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3253,9 +3301,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iri-string"
|
name = "iri-string"
|
||||||
version = "0.7.8"
|
version = "0.7.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
|
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3369,16 +3417,15 @@ name = "key_store"
|
|||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"common",
|
||||||
"futures",
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"nostr",
|
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"states",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3647,6 +3694,15 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mach2"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "malloc_buf"
|
name = "malloc_buf"
|
||||||
version = "0.0.6"
|
version = "0.0.6"
|
||||||
@@ -3679,7 +3735,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
@@ -3928,8 +3984,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.43.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
|
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -3952,8 +4008,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.43.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
|
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3964,8 +4020,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.43.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
|
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -3975,16 +4031,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip"
|
name = "nostr-gossip"
|
||||||
version = "0.43.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
|
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nostr",
|
"nostr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip-memory"
|
name = "nostr-gossip-memory"
|
||||||
version = "0.43.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
|
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -3995,8 +4051,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.43.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
|
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"flume",
|
"flume",
|
||||||
@@ -4009,8 +4065,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-relay-pool"
|
name = "nostr-relay-pool"
|
||||||
version = "0.43.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
|
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -4026,8 +4082,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.43.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d79245efe2c97424acc93da687d5f8077236a74b"
|
source = "git+https://github.com/rust-nostr/nostr#c1a24383cd361a408fc53eeac7c1708a3b589194"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4114,7 +4170,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4391,7 +4447,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4538,7 +4594,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "perf"
|
name = "perf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4554,11 +4610,10 @@ dependencies = [
|
|||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"nostr",
|
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"states",
|
"state",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4591,7 +4646,7 @@ dependencies = [
|
|||||||
"phf_shared",
|
"phf_shared",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4626,7 +4681,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4757,7 +4812,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4788,7 +4843,7 @@ dependencies = [
|
|||||||
"proc-macro-error-attr2",
|
"proc-macro-error-attr2",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4816,7 +4871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
|
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4937,9 +4992,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.41"
|
version = "1.0.42"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@@ -5159,13 +5214,13 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
]
|
]
|
||||||
@@ -5199,6 +5254,23 @@ version = "0.8.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "relay_auth"
|
||||||
|
version = "0.2.11"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"common",
|
||||||
|
"gpui",
|
||||||
|
"log",
|
||||||
|
"nostr-sdk",
|
||||||
|
"settings",
|
||||||
|
"smallvec",
|
||||||
|
"smol",
|
||||||
|
"state",
|
||||||
|
"theme",
|
||||||
|
"ui",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.24"
|
version = "0.12.24"
|
||||||
@@ -5246,7 +5318,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest_client"
|
name = "reqwest_client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5300,11 +5372,11 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rope"
|
name = "rope"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"gpui",
|
|
||||||
"log",
|
"log",
|
||||||
|
"rayon",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"util",
|
"util",
|
||||||
@@ -5336,7 +5408,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rust-embed-utils",
|
"rust-embed-utils",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5379,7 +5451,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5464,9 +5536,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.34"
|
version = "0.23.35"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
|
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"log",
|
"log",
|
||||||
@@ -5624,9 +5696,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "1.0.5"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1317c3bf3e7df961da95b0a56a172a02abead31276215a0497241a7624b487ce"
|
checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
@@ -5638,14 +5710,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars_derive"
|
name = "schemars_derive"
|
||||||
version = "1.0.5"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f760a6150d45dd66ec044983c124595ae76912e77ed0b44124cb3e415cce5d9"
|
checksum = "301858a4023d78debd2353c7426dc486001bddc91ae31a76fb1f55132f7e2633"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde_derive_internals",
|
"serde_derive_internals",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5766,7 +5838,7 @@ checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "semantic_version"
|
name = "semantic_version"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5805,7 +5877,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5816,7 +5888,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5863,7 +5935,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5921,7 +5993,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"states",
|
"state",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6125,16 +6197,17 @@ checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-error2",
|
"proc-macro-error2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "states"
|
name = "state"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"dirs 5.0.1",
|
"common",
|
||||||
"flume",
|
"event-listener 5.4.1",
|
||||||
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"nostr-gossip-memory",
|
"nostr-gossip-memory",
|
||||||
"nostr-lmdb",
|
"nostr-lmdb",
|
||||||
@@ -6142,8 +6215,8 @@ dependencies = [
|
|||||||
"rustls",
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"whoami",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6189,7 +6262,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6201,7 +6274,7 @@ dependencies = [
|
|||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6213,12 +6286,11 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "sum_tree"
|
name = "sum_tree"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"futures",
|
|
||||||
"futures-lite 1.13.0",
|
|
||||||
"log",
|
"log",
|
||||||
|
"rayon",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6339,9 +6411,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.108"
|
version = "2.0.109"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -6374,7 +6446,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6538,7 +6610,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6549,7 +6621,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6677,7 +6749,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6897,7 +6969,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7201,7 +7273,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "util"
|
name = "util"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
@@ -7216,6 +7288,7 @@ dependencies = [
|
|||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
"mach2",
|
||||||
"nix 0.29.0",
|
"nix 0.29.0",
|
||||||
"regex",
|
"regex",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
@@ -7236,11 +7309,11 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "util_macros"
|
name = "util_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#50504793e6562b239dcd9399bc668445fae55f29"
|
source = "git+https://github.com/zed-industries/zed#784fdcaee3be7bbcf9511097cf620256dc2f7ef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"perf",
|
"perf",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7438,7 +7511,7 @@ dependencies = [
|
|||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -7808,7 +7881,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7819,7 +7892,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7830,7 +7903,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7841,7 +7914,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8437,7 +8510,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8484,7 +8557,7 @@ dependencies = [
|
|||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
"zbus_names",
|
"zbus_names",
|
||||||
"zvariant",
|
"zvariant",
|
||||||
"zvariant_utils",
|
"zvariant_utils",
|
||||||
@@ -8632,7 +8705,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8652,7 +8725,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8673,7 +8746,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8706,7 +8779,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8757,7 +8830,7 @@ dependencies = [
|
|||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
"zvariant_utils",
|
"zvariant_utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8770,6 +8843,6 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"syn 2.0.108",
|
"syn 2.0.109",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ gpui_tokio = { git = "https://github.com/zed-industries/zed" }
|
|||||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
|
||||||
# Nostr
|
# Nostr
|
||||||
nostr = { git = "https://github.com/rust-nostr/nostr" }
|
|
||||||
nostr-lmdb = { 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-connect = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-gossip-memory = { 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
|
# Others
|
||||||
anyhow = "1.0.44"
|
anyhow = "1.0.44"
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
dirs = "5.0"
|
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
states = { path = "../states" }
|
state = { path = "../state" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
common = { path = "../common" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
ui = { path = "../ui" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|
||||||
nostr.workspace = true
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
|||||||
@@ -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 gpui::{App, AppContext, Context, Entity, Global, Task};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use state::NostrRegistry;
|
||||||
|
|
||||||
pub fn init(public_key: PublicKey, cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
Account::set_global(cx.new(|cx| Account::new(public_key, cx)), cx);
|
Account::set_global(cx.new(Account::new), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GlobalAccount(Entity<Account>);
|
struct GlobalAccount(Entity<Account>);
|
||||||
@@ -12,10 +18,24 @@ impl Global for GlobalAccount {}
|
|||||||
|
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
/// The public key of the 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 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 {
|
impl Account {
|
||||||
@@ -35,20 +55,152 @@ impl Account {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the global account instance
|
/// 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));
|
cx.set_global(GlobalAccount(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new account instance
|
/// 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 {
|
Self {
|
||||||
public_key,
|
public_key: None,
|
||||||
_tasks: smallvec![],
|
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
|
/// Get the public key of the account
|
||||||
pub fn public_key(&self) -> PublicKey {
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use rust_embed::RustEmbed;
|
|||||||
#[derive(RustEmbed)]
|
#[derive(RustEmbed)]
|
||||||
#[folder = "../../assets"]
|
#[folder = "../../assets"]
|
||||||
#[include = "fonts/**/*"]
|
#[include = "fonts/**/*"]
|
||||||
#[include = "brand/*"]
|
#[include = "brand/**/*"]
|
||||||
#[include = "icons/**/*"]
|
#[include = "icons/**/*"]
|
||||||
#[exclude = "*.DS_Store"]
|
#[exclude = "*.DS_Store"]
|
||||||
pub struct Assets;
|
pub struct Assets;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ publish.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
states = { path = "../states" }
|
state = { path = "../state" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui_tokio.workspace = true
|
gpui_tokio.workspace = true
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||||
|
use common::BOOTSTRAP_RELAYS;
|
||||||
use gpui::http_client::{AsyncBody, HttpClient};
|
use gpui::http_client::{AsyncBody, HttpClient};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task,
|
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task,
|
||||||
@@ -13,7 +14,7 @@ use semver::Version;
|
|||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use smol::fs::File;
|
use smol::fs::File;
|
||||||
use smol::process::Command;
|
use smol::process::Command;
|
||||||
use states::{app_state, BOOTSTRAP_RELAYS};
|
use state::NostrRegistry;
|
||||||
|
|
||||||
const APP_PUBKEY: &str = "npub1y9jvl5vznq49eh9f2gj7679v4042kj80lp7p8fte3ql2cr7hty7qsyca8q";
|
const APP_PUBKEY: &str = "npub1y9jvl5vznq49eh9f2gj7679v4042kj80lp7p8fte3ql2cr7hty7qsyca8q";
|
||||||
|
|
||||||
@@ -230,8 +231,10 @@ impl AutoUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe_to_updates(cx: &App) -> Task<()> {
|
fn subscribe_to_updates(cx: &App) -> Task<()> {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
let app_pubkey = PublicKey::parse(APP_PUBKEY).unwrap();
|
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>> {
|
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 {
|
cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
let app_pubkey = PublicKey::parse(APP_PUBKEY).unwrap();
|
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>) {
|
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 http_client = cx.http_client();
|
||||||
let ids = ids.to_vec();
|
let ids = ids.to_vec();
|
||||||
|
|
||||||
let task: Task<Result<(InstallerDir, PathBuf), Error>> = cx.background_spawn(async move {
|
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 app_pubkey = PublicKey::parse(APP_PUBKEY).unwrap();
|
||||||
let os = std::env::consts::OS;
|
let os = std::env::consts::OS;
|
||||||
|
|
||||||
|
|||||||
@@ -6,20 +6,22 @@ publish.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
states = { path = "../states" }
|
state = { path = "../state" }
|
||||||
account = { path = "../account" }
|
account = { path = "../account" }
|
||||||
|
encryption = { path = "../encryption" }
|
||||||
person = { path = "../person" }
|
person = { path = "../person" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr.workspace = true
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
flume.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,53 @@
|
|||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::{HashMap, HashSet};
|
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 account::Account;
|
||||||
use anyhow::Error;
|
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||||
use common::event::EventUtils;
|
use common::{EventUtils, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT};
|
||||||
|
use encryption::Encryption;
|
||||||
|
use flume::Sender;
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
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 nostr_sdk::prelude::*;
|
||||||
use room::RoomKind;
|
pub use room::*;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
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;
|
mod message;
|
||||||
|
mod room;
|
||||||
pub mod message;
|
|
||||||
pub mod room;
|
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
ChatRegistry::set_global(cx.new(ChatRegistry::new), cx);
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum ChatEvent {
|
pub enum ChatEvent {
|
||||||
OpenRoom(u64),
|
OpenRoom(u64),
|
||||||
@@ -29,43 +55,271 @@ pub enum ChatEvent {
|
|||||||
NewChatRequest(RoomKind),
|
NewChatRequest(RoomKind),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GlobalChatRegistry(Entity<ChatRegistry>);
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum Signal {
|
||||||
impl Global for GlobalChatRegistry {}
|
Loading(bool),
|
||||||
|
Message(NewMessage),
|
||||||
pub struct ChatRegistry {
|
Eose,
|
||||||
/// 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]>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<ChatEvent> for ChatRegistry {}
|
impl EventEmitter<ChatEvent> for ChatRegistry {}
|
||||||
|
|
||||||
impl ChatRegistry {
|
impl ChatRegistry {
|
||||||
/// Retrieve the global registry state
|
/// Retrieve the global chat registry state
|
||||||
pub fn global(cx: &App) -> Entity<Self> {
|
pub fn global(cx: &App) -> Entity<Self> {
|
||||||
cx.global::<GlobalChatRegistry>().0.clone()
|
cx.global::<GlobalChatRegistry>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the global registry instance
|
/// Set the global chat registry instance
|
||||||
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
|
fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||||
cx.set_global(GlobalChatRegistry(state));
|
cx.set_global(GlobalChatRegistry(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new registry instance
|
/// Create a new chat registry instance
|
||||||
pub(crate) fn new(_cx: &mut Context<Self>) -> Self {
|
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 {
|
Self {
|
||||||
rooms: vec![],
|
rooms: vec![],
|
||||||
loading: true,
|
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
|
/// Set the loading status of the chat registry
|
||||||
pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
|
pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
|
||||||
self.loading = loading;
|
self.loading = loading;
|
||||||
@@ -147,97 +401,19 @@ impl ChatRegistry {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load all rooms from the database.
|
/// Push a new room to the chat registry
|
||||||
pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn push_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) {
|
||||||
log::info!("Starting to load chat rooms...");
|
let id = room.read(cx).id;
|
||||||
|
|
||||||
// Get the contact bypass setting
|
if !self.rooms.iter().any(|r| r.read(cx).id == id) {
|
||||||
let bypass_setting = AppSettings::get_contact_bypass(cx);
|
self.add_room(room, cx);
|
||||||
|
}
|
||||||
|
|
||||||
let task: Task<Result<HashSet<Room>, Error>> = cx.background_spawn(async move {
|
cx.emit(ChatEvent::OpenRoom(id));
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
let mut room_map: HashMap<u64, usize> = self
|
||||||
.rooms
|
.rooms
|
||||||
.iter()
|
.iter()
|
||||||
@@ -264,18 +440,111 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push a new room to the chat registry
|
/// Load all rooms from the database.
|
||||||
pub fn push_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) {
|
pub fn get_rooms(&mut self, cx: &mut Context<Self>) {
|
||||||
let id = room.read(cx).id;
|
let task = self.create_get_rooms_task(cx);
|
||||||
|
|
||||||
if !self.rooms.iter().any(|r| r.read(cx).id == id) {
|
self._tasks.push(
|
||||||
self.add_room(room, cx);
|
// Run and finished in the background
|
||||||
}
|
cx.spawn(async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
cx.emit(ChatEvent::OpenRoom(id));
|
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>) {
|
pub fn refresh_rooms(&mut self, ids: Option<Vec<u64>>, cx: &mut Context<Self>) {
|
||||||
if let Some(ids) = ids {
|
if let Some(ids) = ids {
|
||||||
for room in self.rooms.iter() {
|
for room in self.rooms.iter() {
|
||||||
@@ -292,15 +561,15 @@ impl ChatRegistry {
|
|||||||
///
|
///
|
||||||
/// If the room doesn't exist, it will be created.
|
/// If the room doesn't exist, it will be created.
|
||||||
/// Updates room ordering based on the most recent messages.
|
/// Updates room ordering based on the most recent messages.
|
||||||
pub fn new_message(&mut self, msg: NewMessage, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
|
||||||
let id = msg.rumor.uniq_id();
|
let id = message.rumor.uniq_id();
|
||||||
let author = msg.rumor.pubkey;
|
let author = message.rumor.pubkey;
|
||||||
let account = Account::global(cx);
|
let account = Account::global(cx);
|
||||||
|
|
||||||
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
|
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
|
||||||
let is_new_event = msg.rumor.created_at > room.read(cx).created_at;
|
let is_new_event = message.rumor.created_at > room.read(cx).created_at;
|
||||||
let created_at = msg.rumor.created_at;
|
let created_at = message.rumor.created_at;
|
||||||
let event_for_emit = msg.rumor.clone();
|
let event_for_emit = message.rumor.clone();
|
||||||
|
|
||||||
// Update room
|
// Update room
|
||||||
room.update(cx, |this, cx| {
|
room.update(cx, |this, cx| {
|
||||||
@@ -314,26 +583,170 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Emit the new message to the room
|
// Emit the new message to the room
|
||||||
let event_to_emit = event_for_emit.clone();
|
this.emit_message(message.gift_wrap, event_for_emit.clone(), cx);
|
||||||
cx.defer_in(window, move |this, _window, cx| {
|
|
||||||
this.emit_message(msg.gift_wrap, event_to_emit, cx);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resort all rooms in the registry by their created at (after updated)
|
// Resort all rooms in the registry by their created at (after updated)
|
||||||
if is_new_event {
|
if is_new_event {
|
||||||
cx.defer_in(window, |this, _window, cx| {
|
self.sort(cx);
|
||||||
this.sort(cx);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Push the new room to the front of the list
|
// Push the new room to the front of the list
|
||||||
self.add_room(cx.new(|_| Room::from(&msg.rumor)), cx);
|
self.add_room(cx.new(|_| Room::from(&message.rumor)), cx);
|
||||||
|
|
||||||
// Notify the UI about the new room
|
// Notify the UI about the new room
|
||||||
cx.defer_in(window, move |_this, _window, cx| {
|
cx.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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ use std::hash::Hash;
|
|||||||
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct NewMessage {
|
||||||
|
pub gift_wrap: EventId,
|
||||||
|
pub rumor: UnsignedEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewMessage {
|
||||||
|
pub fn new(gift_wrap: EventId, rumor: UnsignedEvent) -> Self {
|
||||||
|
Self { gift_wrap, rumor }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
User(RenderedMessage),
|
User(RenderedMessage),
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use account::Account;
|
use account::Account;
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use common::display::RenderedProfile;
|
use common::{EventUtils, RenderedProfile};
|
||||||
use common::event::EventUtils;
|
use encryption::{Encryption, SignerKind};
|
||||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task};
|
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct SendOptions {
|
pub struct SendOptions {
|
||||||
@@ -159,30 +161,6 @@ impl Eq for Room {}
|
|||||||
|
|
||||||
impl EventEmitter<RoomSignal> 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 {
|
impl From<&UnsignedEvent> for Room {
|
||||||
fn from(val: &UnsignedEvent) -> Self {
|
fn from(val: &UnsignedEvent) -> Self {
|
||||||
let id = val.uniq_id();
|
let id = val.uniq_id();
|
||||||
@@ -209,15 +187,7 @@ impl From<&UnsignedEvent> for Room {
|
|||||||
|
|
||||||
impl Room {
|
impl Room {
|
||||||
/// Constructs a new room with the given receiver and tags.
|
/// Constructs a new room with the given receiver and tags.
|
||||||
pub async fn new(subject: Option<String>, receivers: Vec<PublicKey>) -> Result<Self, Error> {
|
pub fn new(subject: Option<String>, author: PublicKey, receivers: Vec<PublicKey>) -> Self {
|
||||||
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"));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert receiver's public keys into tags
|
// Convert receiver's public keys into tags
|
||||||
let mut tags: Tags = Tags::from_list(
|
let mut tags: Tags = Tags::from_list(
|
||||||
receivers
|
receivers
|
||||||
@@ -235,12 +205,12 @@ impl Room {
|
|||||||
|
|
||||||
let mut event = EventBuilder::new(Kind::PrivateDirectMessage, "")
|
let mut event = EventBuilder::new(Kind::PrivateDirectMessage, "")
|
||||||
.tags(tags)
|
.tags(tags)
|
||||||
.build(public_key);
|
.build(author);
|
||||||
|
|
||||||
// Generate event ID
|
// Generate event ID
|
||||||
event.ensure_id();
|
event.ensure_id();
|
||||||
|
|
||||||
Ok(Room::from(&event))
|
Room::from(&event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the kind of the room and returns the modified room
|
/// Sets the kind of the room and returns the modified room
|
||||||
@@ -292,11 +262,11 @@ impl Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the display image for the 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() {
|
if !self.is_group() {
|
||||||
self.display_member(cx).avatar(proxy)
|
self.display_member(cx).avatar(proxy)
|
||||||
} else {
|
} 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
|
/// Get messaging relays and encryption keys announcement for each member
|
||||||
pub fn connect(&self, cx: &App) -> Task<Result<(), Error>> {
|
pub fn connect(&self, cx: &App) -> Task<Result<(), Error>> {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
let members = self.members();
|
let members = self.members();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
@@ -396,10 +367,11 @@ impl Room {
|
|||||||
|
|
||||||
/// Get all messages belonging to the room
|
/// Get all messages belonging to the room
|
||||||
pub fn get_messages(&self, cx: &App) -> Task<Result<Vec<UnsignedEvent>, Error>> {
|
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();
|
let conversation_id = self.id.to_string();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::ApplicationSpecificData)
|
.kind(Kind::ApplicationSpecificData)
|
||||||
.custom_tag(SingleLetterTag::lowercase(Alphabet::C), conversation_id);
|
.custom_tag(SingleLetterTag::lowercase(Alphabet::C), conversation_id);
|
||||||
@@ -419,10 +391,6 @@ impl Room {
|
|||||||
|
|
||||||
/// Create a new message event (unsigned)
|
/// Create a new message event (unsigned)
|
||||||
pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent {
|
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
|
// Get current user
|
||||||
let account = Account::global(cx);
|
let account = Account::global(cx);
|
||||||
let public_key = account.read(cx).public_key();
|
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
|
// NOTE: current user will be removed from the list of receivers
|
||||||
for member in self.members.iter() {
|
for member in self.members.iter() {
|
||||||
// Get relay hint if available
|
// Get relay hint if available
|
||||||
let relay_url = relay_cache
|
let relay_url = None;
|
||||||
.get(member)
|
|
||||||
.and_then(|urls| urls.iter().nth(0).cloned());
|
|
||||||
|
|
||||||
// Construct a public key tag with relay hint
|
// Construct a public key tag with relay hint
|
||||||
let tag = TagStandard::PublicKey {
|
let tag = TagStandard::PublicKey {
|
||||||
@@ -475,12 +441,12 @@ impl Room {
|
|||||||
|
|
||||||
// Construct a direct message event
|
// 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)
|
let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content)
|
||||||
.tags(tags)
|
.tags(tags)
|
||||||
.build(public_key);
|
.build(public_key);
|
||||||
|
|
||||||
// Generate event ID
|
// Ensure the event id has been generated
|
||||||
event.ensure_id();
|
event.ensure_id();
|
||||||
|
|
||||||
event
|
event
|
||||||
@@ -493,6 +459,14 @@ impl Room {
|
|||||||
opts: &SendOptions,
|
opts: &SendOptions,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Task<Result<Vec<SendReport>, Error>> {
|
) -> 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 rumor = rumor.to_owned();
|
||||||
let opts = opts.to_owned();
|
let opts = opts.to_owned();
|
||||||
|
|
||||||
@@ -500,16 +474,12 @@ impl Room {
|
|||||||
let mut members = self.members();
|
let mut members = self.members();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let state = app_state();
|
|
||||||
let client = state.client();
|
|
||||||
let signer_kind = opts.signer_kind;
|
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
|
// 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()
|
signer.get_public_key().await.ok()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -523,14 +493,14 @@ impl Room {
|
|||||||
members.retain(|&pk| pk != user_pubkey);
|
members.retain(|&pk| pk != user_pubkey);
|
||||||
|
|
||||||
// Determine the signer will be used based on the provided options
|
// 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
|
// Collect the send reports
|
||||||
let mut reports: Vec<SendReport> = vec![];
|
let mut reports: Vec<SendReport> = vec![];
|
||||||
|
|
||||||
for member in members.into_iter() {
|
for member in members.into_iter() {
|
||||||
// Get user's messaging relays
|
// 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
|
// Check if there are any relays to send the message to
|
||||||
if urls.is_empty() {
|
if urls.is_empty() {
|
||||||
@@ -539,8 +509,8 @@ impl Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get user's encryption public key if available
|
// Get user's encryption public key if available
|
||||||
let encryption = announcement_cache
|
let encryption = cache
|
||||||
.get(&member)
|
.announcement(&member)
|
||||||
.and_then(|a| a.to_owned().map(|a| a.public_key()));
|
.and_then(|a| a.to_owned().map(|a| a.public_key()));
|
||||||
|
|
||||||
// Skip sending if using encryption signer but receiver's encryption keys not found
|
// 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 receiver = Self::select_receiver(&signer_kind, member, encryption)?;
|
||||||
let rumor = rumor.clone();
|
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
|
// 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
|
// Send the gift wrap event to the messaging relays
|
||||||
match client.send_event_to(urls, &event).await {
|
match client.send_event_to(urls, &event).await {
|
||||||
@@ -566,8 +542,7 @@ impl Room {
|
|||||||
if auth {
|
if auth {
|
||||||
// Wait for authenticated and resent event successfully
|
// Wait for authenticated and resent event successfully
|
||||||
for attempt in 0..=SEND_RETRY {
|
for attempt in 0..=SEND_RETRY {
|
||||||
let retry_manager = state.tracker().read().await;
|
let ids = tracker.resent_ids();
|
||||||
let ids = retry_manager.resent_ids();
|
|
||||||
|
|
||||||
// Check if event was successfully resent
|
// Check if event was successfully resent
|
||||||
if let Some(output) = ids.iter().find(|e| e.id() == &id).cloned() {
|
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 receiver = Self::select_receiver(&signer_kind, user_pubkey, encryption_pubkey)?;
|
||||||
let rumor = rumor.clone();
|
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
|
// 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
|
// Only send a backup message to current user if sent successfully to others
|
||||||
if opts.backup() && reports.iter().all(|r| r.is_sent_success()) {
|
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
|
// Check if there are any relays to send the event to
|
||||||
if urls.is_empty() {
|
if urls.is_empty() {
|
||||||
@@ -633,9 +614,12 @@ impl Room {
|
|||||||
reports: Vec<SendReport>,
|
reports: Vec<SendReport>,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Task<Result<Vec<SendReport>, Error>> {
|
) -> 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 {
|
cx.background_spawn(async move {
|
||||||
let states = app_state();
|
let cache = cache_manager.read().await;
|
||||||
let client = states.client();
|
|
||||||
let mut resend_reports = vec![];
|
let mut resend_reports = vec![];
|
||||||
|
|
||||||
for report in reports.into_iter() {
|
for report in reports.into_iter() {
|
||||||
@@ -664,7 +648,7 @@ impl Room {
|
|||||||
|
|
||||||
// Process the on hold event if it exists
|
// Process the on hold event if it exists
|
||||||
if let Some(event) = report.on_hold {
|
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
|
// Check if there are any relays to send the event to
|
||||||
if urls.is_empty() {
|
if urls.is_empty() {
|
||||||
@@ -702,15 +686,15 @@ impl Room {
|
|||||||
|
|
||||||
fn select_receiver(
|
fn select_receiver(
|
||||||
kind: &SignerKind,
|
kind: &SignerKind,
|
||||||
user: PublicKey,
|
members: PublicKey,
|
||||||
encryption: Option<PublicKey>,
|
encryption: Option<PublicKey>,
|
||||||
) -> Result<PublicKey, Error> {
|
) -> Result<PublicKey, Error> {
|
||||||
match kind {
|
match kind {
|
||||||
SignerKind::Encryption => {
|
SignerKind::Encryption => {
|
||||||
Ok(encryption.ok_or_else(|| anyhow!("Receiver's encryption key not found"))?)
|
Ok(encryption.ok_or_else(|| anyhow!("Receiver's encryption key not found"))?)
|
||||||
}
|
}
|
||||||
SignerKind::User => Ok(user),
|
SignerKind::User => Ok(members),
|
||||||
SignerKind::Auto => Ok(encryption.unwrap_or(user)),
|
SignerKind::Auto => Ok(encryption.unwrap_or(members)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
state = { path = "../state" }
|
||||||
ui = { path = "../ui" }
|
ui = { path = "../ui" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
states = { path = "../states" }
|
|
||||||
account = { path = "../account" }
|
account = { path = "../account" }
|
||||||
|
encryption = { path = "../encryption" }
|
||||||
person = { path = "../person" }
|
person = { path = "../person" }
|
||||||
chat = { path = "../chat" }
|
chat = { path = "../chat" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
@@ -17,7 +18,6 @@ settings = { path = "../settings" }
|
|||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui_tokio.workspace = true
|
gpui_tokio.workspace = true
|
||||||
|
|
||||||
nostr.workspace = true
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
use encryption::SignerKind;
|
||||||
use gpui::Action;
|
use gpui::Action;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use states::SignerKind;
|
|
||||||
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||||
#[action(namespace = chat, no_json)]
|
#[action(namespace = chat, no_json)]
|
||||||
|
|||||||
@@ -2,16 +2,15 @@ use std::collections::HashSet;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub use actions::*;
|
pub use actions::*;
|
||||||
use chat::message::{Message, RenderedMessage};
|
use chat::{Message, RenderedMessage, Room, RoomKind, RoomSignal, SendOptions, SendReport};
|
||||||
use chat::room::{Room, RoomKind, RoomSignal, SendOptions, SendReport};
|
use common::{nip96_upload, RenderedProfile, RenderedTimestamp};
|
||||||
use common::display::{RenderedProfile, RenderedTimestamp};
|
use encryption::SignerKind;
|
||||||
use common::nip96::nip96_upload;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext,
|
div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext,
|
||||||
ClipboardItem, Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable,
|
ClipboardItem, Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable,
|
||||||
InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit,
|
InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit,
|
||||||
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString, SharedUri,
|
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString,
|
||||||
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, Window,
|
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, Window,
|
||||||
};
|
};
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
@@ -22,7 +21,7 @@ use person::PersonRegistry;
|
|||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use states::{app_state, SignerKind};
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
@@ -147,16 +146,16 @@ impl ChatPanel {
|
|||||||
cx.subscribe_in(&room, window, move |this, _, signal, window, cx| {
|
cx.subscribe_in(&room, window, move |this, _, signal, window, cx| {
|
||||||
match signal {
|
match signal {
|
||||||
RoomSignal::NewMessage((gift_wrap_id, event)) => {
|
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 gift_wrap_id = gift_wrap_id.to_owned();
|
||||||
let message = Message::user(event.clone());
|
let message = Message::user(event.clone());
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let states = app_state();
|
let tracker = tracker.read().await;
|
||||||
let event_tracker = states.tracker().read().await;
|
|
||||||
let sent_ids = event_tracker.sent_ids();
|
|
||||||
|
|
||||||
this.update_in(cx, |this, _window, cx| {
|
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);
|
this.insert_message(message, false, cx);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -488,6 +487,9 @@ impl ChatPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
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
|
// Get the user's configured NIP96 server
|
||||||
let nip96_server = AppSettings::get_media_server(cx);
|
let nip96_server = AppSettings::get_media_server(cx);
|
||||||
|
|
||||||
@@ -503,9 +505,8 @@ impl ChatPanel {
|
|||||||
let path = paths.pop()?;
|
let path = paths.pop()?;
|
||||||
|
|
||||||
let upload = Tokio::spawn(cx, async move {
|
let upload = Tokio::spawn(cx, async move {
|
||||||
let client = app_state().client();
|
|
||||||
let file = fs::read(path).await.ok()?;
|
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)
|
Some(url)
|
||||||
});
|
});
|
||||||
@@ -1071,7 +1072,7 @@ impl ChatPanel {
|
|||||||
.relative()
|
.relative()
|
||||||
.w_16()
|
.w_16()
|
||||||
.child(
|
.child(
|
||||||
img(SharedUri::from(url.to_string()))
|
img(url.as_str())
|
||||||
.size_16()
|
.size_16()
|
||||||
.shadow_lg()
|
.shadow_lg()
|
||||||
.rounded(cx.theme().radius)
|
.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>) {
|
fn on_open_seen_on(&mut self, ev: &SeenOn, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let id = ev.0;
|
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 task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
||||||
let states = app_state();
|
let tracker = tracker.read().await;
|
||||||
let client = states.client();
|
|
||||||
let event_tracker = states.tracker().read().await;
|
|
||||||
let mut relays: Vec<RelayUrl> = vec![];
|
let mut relays: Vec<RelayUrl> = vec![];
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
@@ -1252,7 +1254,7 @@ impl ChatPanel {
|
|||||||
|
|
||||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
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(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);
|
relays.extend(urls);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use common::display::RenderedProfile;
|
use common::RenderedProfile;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, ElementId, HighlightStyle, InteractiveText, IntoElement, SharedString,
|
AnyElement, App, ElementId, HighlightStyle, InteractiveText, IntoElement, SharedString,
|
||||||
StyledText, UnderlineStyle, Window,
|
StyledText, UnderlineStyle, Window,
|
||||||
@@ -129,8 +129,6 @@ fn render_plain_text_mut(
|
|||||||
let range = link.start()..link.end();
|
let range = link.start()..link.end();
|
||||||
let url = link.as_str().to_string();
|
let url = link.as_str().to_string();
|
||||||
|
|
||||||
log::info!("Found URL: {}", url);
|
|
||||||
|
|
||||||
url_matches.push((range, url));
|
url_matches.push((range, url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,9 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
states = { path = "../states" }
|
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-connect.workspace = true
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
nostr.workspace = true
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
@@ -19,6 +16,8 @@ smol.workspace = true
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
webbrowser.workspace = true
|
|
||||||
|
|
||||||
|
dirs = "5.0"
|
||||||
qrcode = "0.14.1"
|
qrcode = "0.14.1"
|
||||||
|
whoami = "1.6.1"
|
||||||
|
nostr = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
|
|||||||
@@ -1,44 +1,35 @@
|
|||||||
pub const CLIENT_NAME: &str = "Coop";
|
pub const CLIENT_NAME: &str = "Coop";
|
||||||
pub const APP_ID: &str = "su.reya.coop";
|
pub const APP_ID: &str = "su.reya.coop";
|
||||||
|
|
||||||
pub const SETTINGS_IDENTIFIER: &str = "coop:settings";
|
/// Bootstrap Relays.
|
||||||
pub const INBOX_SUB_ID: &str = "inbox";
|
pub const BOOTSTRAP_RELAYS: [&str; 5] = [
|
||||||
|
"wss://relay.damus.io",
|
||||||
/// Bootstrap Relays.
|
"wss://relay.primal.net",
|
||||||
pub const BOOTSTRAP_RELAYS: [&str; 5] = [
|
"wss://relay.nos.social",
|
||||||
"wss://relay.damus.io",
|
"wss://user.kindpag.es",
|
||||||
"wss://relay.primal.net",
|
"wss://purplepag.es",
|
||||||
"wss://relay.nos.social",
|
];
|
||||||
"wss://user.kindpag.es",
|
|
||||||
"wss://purplepag.es",
|
/// Search Relays.
|
||||||
];
|
pub const SEARCH_RELAYS: [&str; 1] = ["wss://relay.nostr.band"];
|
||||||
|
|
||||||
/// Search Relays.
|
/// Default relay for Nostr Connect
|
||||||
pub const SEARCH_RELAYS: [&str; 1] = ["wss://relay.nostr.band"];
|
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
||||||
|
|
||||||
/// Default relay for Nostr Connect
|
/// Default retry count for fetching NIP-17 relays
|
||||||
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
pub const RELAY_RETRY: u64 = 2;
|
||||||
|
|
||||||
/// Default retry count for fetching NIP-17 relays
|
/// Default retry count for sending messages
|
||||||
pub const RELAY_RETRY: u64 = 2;
|
pub const SEND_RETRY: u64 = 10;
|
||||||
|
|
||||||
/// Default retry count for sending messages
|
/// Default timeout (in seconds) for Nostr Connect
|
||||||
pub const SEND_RETRY: u64 = 10;
|
pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
|
||||||
|
|
||||||
/// Default timeout (in seconds) for Nostr Connect
|
/// Default timeout (in seconds) for Nostr Connect (Bunker)
|
||||||
pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
|
pub const BUNKER_TIMEOUT: u64 = 30;
|
||||||
|
|
||||||
/// Default timeout (in seconds) for Nostr Connect (Bunker)
|
/// Total metadata requests will be grouped.
|
||||||
pub const BUNKER_TIMEOUT: u64 = 30;
|
pub const METADATA_BATCH_LIMIT: usize = 20;
|
||||||
|
|
||||||
/// Default timeout (in seconds) for fetching events
|
/// Default width of the sidebar.
|
||||||
pub const QUERY_TIMEOUT: u64 = 3;
|
pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.;
|
||||||
|
|
||||||
/// 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;
|
|
||||||
|
|
||||||
/// Default width of the sidebar.
|
|
||||||
pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.;
|
|
||||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{Local, TimeZone};
|
||||||
use gpui::{Image, ImageFormat, SharedString, SharedUri};
|
use gpui::{Image, ImageFormat, SharedString};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use qrcode::render::svg;
|
use qrcode::render::svg;
|
||||||
use qrcode::QrCode;
|
use qrcode::QrCode;
|
||||||
@@ -16,12 +16,12 @@ const FALLBACK_IMG: &str = "https://image.nostr.build/c30703b48f511c293a9003be81
|
|||||||
const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";
|
const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";
|
||||||
|
|
||||||
pub trait RenderedProfile {
|
pub trait RenderedProfile {
|
||||||
fn avatar(&self, proxy: bool) -> SharedUri;
|
fn avatar(&self, proxy: bool) -> SharedString;
|
||||||
fn display_name(&self) -> SharedString;
|
fn display_name(&self) -> SharedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderedProfile for Profile {
|
impl RenderedProfile for Profile {
|
||||||
fn avatar(&self, proxy: bool) -> SharedUri {
|
fn avatar(&self, proxy: bool) -> SharedString {
|
||||||
self.metadata()
|
self.metadata()
|
||||||
.picture
|
.picture
|
||||||
.as_ref()
|
.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"
|
"{IMAGE_RESIZE_SERVICE}/?url={picture}&w=100&h=100&fit=cover&mask=circle&default={FALLBACK_IMG}&n=-1"
|
||||||
);
|
);
|
||||||
|
|
||||||
SharedUri::from(url)
|
url.into()
|
||||||
} else {
|
} 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 {
|
fn display_name(&self) -> SharedString {
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ impl EventUtils for UnsignedEvent {
|
|||||||
fn all_pubkeys(&self) -> Vec<PublicKey> {
|
fn all_pubkeys(&self) -> Vec<PublicKey> {
|
||||||
let mut public_keys: Vec<PublicKey> = self.tags.public_keys().copied().collect();
|
let mut public_keys: Vec<PublicKey> = self.tags.public_keys().copied().collect();
|
||||||
public_keys.push(self.pubkey);
|
public_keys.push(self.pubkey);
|
||||||
|
public_keys.into_iter().unique().sorted().collect()
|
||||||
public_keys
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,68 @@
|
|||||||
pub mod debounced_delay;
|
use std::sync::OnceLock;
|
||||||
pub mod display;
|
|
||||||
pub mod event;
|
pub use constants::*;
|
||||||
pub mod nip05;
|
pub use debounced_delay::*;
|
||||||
pub mod nip96;
|
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(),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,14 +32,17 @@ ui = { path = "../ui" }
|
|||||||
title_bar = { path = "../title_bar" }
|
title_bar = { path = "../title_bar" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
states = { path = "../states" }
|
state = { path = "../state" }
|
||||||
key_store = { path = "../key_store" }
|
key_store = { path = "../key_store" }
|
||||||
chat = { path = "../chat" }
|
chat = { path = "../chat" }
|
||||||
chat_ui = { path = "../chat_ui" }
|
chat_ui = { path = "../chat_ui" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
auto_update = { path = "../auto_update" }
|
auto_update = { path = "../auto_update" }
|
||||||
account = { path = "../account" }
|
account = { path = "../account" }
|
||||||
|
encryption = { path = "../encryption" }
|
||||||
|
encryption_ui = { path = "../encryption_ui" }
|
||||||
person = { path = "../person" }
|
person = { path = "../person" }
|
||||||
|
relay_auth = { path = "../relay_auth" }
|
||||||
|
|
||||||
rust-i18n.workspace = true
|
rust-i18n.workspace = true
|
||||||
i18n.workspace = true
|
i18n.workspace = true
|
||||||
@@ -49,19 +52,16 @@ reqwest_client.workspace = true
|
|||||||
|
|
||||||
nostr-connect.workspace = true
|
nostr-connect.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
nostr.workspace = true
|
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
dirs.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
oneshot.workspace = true
|
oneshot.workspace = true
|
||||||
flume.workspace = true
|
|
||||||
webbrowser.workspace = true
|
webbrowser.workspace = true
|
||||||
|
|
||||||
indexset = "0.12.3"
|
indexset = "0.12.3"
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use gpui::{actions, App};
|
use gpui::{actions, App};
|
||||||
use key_store::backend::KeyItem;
|
use key_store::{KeyItem, KeyStore};
|
||||||
use key_store::KeyStore;
|
|
||||||
use nostr_connect::prelude::*;
|
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]);
|
actions!(sidebar, [Reload, RelayStatus]);
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -49,10 +48,9 @@ pub fn load_embedded_fonts(cx: &App) {
|
|||||||
|
|
||||||
pub fn reset(cx: &mut App) {
|
pub fn reset(cx: &mut App) {
|
||||||
let backend = KeyStore::global(cx).read(cx).backend();
|
let backend = KeyStore::global(cx).read(cx).backend();
|
||||||
|
let client = NostrRegistry::global(cx).read(cx).client();
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let client = app_state().client();
|
|
||||||
|
|
||||||
// Remove the signer
|
// Remove the signer
|
||||||
client.unset_signer().await;
|
client.unset_signer().await;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,12 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
|
use common::{APP_ID, CLIENT_NAME};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString,
|
point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString,
|
||||||
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
|
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
|
||||||
WindowOptions,
|
WindowOptions,
|
||||||
};
|
};
|
||||||
use states::{app_state, APP_ID, BOOTSTRAP_RELAYS, CLIENT_NAME, SEARCH_RELAYS};
|
|
||||||
use ui::Root;
|
use ui::Root;
|
||||||
|
|
||||||
use crate::actions::{load_embedded_fonts, quit, Quit};
|
use crate::actions::{load_embedded_fonts, quit, Quit};
|
||||||
@@ -26,29 +26,6 @@ fn main() {
|
|||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
|
.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
|
// Run application
|
||||||
app.run(move |cx| {
|
app.run(move |cx| {
|
||||||
// Load embedded fonts in assets/fonts
|
// Load embedded fonts in assets/fonts
|
||||||
@@ -102,18 +79,30 @@ fn main() {
|
|||||||
// Initialize components
|
// Initialize components
|
||||||
ui::init(cx);
|
ui::init(cx);
|
||||||
|
|
||||||
// Initialize app registry
|
// Initialize backend for keys storage
|
||||||
chat::init(cx);
|
key_store::init(cx);
|
||||||
|
|
||||||
|
// Initialize the nostr client
|
||||||
|
state::init(cx);
|
||||||
|
|
||||||
// Initialize person registry
|
// Initialize person registry
|
||||||
person::init(cx);
|
person::init(cx);
|
||||||
|
|
||||||
// Initialize backend for keys storage
|
|
||||||
key_store::init(cx);
|
|
||||||
|
|
||||||
// Initialize settings
|
// Initialize settings
|
||||||
settings::init(cx);
|
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
|
// Initialize auto update
|
||||||
auto_update::init(cx);
|
auto_update::init(cx);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use dirs::document_dir;
|
use common::home_dir;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, AppContext, ClipboardItem, Context, Entity, Flatten, IntoElement, ParentElement, Render,
|
div, AppContext, ClipboardItem, Context, Entity, Flatten, IntoElement, ParentElement, Render,
|
||||||
SharedString, Styled, Task, Window,
|
SharedString, Styled, Task, Window,
|
||||||
@@ -46,9 +46,8 @@ impl BackupKeys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn backup(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Task<()>> {
|
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 dir = home_dir();
|
||||||
|
let path = cx.prompt_for_new_path(dir, Some("My Nostr Account"));
|
||||||
let path = cx.prompt_for_new_path(&document_dir, Some("My Nostr Account"));
|
|
||||||
let nsec = self.secret_input.read(cx).value().to_string();
|
let nsec = self.secret_input.read(cx).value().to_string();
|
||||||
|
|
||||||
Some(cx.spawn_in(window, async move |this, cx| {
|
Some(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use account::Account;
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use chat::room::Room;
|
use chat::{ChatRegistry, Room};
|
||||||
use chat::ChatRegistry;
|
use common::{nip05_profile, RenderedProfile, TextUtils, BOOTSTRAP_RELAYS};
|
||||||
use common::display::{RenderedProfile, TextUtils};
|
|
||||||
use common::nip05::nip05_profile;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement,
|
div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement,
|
||||||
@@ -18,7 +17,7 @@ use nostr_sdk::prelude::*;
|
|||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::{app_state, BOOTSTRAP_RELAYS};
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
@@ -115,7 +114,10 @@ pub struct Compose {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 contacts = cx.new(|_| vec![]);
|
||||||
let error_message = cx.new(|_| None);
|
let error_message = cx.new(|_| None);
|
||||||
|
|
||||||
@@ -129,7 +131,6 @@ impl Compose {
|
|||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
let get_contacts: Task<Result<Vec<Contact>, Error>> = cx.background_spawn(async move {
|
let get_contacts: Task<Result<Vec<Contact>, Error>> = cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
let profiles = client.database().contacts(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> {
|
async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> {
|
||||||
let states = app_state();
|
|
||||||
let client = states.client();
|
|
||||||
|
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
||||||
let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
|
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>) {
|
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;
|
let pk = contact.public_key;
|
||||||
|
|
||||||
if !self.contacts.read(cx).iter().any(|c| c.public_key == pk) {
|
if !self.contacts.read(cx).iter().any(|c| c.public_key == pk) {
|
||||||
self._tasks.push(cx.background_spawn(async move {
|
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| {
|
cx.defer_in(window, |this, window, cx| {
|
||||||
@@ -313,6 +313,10 @@ impl Compose {
|
|||||||
|
|
||||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let chat = ChatRegistry::global(cx);
|
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 receivers: Vec<PublicKey> = self.selected(cx);
|
||||||
let subject_input = self.title_input.read(cx).value();
|
let subject_input = self.title_input.read(cx).value();
|
||||||
let subject = (!subject_input.is_empty()).then(|| subject_input.to_string());
|
let subject = (!subject_input.is_empty()).then(|| subject_input.to_string());
|
||||||
@@ -322,25 +326,11 @@ impl Compose {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
chat.update(cx, |this, cx| {
|
||||||
let result = Room::new(subject, receivers).await;
|
this.push_room(cx.new(|_| Room::new(subject, public_key, receivers)), cx);
|
||||||
|
});
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
window.close_modal(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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_error(&mut self, error: impl Into<SharedString>, cx: &mut Context<Self>) {
|
fn set_error(&mut self, error: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::str::FromStr;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use common::nip96::nip96_upload;
|
use common::nip96_upload;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
|
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
|
||||||
@@ -12,7 +12,7 @@ use i18n::{shared_t, t};
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use states::app_state;
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{InputState, TextInput};
|
use ui::input::{InputState, TextInput};
|
||||||
@@ -34,12 +34,18 @@ pub struct EditProfile {
|
|||||||
|
|
||||||
impl EditProfile {
|
impl EditProfile {
|
||||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
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 =
|
let name_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder(t!("profile.placeholder_name")));
|
cx.new(|cx| InputState::new(window, cx).placeholder(t!("profile.placeholder_name")));
|
||||||
|
|
||||||
let avatar_input =
|
let avatar_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg"));
|
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg"));
|
||||||
|
|
||||||
let website_input =
|
let website_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://your-website.com"));
|
cx.new(|cx| InputState::new(window, cx).placeholder("https://your-website.com"));
|
||||||
|
|
||||||
let bio_input = cx.new(|cx| {
|
let bio_input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.multi_line()
|
.multi_line()
|
||||||
@@ -58,7 +64,6 @@ impl EditProfile {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
let metadata = client
|
let metadata = client
|
||||||
@@ -104,8 +109,12 @@ impl EditProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
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 nip96 = AppSettings::get_media_server(cx);
|
||||||
let avatar_input = self.avatar_input.downgrade();
|
let avatar_input = self.avatar_input.downgrade();
|
||||||
|
|
||||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||||
files: true,
|
files: true,
|
||||||
directories: false,
|
directories: false,
|
||||||
@@ -125,9 +134,7 @@ impl EditProfile {
|
|||||||
let (tx, rx) = oneshot::channel::<Url>();
|
let (tx, rx) = oneshot::channel::<Url>();
|
||||||
|
|
||||||
nostr_sdk::async_utility::task::spawn(async move {
|
nostr_sdk::async_utility::task::spawn(async move {
|
||||||
if let Ok(url) =
|
if let Ok(url) = nip96_upload(&client, &nip96, file_data).await {
|
||||||
nip96_upload(app_state().client(), &nip96, file_data).await
|
|
||||||
{
|
|
||||||
_ = tx.send(url);
|
_ = tx.send(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -168,6 +175,9 @@ impl EditProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Profile, Error>> {
|
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 avatar = self.avatar_input.read(cx).value().to_string();
|
||||||
let name = self.name_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();
|
let bio = self.bio_input.read(cx).value().to_string();
|
||||||
@@ -190,7 +200,6 @@ impl EditProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
|
|
||||||
// Sign the new metadata event
|
// Sign the new metadata event
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use common::BUNKER_TIMEOUT;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
||||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
||||||
};
|
};
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
use key_store::backend::KeyItem;
|
use key_store::{KeyItem, KeyStore};
|
||||||
use key_store::KeyStore;
|
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::{app_state, BUNKER_TIMEOUT};
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
@@ -213,8 +213,10 @@ impl Login {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn connect(&mut self, signer: NostrConnect, cx: &mut Context<Self>) {
|
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 {
|
cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
client.set_signer(signer).await;
|
client.set_signer(signer).await;
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -262,6 +264,10 @@ impl Login {
|
|||||||
|
|
||||||
pub fn login_with_keys(&mut self, keys: Keys, cx: &mut Context<Self>) {
|
pub fn login_with_keys(&mut self, keys: Keys, cx: &mut Context<Self>) {
|
||||||
let keystore = KeyStore::global(cx).read(cx).backend();
|
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 username = keys.public_key().to_hex();
|
||||||
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
||||||
|
|
||||||
@@ -281,7 +287,6 @@ impl Login {
|
|||||||
|
|
||||||
// Update the signer
|
// Update the signer
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
client.set_signer(keys).await;
|
client.set_signer(keys).await;
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
pub mod account;
|
|
||||||
pub mod backup_keys;
|
pub mod backup_keys;
|
||||||
pub mod compose;
|
pub mod compose;
|
||||||
pub mod edit_profile;
|
pub mod edit_profile;
|
||||||
@@ -9,5 +8,6 @@ pub mod preferences;
|
|||||||
pub mod screening;
|
pub mod screening;
|
||||||
pub mod setup_relay;
|
pub mod setup_relay;
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
|
pub mod startup;
|
||||||
pub mod user_profile;
|
pub mod user_profile;
|
||||||
pub mod welcome;
|
pub mod welcome;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use common::nip96::nip96_upload;
|
use common::{default_nip17_relays, default_nip65_relays, nip96_upload, BOOTSTRAP_RELAYS};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
|
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
|
||||||
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
|
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
|
||||||
@@ -7,12 +7,11 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
use key_store::backend::KeyItem;
|
use key_store::{KeyItem, KeyStore};
|
||||||
use key_store::KeyStore;
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use states::{app_state, default_nip17_relays, default_nip65_relays, BOOTSTRAP_RELAYS};
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
@@ -106,6 +105,9 @@ impl NewAccount {
|
|||||||
pub fn set_signer(&mut self, cx: &mut Context<Self>) {
|
pub fn set_signer(&mut self, cx: &mut Context<Self>) {
|
||||||
let keystore = KeyStore::global(cx).read(cx).backend();
|
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 keys = self.temp_keys.read(cx).clone();
|
||||||
let username = keys.public_key().to_hex();
|
let username = keys.public_key().to_hex();
|
||||||
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
||||||
@@ -130,8 +132,6 @@ impl NewAccount {
|
|||||||
// Update the signer
|
// Update the signer
|
||||||
// Set the client's signer with the current keys
|
// Set the client's signer with the current keys
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
|
|
||||||
// Set the client's signer with the current keys
|
// Set the client's signer with the current keys
|
||||||
client.set_signer(keys).await;
|
client.set_signer(keys).await;
|
||||||
|
|
||||||
@@ -178,6 +178,9 @@ impl NewAccount {
|
|||||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.uploading(true, cx);
|
self.uploading(true, cx);
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
// Get the user's configured NIP96 server
|
// Get the user's configured NIP96 server
|
||||||
let nip96_server = AppSettings::get_media_server(cx);
|
let nip96_server = AppSettings::get_media_server(cx);
|
||||||
|
|
||||||
@@ -194,7 +197,7 @@ impl NewAccount {
|
|||||||
Ok(Some(mut paths)) => {
|
Ok(Some(mut paths)) => {
|
||||||
if let Some(path) = paths.pop() {
|
if let Some(path) = paths.pop() {
|
||||||
let file = fs::read(path).await?;
|
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)
|
Ok(url)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
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::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, px, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
div, img, px, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||||
@@ -9,11 +9,10 @@ use gpui::{
|
|||||||
SharedString, StatefulInteractiveElement, Styled, Task, Window,
|
SharedString, StatefulInteractiveElement, Styled, Task, Window,
|
||||||
};
|
};
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
use key_store::backend::KeyItem;
|
use key_store::{KeyItem, KeyStore};
|
||||||
use key_store::KeyStore;
|
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::{app_state, CLIENT_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
@@ -165,8 +164,10 @@ impl Onboarding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn connect(&mut self, signer: NostrConnect, cx: &mut Context<Self>) {
|
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 {
|
cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
client.set_signer(signer).await;
|
client.set_signer(signer).await;
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -223,7 +224,7 @@ impl Focusable for Onboarding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Render 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()
|
h_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use account::Account;
|
use account::Account;
|
||||||
use common::display::RenderedProfile;
|
use common::RenderedProfile;
|
||||||
use gpui::http_client::Url;
|
use gpui::http_client::Url;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
|
div, px, relative, rems, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use common::display::{shorten_pubkey, RenderedProfile, RenderedTimestamp};
|
use common::{nip05_verify, shorten_pubkey, RenderedProfile, RenderedTimestamp, BOOTSTRAP_RELAYS};
|
||||||
use common::nip05::nip05_verify;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity,
|
div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity,
|
||||||
@@ -13,7 +13,7 @@ use nostr_sdk::prelude::*;
|
|||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::{app_state, BOOTSTRAP_RELAYS};
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
@@ -35,14 +35,17 @@ pub struct Screening {
|
|||||||
|
|
||||||
impl Screening {
|
impl Screening {
|
||||||
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let profile = persons.read(cx).get_person(&public_key, cx);
|
let profile = persons.read(cx).get_person(&public_key, cx);
|
||||||
|
|
||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
let contact_check: Task<Result<(bool, Vec<Profile>), Error>> =
|
let contact_check: Task<Result<(bool, Vec<Profile>), Error>> = cx.background_spawn({
|
||||||
cx.background_spawn(async move {
|
let client = Arc::clone(&client);
|
||||||
let client = app_state().client();
|
async move {
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let signer_pubkey = signer.get_public_key().await?;
|
let signer_pubkey = signer.get_public_key().await?;
|
||||||
|
|
||||||
@@ -64,10 +67,10 @@ impl Screening {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok((followed, mutual_contacts))
|
Ok((followed, mutual_contacts))
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let activity_check = cx.background_spawn(async move {
|
let activity_check = cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
let filter = Filter::new().author(public_key).limit(1);
|
let filter = Filter::new().author(public_key).limit(1);
|
||||||
let mut activity: Option<Timestamp> = None;
|
let mut activity: Option<Timestamp> = None;
|
||||||
|
|
||||||
@@ -153,12 +156,12 @@ impl Screening {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn report(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
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 public_key = self.profile.public_key();
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
|
|
||||||
let tag = Tag::public_key_report(public_key, Report::Impersonation);
|
let tag = Tag::public_key_report(public_key, Report::Impersonation);
|
||||||
let event = EventBuilder::report(vec![tag], "").sign(&signer).await?;
|
let event = EventBuilder::report(vec![tag], "").sign(&signer).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ use std::time::Duration;
|
|||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, uniform_list, App, AppContext, AsyncWindowContext, Context, Entity,
|
div, px, uniform_list, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
|
||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
ParentElement, Render, SharedString, Styled, Subscription, Task, TextAlign, UniformList,
|
||||||
Task, TextAlign, UniformList, Window,
|
Window,
|
||||||
};
|
};
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
use itertools::Itertools;
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::app_state;
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
@@ -39,6 +38,9 @@ pub struct SetupRelay {
|
|||||||
|
|
||||||
impl SetupRelay {
|
impl SetupRelay {
|
||||||
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 input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
|
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
@@ -47,7 +49,11 @@ impl SetupRelay {
|
|||||||
tasks.push(
|
tasks.push(
|
||||||
// Load user's relays in the local database
|
// Load user's relays in the local database
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
if let Ok(relays) = 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.update(cx, |this, cx| {
|
||||||
this.relays.extend(relays);
|
this.relays.extend(relays);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -79,24 +85,21 @@ impl SetupRelay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load(cx: &AsyncWindowContext) -> Task<Result<Vec<RelayUrl>, Error>> {
|
async fn load(client: &Client) -> Result<Vec<RelayUrl>, Error> {
|
||||||
cx.background_spawn(async move {
|
let signer = client.signer().await?;
|
||||||
let client = app_state().client();
|
let public_key = signer.get_public_key().await?;
|
||||||
let signer = client.signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::InboxRelays)
|
.kind(Kind::InboxRelays)
|
||||||
.author(public_key)
|
.author(public_key)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||||
let urls = nip17::extract_owned_relay_list(event).collect();
|
let urls = nip17::extract_owned_relay_list(event).collect();
|
||||||
Ok(urls)
|
Ok(urls)
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("Not found."))
|
Err(anyhow!("Not found."))
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
@@ -150,13 +153,12 @@ impl SetupRelay {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
let relays = self.relays.clone();
|
let relays = self.relays.clone();
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
let states = app_state();
|
|
||||||
let client = states.client();
|
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
|
|
||||||
let tags: Vec<Tag> = relays
|
let tags: Vec<Tag> = relays
|
||||||
.iter()
|
.iter()
|
||||||
@@ -177,12 +179,6 @@ impl SetupRelay {
|
|||||||
client.connect_relay(relay).await.ok();
|
client.connect_relay(relay).await.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch gift wrap events
|
|
||||||
states
|
|
||||||
.get_messages(public_key, &relays.into_iter().collect_vec())
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use chat::room::RoomKind;
|
use chat::{ChatRegistry, RoomKind};
|
||||||
use chat::ChatRegistry;
|
|
||||||
use chat_ui::{CopyPublicKey, OpenPublicKey};
|
use chat_ui::{CopyPublicKey, OpenPublicKey};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, rems, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce,
|
div, rems, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce,
|
||||||
SharedString, SharedUri, StatefulInteractiveElement, Styled, Window,
|
SharedString, StatefulInteractiveElement, Styled, Window,
|
||||||
};
|
};
|
||||||
use i18n::t;
|
use i18n::t;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -26,7 +25,7 @@ pub struct RoomListItem {
|
|||||||
room_id: Option<u64>,
|
room_id: Option<u64>,
|
||||||
public_key: Option<PublicKey>,
|
public_key: Option<PublicKey>,
|
||||||
name: Option<SharedString>,
|
name: Option<SharedString>,
|
||||||
avatar: Option<SharedUri>,
|
avatar: Option<SharedString>,
|
||||||
created_at: Option<SharedString>,
|
created_at: Option<SharedString>,
|
||||||
kind: Option<RoomKind>,
|
kind: Option<RoomKind>,
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
@@ -62,7 +61,7 @@ impl RoomListItem {
|
|||||||
self
|
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.avatar = Some(avatar.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ use std::ops::Range;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use chat::room::{Room, RoomKind};
|
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
||||||
use chat::{ChatEvent, ChatRegistry};
|
use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
||||||
use common::debounced_delay::DebouncedDelay;
|
|
||||||
use common::display::{RenderedTimestamp, TextUtils};
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
deferred, div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity,
|
deferred, div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity,
|
||||||
@@ -20,7 +18,7 @@ use list_item::RoomListItem;
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::{app_state, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
@@ -137,8 +135,7 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn request_metadata(public_key: PublicKey) -> Result<(), Error> {
|
async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> {
|
||||||
let client = app_state().client();
|
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
||||||
let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
|
let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
|
||||||
@@ -152,18 +149,7 @@ impl Sidebar {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_temp_room(receiver: PublicKey) -> Result<Room, Error> {
|
async fn nip50(client: &Client, query: &str) -> Result<BTreeSet<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();
|
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
@@ -186,10 +172,13 @@ impl Sidebar {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a temporary room
|
// Request metadata event's author
|
||||||
if let Ok(room) = Self::create_temp_room(event.pubkey).await {
|
Self::request_metadata(client, event.pubkey).await?;
|
||||||
rooms.insert(room);
|
|
||||||
}
|
// Construct room
|
||||||
|
let room = Room::new(None, public_key, vec![event.pubkey]);
|
||||||
|
|
||||||
|
rooms.insert(room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,11 +201,13 @@ impl Sidebar {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
let query = query.to_owned();
|
let query = query.to_owned();
|
||||||
let query_cloned = query.clone();
|
let query_cloned = query.clone();
|
||||||
|
|
||||||
let task = smol::future::or(
|
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 {
|
Tokio::spawn(cx, async move {
|
||||||
let _ = rx.recv().await.is_ok();
|
let _ = rx.recv().await.is_ok();
|
||||||
None
|
None
|
||||||
@@ -263,13 +254,20 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn search_by_nip05(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
|
fn search_by_nip05(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
let address = query.to_owned();
|
let address = query.to_owned();
|
||||||
|
|
||||||
let task = Tokio::spawn(cx, async move {
|
let task = Tokio::spawn(cx, async move {
|
||||||
if let Ok(profile) = common::nip05::nip05_profile(&address).await {
|
match common::nip05_profile(&address).await {
|
||||||
Self::create_temp_room(profile.public_key).await
|
Ok(profile) => {
|
||||||
} else {
|
let signer = client.signer().await?;
|
||||||
Err(anyhow!(t!("sidebar.addr_error")))
|
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>) {
|
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 {
|
let Ok(public_key) = query.to_public_key() else {
|
||||||
window.push_notification(t!("common.pubkey_invalid"), cx);
|
window.push_notification(t!("common.pubkey_invalid"), cx);
|
||||||
self.set_finding(false, window, cx);
|
self.set_finding(false, window, cx);
|
||||||
@@ -317,8 +318,13 @@ impl Sidebar {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let task: Task<Result<Room, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Room, Error>> = cx.background_spawn(async move {
|
||||||
// Create a gift wrap event to represent as room
|
let signer = client.signer().await?;
|
||||||
Self::create_temp_room(public_key).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| {
|
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>) {
|
fn on_reload(&mut self, _ev: &Reload, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
ChatRegistry::global(cx).update(cx, |this, cx| {
|
||||||
this.load_rooms(window, cx);
|
this.get_rooms(cx);
|
||||||
});
|
});
|
||||||
window.push_notification(t!("common.refreshed"), cx);
|
window.push_notification(t!("common.refreshed"), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_manage(&mut self, _ev: &RelayStatus, window: &mut Window, cx: &mut Context<Self>) {
|
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 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 subscription = client.subscription(&SubscriptionId::new("inbox")).await;
|
||||||
let mut relays: Vec<Relay> = vec![];
|
let mut relays: Vec<Relay> = vec![];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use common::display::RenderedProfile;
|
use common::{RenderedProfile, BUNKER_TIMEOUT};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||||
@@ -9,12 +9,11 @@ use gpui::{
|
|||||||
Window,
|
Window,
|
||||||
};
|
};
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
use key_store::backend::KeyItem;
|
use key_store::{Credential, KeyItem, KeyStore};
|
||||||
use key_store::KeyStore;
|
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::{app_state, BUNKER_TIMEOUT};
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
@@ -24,18 +23,14 @@ use ui::{h_flex, v_flex, ContextModal, Sizable, StyledExt};
|
|||||||
|
|
||||||
use crate::actions::{reset, CoopAuthUrlHandler};
|
use crate::actions::{reset, CoopAuthUrlHandler};
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(cre: Credential, window: &mut Window, cx: &mut App) -> Entity<Startup> {
|
||||||
public_key: PublicKey,
|
cx.new(|cx| Startup::new(cre, window, cx))
|
||||||
secret: String,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Entity<Account> {
|
|
||||||
cx.new(|cx| Account::new(public_key, secret, window, cx))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Account {
|
/// Startup
|
||||||
public_key: PublicKey,
|
#[derive(Debug)]
|
||||||
secret: String,
|
pub struct Startup {
|
||||||
|
credential: Credential,
|
||||||
loading: bool,
|
loading: bool,
|
||||||
|
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
@@ -49,13 +44,8 @@ pub struct Account {
|
|||||||
_tasks: SmallVec<[Task<()>; 1]>,
|
_tasks: SmallVec<[Task<()>; 1]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Account {
|
impl Startup {
|
||||||
fn new(
|
fn new(credential: Credential, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
public_key: PublicKey,
|
|
||||||
secret: String,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let tasks = smallvec![];
|
let tasks = smallvec![];
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
@@ -69,8 +59,7 @@ impl Account {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
public_key,
|
credential,
|
||||||
secret,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
name: "Account".into(),
|
name: "Account".into(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
@@ -83,9 +72,11 @@ impl Account {
|
|||||||
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.set_loading(true, cx);
|
self.set_loading(true, cx);
|
||||||
|
|
||||||
|
let secret = self.credential.secret();
|
||||||
|
|
||||||
// Try to login with bunker
|
// Try to login with bunker
|
||||||
if self.secret.starts_with("bunker://") {
|
if secret.starts_with("bunker://") {
|
||||||
match NostrConnectURI::parse(&self.secret) {
|
match NostrConnectURI::parse(secret) {
|
||||||
Ok(uri) => {
|
Ok(uri) => {
|
||||||
self.login_with_bunker(uri, window, cx);
|
self.login_with_bunker(uri, window, cx);
|
||||||
}
|
}
|
||||||
@@ -98,7 +89,7 @@ impl Account {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Fall back to login with keys
|
// Fall back to login with keys
|
||||||
match SecretKey::parse(&self.secret) {
|
match SecretKey::parse(secret) {
|
||||||
Ok(secret) => {
|
Ok(secret) => {
|
||||||
self.login_with_keys(secret, cx);
|
self.login_with_keys(secret, cx);
|
||||||
}
|
}
|
||||||
@@ -115,6 +106,8 @@ impl Account {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
let keystore = KeyStore::global(cx).read(cx).backend();
|
let keystore = KeyStore::global(cx).read(cx).backend();
|
||||||
|
|
||||||
// Handle connection in the background
|
// Handle connection in the background
|
||||||
@@ -138,8 +131,6 @@ impl Account {
|
|||||||
this._tasks.push(
|
this._tasks.push(
|
||||||
// Handle connection in the background
|
// Handle connection in the background
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let client = app_state().client();
|
|
||||||
|
|
||||||
match signer.bunker_uri().await {
|
match signer.bunker_uri().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
client.set_signer(signer).await;
|
client.set_signer(signer).await;
|
||||||
@@ -171,11 +162,12 @@ impl Account {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn login_with_keys(&mut self, secret: SecretKey, cx: &mut Context<Self>) {
|
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);
|
let keys = Keys::new(secret);
|
||||||
|
|
||||||
// Update the signer
|
// Update the signer
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
client.set_signer(keys).await;
|
client.set_signer(keys).await;
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -187,7 +179,7 @@ impl Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Panel for Account {
|
impl Panel for Startup {
|
||||||
fn panel_id(&self) -> SharedString {
|
fn panel_id(&self) -> SharedString {
|
||||||
self.name.clone()
|
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 {
|
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||||
self.focus_handle.clone()
|
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 {
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let profile = persons.read(cx).get_person(&self.public_key, cx);
|
let bunker = self.credential.secret().starts_with("bunker://");
|
||||||
let bunker = self.secret.starts_with("bunker://");
|
let profile = persons
|
||||||
|
.read(cx)
|
||||||
|
.get_person(&self.credential.public_key(), cx);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.image_cache(self.image_cache.clone())
|
.image_cache(self.image_cache.clone())
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use common::display::RenderedProfile;
|
use common::{nip05_verify, RenderedProfile};
|
||||||
use common::nip05::nip05_verify;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement,
|
div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement,
|
||||||
@@ -13,7 +12,7 @@ use nostr_sdk::prelude::*;
|
|||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::app_state;
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
@@ -33,13 +32,15 @@ pub struct UserProfile {
|
|||||||
|
|
||||||
impl UserProfile {
|
impl UserProfile {
|
||||||
pub fn new(target: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(target: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let profile = persons.read(cx).get_person(&target, cx);
|
let profile = persons.read(cx).get_person(&target, cx);
|
||||||
|
|
||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
let check_follow: Task<Result<bool, Error>> = cx.background_spawn(async move {
|
let check_follow: Task<Result<bool, Error>> = cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
let contact_list = client.database().contacts_public_keys(public_key).await?;
|
let contact_list = client.database().contacts_public_keys(public_key).await?;
|
||||||
|
|||||||
22
crates/encryption/Cargo.toml
Normal file
22
crates/encryption/Cargo.toml
Normal 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
|
||||||
631
crates/encryption/src/lib.rs
Normal file
631
crates/encryption/src/lib.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
9
crates/encryption/src/signer.rs
Normal file
9
crates/encryption/src/signer.rs
Normal 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,
|
||||||
|
}
|
||||||
27
crates/encryption_ui/Cargo.toml
Normal file
27
crates/encryption_ui/Cargo.toml
Normal 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
|
||||||
407
crates/encryption_ui/src/lib.rs
Normal file
407
crates/encryption_ui/src/lib.rs
Normal 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);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,9 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
states = { path = "../states" }
|
common = { path = "../common" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|
||||||
nostr.workspace = true
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|||||||
@@ -6,9 +6,33 @@ use std::path::PathBuf;
|
|||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use common::config_dir;
|
||||||
use futures::FutureExt as _;
|
use futures::FutureExt as _;
|
||||||
use gpui::AsyncApp;
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum KeyItem {
|
pub enum KeyItem {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use std::sync::{Arc, LazyLock};
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
|
pub use backend::*;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, Task};
|
use gpui::{App, AppContext, Context, Entity, Global, Task};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
use crate::backend::{FileProvider, KeyBackend, KeyringProvider};
|
mod backend;
|
||||||
|
|
||||||
pub mod backend;
|
|
||||||
|
|
||||||
static DISABLE_KEYRING: LazyLock<bool> =
|
static DISABLE_KEYRING: LazyLock<bool> =
|
||||||
LazyLock::new(|| std::env::var("DISABLE_KEYRING").is_ok_and(|value| !value.is_empty()));
|
LazyLock::new(|| std::env::var("DISABLE_KEYRING").is_ok_and(|value| !value.is_empty()));
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ publish.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
states = { path = "../states" }
|
state = { path = "../state" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr.workspace = true
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
|
|||||||
@@ -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 nostr_sdk::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::app_state;
|
use state::NostrRegistry;
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx);
|
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx);
|
||||||
@@ -13,6 +14,8 @@ struct GlobalPersonRegistry(Entity<PersonRegistry>);
|
|||||||
|
|
||||||
impl Global for GlobalPersonRegistry {}
|
impl Global for GlobalPersonRegistry {}
|
||||||
|
|
||||||
|
/// Person Registry
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct PersonRegistry {
|
pub struct PersonRegistry {
|
||||||
/// Collection of all persons (user profiles)
|
/// Collection of all persons (user profiles)
|
||||||
pub persons: HashMap<PublicKey, Entity<Profile>>,
|
pub persons: HashMap<PublicKey, Entity<Profile>>,
|
||||||
@@ -34,12 +37,58 @@ impl PersonRegistry {
|
|||||||
|
|
||||||
/// Create a new person registry instance
|
/// Create a new person registry instance
|
||||||
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
let mut tasks = smallvec![];
|
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(
|
tasks.push(
|
||||||
// Load all user profiles from the database
|
// Load all user profiles from the database
|
||||||
cx.spawn(async move |this, cx| {
|
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) => {
|
Ok(profiles) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.bulk_insert_persons(profiles, cx);
|
this.bulk_insert_persons(profiles, cx);
|
||||||
@@ -59,23 +108,20 @@ impl PersonRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a task to load all user profiles from the database
|
/// Load all user profiles from the database
|
||||||
fn load_persons(cx: &AsyncApp) -> Task<Result<Vec<Profile>, Error>> {
|
async fn load_persons(client: &Client) -> Result<Vec<Profile>, Error> {
|
||||||
cx.background_spawn(async move {
|
let filter = Filter::new().kind(Kind::Metadata).limit(200);
|
||||||
let client = app_state().client();
|
let events = client.database().query(filter).await?;
|
||||||
let filter = Filter::new().kind(Kind::Metadata).limit(200);
|
|
||||||
let events = client.database().query(filter).await?;
|
|
||||||
|
|
||||||
let mut profiles = vec![];
|
let mut profiles = vec![];
|
||||||
|
|
||||||
for event in events.into_iter() {
|
for event in events.into_iter() {
|
||||||
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||||
let profile = Profile::new(event.pubkey, metadata);
|
let profile = Profile::new(event.pubkey, metadata);
|
||||||
profiles.push(profile);
|
profiles.push(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(profiles)
|
Ok(profiles)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert batch of persons
|
/// Insert batch of persons
|
||||||
|
|||||||
20
crates/relay_auth/Cargo.toml
Normal file
20
crates/relay_auth/Cargo.toml
Normal 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
|
||||||
326
crates/relay_auth/src/lib.rs
Normal file
326
crates/relay_auth/src/lib.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
states = { path = "../states" }
|
state = { path = "../state" }
|
||||||
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smallvec::{smallvec, SmallVec};
|
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) {
|
pub fn init(cx: &mut App) {
|
||||||
let state = cx.new(AppSettings::new);
|
let state = cx.new(AppSettings::new);
|
||||||
@@ -122,8 +124,10 @@ impl AppSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_settings(&self, cx: &mut Context<Self>) {
|
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 task: Task<Result<Settings, anyhow::Error>> = cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
@@ -153,9 +157,11 @@ impl AppSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_settings(&self, cx: &mut Context<Self>) {
|
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) {
|
if let Ok(content) = serde_json::to_string(&self.setting_values) {
|
||||||
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "states"
|
name = "state"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
nostr-lmdb.workspace = true
|
nostr-lmdb.workspace = true
|
||||||
nostr-gossip-memory.workspace = true
|
nostr-gossip-memory.workspace = true
|
||||||
|
|
||||||
dirs.workspace = true
|
gpui.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
flume.workspace = true
|
smallvec.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
whoami = "1.6.1"
|
|
||||||
rustls = "0.23.23"
|
rustls = "0.23.23"
|
||||||
|
event-listener = "5.4.1"
|
||||||
312
crates/state/src/lib.rs
Normal file
312
crates/state/src/lib.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
87
crates/state/src/storage.rs
Normal file
87
crates/state/src/storage.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use nostr_sdk::prelude::*;
|
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)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct EventTracker {
|
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
|
/// Tracking events that have been resent by Coop in the current session
|
||||||
pub resent_ids: Vec<Output<EventId>>,
|
pub resent_ids: Vec<Output<EventId>>,
|
||||||
|
|
||||||
@@ -18,6 +28,10 @@ pub struct EventTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EventTracker {
|
impl EventTracker {
|
||||||
|
pub fn failed_unwrap_events(&self) -> &Vec<Event> {
|
||||||
|
&self.failed_unwrap_events
|
||||||
|
}
|
||||||
|
|
||||||
pub fn resent_ids(&self) -> &Vec<Output<EventId>> {
|
pub fn resent_ids(&self) -> &Vec<Output<EventId>> {
|
||||||
&self.resent_ids
|
&self.resent_ids
|
||||||
}
|
}
|
||||||
@@ -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(),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -541,13 +541,10 @@ impl Element for TextElement {
|
|||||||
let mut bounds = bounds;
|
let mut bounds = bounds;
|
||||||
|
|
||||||
let (display_text, text_color) = if is_empty {
|
let (display_text, text_color) = if is_empty {
|
||||||
(
|
(Rope::from(placeholder.as_str()), cx.theme().text_muted)
|
||||||
Rope::from_str_small(placeholder.as_str()),
|
|
||||||
cx.theme().text_muted,
|
|
||||||
)
|
|
||||||
} else if state.masked {
|
} else if state.masked {
|
||||||
(
|
(
|
||||||
Rope::from_str_small("*".repeat(text.chars_count()).as_str()),
|
Rope::from("*".repeat(text.chars_count()).as_str()),
|
||||||
cx.theme().text,
|
cx.theme().text,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -718,7 +718,7 @@ impl InputState {
|
|||||||
/// Set the default value of the input field.
|
/// Set the default value of the input field.
|
||||||
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
|
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
|
||||||
let text: SharedString = value.into();
|
let text: SharedString = value.into();
|
||||||
self.text = Rope::from_str_small(text.as_str());
|
self.text = Rope::from(text.as_str());
|
||||||
self.text_wrapper.set_default_text(&self.text);
|
self.text_wrapper.set_default_text(&self.text);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -2099,9 +2099,7 @@ impl EntityInputHandler for InputState {
|
|||||||
.unwrap_or(self.selected_range.into());
|
.unwrap_or(self.selected_range.into());
|
||||||
|
|
||||||
let old_text = self.text.clone();
|
let old_text = self.text.clone();
|
||||||
let executor = cx.background_executor();
|
self.text.replace(range.clone(), new_text);
|
||||||
|
|
||||||
self.text.replace(range.clone(), new_text, executor);
|
|
||||||
|
|
||||||
let mut new_offset = (range.start + new_text.len()).min(self.text.len());
|
let mut new_offset = (range.start + new_text.len()).min(self.text.len());
|
||||||
|
|
||||||
@@ -2115,7 +2113,7 @@ impl EntityInputHandler for InputState {
|
|||||||
|
|
||||||
if !self.mask_pattern.is_none() {
|
if !self.mask_pattern.is_none() {
|
||||||
let mask_text = self.mask_pattern.mask(&pending_text);
|
let mask_text = self.mask_pattern.mask(&pending_text);
|
||||||
self.text = Rope::from_str_small(mask_text.as_str());
|
self.text = Rope::from(mask_text.as_str());
|
||||||
let new_text_len =
|
let new_text_len =
|
||||||
(new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
|
(new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
|
||||||
new_offset = (range.start + new_text_len).min(mask_text.len());
|
new_offset = (range.start + new_text_len).min(mask_text.len());
|
||||||
@@ -2123,13 +2121,8 @@ impl EntityInputHandler for InputState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.push_history(&old_text, &range, new_text);
|
self.push_history(&old_text, &range, new_text);
|
||||||
self.text_wrapper.update(
|
self.text_wrapper
|
||||||
&self.text,
|
.update(&self.text, &range, &Rope::from(new_text), false, cx);
|
||||||
&range,
|
|
||||||
&Rope::from_str_small(new_text),
|
|
||||||
false,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
self.selected_range = (new_offset..new_offset).into();
|
self.selected_range = (new_offset..new_offset).into();
|
||||||
self.ime_marked_range.take();
|
self.ime_marked_range.take();
|
||||||
self.update_preferred_column();
|
self.update_preferred_column();
|
||||||
@@ -2161,9 +2154,7 @@ impl EntityInputHandler for InputState {
|
|||||||
.unwrap_or(self.selected_range.into());
|
.unwrap_or(self.selected_range.into());
|
||||||
|
|
||||||
let old_text = self.text.clone();
|
let old_text = self.text.clone();
|
||||||
let executor = cx.background_executor();
|
self.text.replace(range.clone(), new_text);
|
||||||
|
|
||||||
self.text.replace(range.clone(), new_text, executor);
|
|
||||||
|
|
||||||
if self.mode.is_single_line() {
|
if self.mode.is_single_line() {
|
||||||
let pending_text = self.text.to_string();
|
let pending_text = self.text.to_string();
|
||||||
@@ -2174,13 +2165,9 @@ impl EntityInputHandler for InputState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.push_history(&old_text, &range, new_text);
|
self.push_history(&old_text, &range, new_text);
|
||||||
self.text_wrapper.update(
|
self.text_wrapper
|
||||||
&self.text,
|
.update(&self.text, &range, &Rope::from(new_text), false, cx);
|
||||||
&range,
|
|
||||||
&Rope::from_str_small(new_text),
|
|
||||||
false,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
if new_text.is_empty() {
|
if new_text.is_empty() {
|
||||||
// Cancel selection, when cancel IME input.
|
// Cancel selection, when cancel IME input.
|
||||||
self.selected_range = (range.start..range.start).into();
|
self.selected_range = (range.start..range.start).into();
|
||||||
@@ -2195,6 +2182,7 @@ impl EntityInputHandler for InputState {
|
|||||||
.into();
|
.into();
|
||||||
}
|
}
|
||||||
self.mode.update_auto_grow(&self.text_wrapper);
|
self.mode.update_auto_grow(&self.text_wrapper);
|
||||||
|
|
||||||
cx.emit(InputEvent::Change);
|
cx.emit(InputEvent::Change);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ impl RenderOnce for MenuItemElement {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.id(self.id)
|
.id(self.id)
|
||||||
.group(&self.group_name)
|
.group(&self.group_name)
|
||||||
.gap_x_1()
|
.gap_x_2()
|
||||||
.py_1()
|
.py_1()
|
||||||
.px_2()
|
.px_2()
|
||||||
.text_base()
|
.text_base()
|
||||||
|
|||||||
@@ -861,8 +861,8 @@ impl PopupMenu {
|
|||||||
fn render_icon(
|
fn render_icon(
|
||||||
has_icon: bool,
|
has_icon: bool,
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
_: &mut Window,
|
_window: &mut Window,
|
||||||
_: &mut Context<Self>,
|
_cx: &mut Context<Self>,
|
||||||
) -> Option<impl IntoElement> {
|
) -> Option<impl IntoElement> {
|
||||||
if !has_icon {
|
if !has_icon {
|
||||||
return None;
|
return None;
|
||||||
@@ -873,7 +873,7 @@ impl PopupMenu {
|
|||||||
.h_3p5()
|
.h_3p5()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.when_some(icon, |this, icon| this.child(icon.clone().xsmall()));
|
.when_some(icon, |this, icon| this.child(icon.clone().small()));
|
||||||
|
|
||||||
Some(icon)
|
Some(icon)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,15 +68,11 @@ keyring_disable:
|
|||||||
label:
|
label:
|
||||||
en: "Keyring is disabled"
|
en: "Keyring is disabled"
|
||||||
body_1:
|
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:
|
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."
|
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."
|
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:
|
pending_encryption:
|
||||||
label:
|
label:
|
||||||
@@ -94,14 +90,6 @@ request_encryption:
|
|||||||
body:
|
body:
|
||||||
en: "You've requested for the encryption Key from:"
|
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:
|
auto_update:
|
||||||
updating:
|
updating:
|
||||||
en: "Installing the new update..."
|
en: "Installing the new update..."
|
||||||
|
|||||||
Reference in New Issue
Block a user