feat: implement multiple keystores (#187)
* keystore * . * fix * . * allow user disable keyring * update texts
This commit is contained in:
188
Cargo.lock
generated
188
Cargo.lock
generated
@@ -102,7 +102,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -346,7 +346,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -422,7 +422,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -595,7 +595,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"shlex",
|
"shlex",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -615,7 +615,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"shlex",
|
"shlex",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -747,7 +747,7 @@ checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -847,7 +847,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -952,7 +952,7 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"toml 0.8.23",
|
"toml 0.8.23",
|
||||||
]
|
]
|
||||||
@@ -1074,18 +1074,6 @@ dependencies = [
|
|||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "client_keys"
|
|
||||||
version = "0.2.11"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"gpui",
|
|
||||||
"log",
|
|
||||||
"nostr-sdk",
|
|
||||||
"smallvec",
|
|
||||||
"states",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmake"
|
name = "cmake"
|
||||||
version = "0.1.54"
|
version = "0.1.54"
|
||||||
@@ -1169,7 +1157,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1282,7 +1270,6 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"assets",
|
"assets",
|
||||||
"auto_update",
|
"auto_update",
|
||||||
"client_keys",
|
|
||||||
"common",
|
"common",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
"flume",
|
"flume",
|
||||||
@@ -1547,7 +1534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1603,17 +1590,17 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_refineable"
|
name = "derive_refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1692,7 +1679,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1829,7 +1816,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1849,7 +1836,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1972,7 +1959,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2166,7 +2153,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2320,7 +2307,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2506,7 +2493,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2601,18 +2588,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#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_tokio"
|
name = "gpui_tokio"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -2841,7 +2828,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#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
@@ -2866,7 +2853,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#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -3210,7 +3197,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3660,7 +3647,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
@@ -3907,7 +3894,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a499845dce2b16f52c0350771fe60608da887435"
|
source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -3931,7 +3918,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a499845dce2b16f52c0350771fe60608da887435"
|
source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3943,7 +3930,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a499845dce2b16f52c0350771fe60608da887435"
|
source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -3954,7 +3941,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a499845dce2b16f52c0350771fe60608da887435"
|
source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"flume",
|
"flume",
|
||||||
@@ -3968,7 +3955,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-relay-pool"
|
name = "nostr-relay-pool"
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a499845dce2b16f52c0350771fe60608da887435"
|
source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -3985,7 +3972,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a499845dce2b16f52c0350771fe60608da887435"
|
source = "git+https://github.com/rust-nostr/nostr#6cf4905a881f60bc2deac603d178839779148bc2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4078,7 +4065,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4346,7 +4333,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4493,7 +4480,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#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4530,7 +4517,7 @@ dependencies = [
|
|||||||
"phf_shared",
|
"phf_shared",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4565,7 +4552,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4702,7 +4689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4733,7 +4720,7 @@ dependencies = [
|
|||||||
"proc-macro-error-attr2",
|
"proc-macro-error-attr2",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4761,7 +4748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
|
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5103,13 +5090,13 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
]
|
]
|
||||||
@@ -5150,6 +5137,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"common",
|
"common",
|
||||||
"flume",
|
"flume",
|
||||||
|
"futures",
|
||||||
"fuzzy-matcher",
|
"fuzzy-matcher",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
@@ -5158,6 +5146,8 @@ dependencies = [
|
|||||||
"nostr-lmdb",
|
"nostr-lmdb",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"rustls",
|
"rustls",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
@@ -5216,7 +5206,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#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5270,7 +5260,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rope"
|
name = "rope"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -5289,9 +5279,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-embed"
|
name = "rust-embed"
|
||||||
version = "8.7.2"
|
version = "8.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a"
|
checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rust-embed-impl",
|
"rust-embed-impl",
|
||||||
"rust-embed-utils",
|
"rust-embed-utils",
|
||||||
@@ -5300,22 +5290,22 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-embed-impl"
|
name = "rust-embed-impl"
|
||||||
version = "8.7.2"
|
version = "8.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c"
|
checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rust-embed-utils",
|
"rust-embed-utils",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-embed-utils"
|
name = "rust-embed-utils"
|
||||||
version = "8.7.2"
|
version = "8.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594"
|
checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"globset",
|
"globset",
|
||||||
"sha2",
|
"sha2",
|
||||||
@@ -5350,7 +5340,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5616,7 +5606,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde_derive_internals",
|
"serde_derive_internals",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5737,7 +5727,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "semantic_version"
|
name = "semantic_version"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5776,7 +5766,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5787,7 +5777,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5834,7 +5824,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6096,7 +6086,7 @@ checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-error2",
|
"proc-macro-error2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6157,7 +6147,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6169,7 +6159,7 @@ dependencies = [
|
|||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6181,7 +6171,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "sum_tree"
|
name = "sum_tree"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -6306,9 +6296,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.106"
|
version = "2.0.107"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -6341,7 +6331,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6516,7 +6506,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6527,7 +6517,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6686,7 +6676,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6906,7 +6896,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7217,7 +7207,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#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
@@ -7252,11 +7242,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#3d6722be9abf94af5680a3323b69a452154c55b2"
|
source = "git+https://github.com/zed-industries/zed#197d24437809a7d5bbd214f79e11b850367e35e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"perf",
|
"perf",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7432,7 +7422,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -7467,7 +7457,7 @@ checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
@@ -7825,7 +7815,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7836,7 +7826,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7847,7 +7837,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7858,7 +7848,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8465,7 +8455,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8512,7 +8502,7 @@ dependencies = [
|
|||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
"zbus_names",
|
"zbus_names",
|
||||||
"zvariant",
|
"zvariant",
|
||||||
"zvariant_utils",
|
"zvariant_utils",
|
||||||
@@ -8660,7 +8650,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8680,7 +8670,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8701,7 +8691,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8734,7 +8724,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8785,7 +8775,7 @@ dependencies = [
|
|||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
"zvariant_utils",
|
"zvariant_utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8798,6 +8788,6 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"syn 2.0.106",
|
"syn 2.0.107",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "client_keys"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
publish.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
states = { path = "../states" }
|
|
||||||
|
|
||||||
nostr-sdk.workspace = true
|
|
||||||
gpui.workspace = true
|
|
||||||
anyhow.workspace = true
|
|
||||||
log.workspace = true
|
|
||||||
smallvec.workspace = true
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Window};
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
|
||||||
use states::constants::KEYRING_URL;
|
|
||||||
use states::paths::config_dir;
|
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
|
||||||
ClientKeys::set_global(cx.new(ClientKeys::new), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GlobalClientKeys(Entity<ClientKeys>);
|
|
||||||
|
|
||||||
impl Global for GlobalClientKeys {}
|
|
||||||
|
|
||||||
pub struct ClientKeys {
|
|
||||||
keys: Option<Keys>,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
subscriptions: SmallVec<[Subscription; 1]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientKeys {
|
|
||||||
/// Retrieve the Global Client Keys instance
|
|
||||||
pub fn global(cx: &App) -> Entity<Self> {
|
|
||||||
cx.global::<GlobalClientKeys>().0.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve the Client Keys instance
|
|
||||||
pub fn read_global(cx: &App) -> &Self {
|
|
||||||
cx.global::<GlobalClientKeys>().0.read(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the Global Client Keys instance
|
|
||||||
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
|
|
||||||
cx.set_global(GlobalClientKeys(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
|
||||||
let mut subscriptions = smallvec![];
|
|
||||||
|
|
||||||
subscriptions.push(cx.observe_new::<Self>(|this, window, cx| {
|
|
||||||
if let Some(window) = window {
|
|
||||||
this.load(window, cx);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
Self {
|
|
||||||
keys: None,
|
|
||||||
subscriptions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
// Prevent macOS from asking for password every time
|
|
||||||
// Only for debug builds
|
|
||||||
if cfg!(debug_assertions) && cfg!(target_os = "macos") {
|
|
||||||
log::warn!("Running debug build on macOS");
|
|
||||||
log::warn!("Skipping keychain access, generating new client keys");
|
|
||||||
self.new_keys(cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let read_client_keys = cx.read_credentials(KEYRING_URL);
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
if let Ok(Some((_, secret))) = read_client_keys.await {
|
|
||||||
// Update the client keys with the stored secret key from the keychain
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
let Ok(secret_key) = SecretKey::from_slice(&secret) else {
|
|
||||||
this.set_keys(None, false, true, cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let keys = Keys::new(secret_key);
|
|
||||||
this.set_keys(Some(keys), false, true, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
} else if Self::first_run() {
|
|
||||||
// If this is the first run, generate new keys and use them for the client keys
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.new_keys(cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
} else {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_keys(None, false, true, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn set_keys(
|
|
||||||
&mut self,
|
|
||||||
keys: Option<Keys>,
|
|
||||||
persist: bool,
|
|
||||||
notify: bool,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if persist {
|
|
||||||
if let Some(keys) = keys.as_ref() {
|
|
||||||
let username = keys.public_key().to_hex();
|
|
||||||
let password = keys.secret_key().secret_bytes();
|
|
||||||
let write_keys = cx.write_credentials(KEYRING_URL, &username, &password);
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
if let Err(e) = write_keys.await {
|
|
||||||
log::error!("Failed to save the client keys: {e}")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.keys = keys;
|
|
||||||
|
|
||||||
// Notify GPUI to reload UI
|
|
||||||
if notify {
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_keys(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.set_keys(Some(Keys::generate()), true, true, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn force_new_keys(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.set_keys(Some(Keys::generate()), true, false, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn keys(&self) -> Keys {
|
|
||||||
self.keys
|
|
||||||
.clone()
|
|
||||||
.expect("Keys should always be initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_keys(&self) -> bool {
|
|
||||||
self.keys.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn first_run() -> bool {
|
|
||||||
let flag = config_dir().join(".first_run");
|
|
||||||
!flag.exists() && std::fs::write(&flag, "").is_ok()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,7 +35,6 @@ common = { path = "../common" }
|
|||||||
states = { path = "../states" }
|
states = { path = "../states" }
|
||||||
registry = { path = "../registry" }
|
registry = { path = "../registry" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
client_keys = { path = "../client_keys" }
|
|
||||||
auto_update = { path = "../auto_update" }
|
auto_update = { path = "../auto_update" }
|
||||||
|
|
||||||
rust-i18n.workspace = true
|
rust-i18n.workspace = true
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use gpui::{actions, App};
|
use gpui::{actions, App, AppContext};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
|
use registry::keystore::KeyItem;
|
||||||
|
use registry::Registry;
|
||||||
|
use states::app_state;
|
||||||
|
|
||||||
actions!(coop, [ReloadMetadata, DarkMode, Settings, Logout, Quit]);
|
actions!(coop, [ReloadMetadata, DarkMode, Settings, Logout, Quit]);
|
||||||
actions!(sidebar, [Reload, RelayStatus]);
|
actions!(sidebar, [Reload, RelayStatus]);
|
||||||
@@ -43,6 +46,36 @@ pub fn load_embedded_fonts(cx: &App) {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(cx: &mut App) {
|
||||||
|
let registry = Registry::global(cx);
|
||||||
|
let keystore = registry.read(cx).keystore();
|
||||||
|
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let client = app_state().client();
|
||||||
|
client.unset_signer().await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
keystore
|
||||||
|
.delete_credentials(&KeyItem::User.to_string(), cx)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
keystore
|
||||||
|
.delete_credentials(&KeyItem::Bunker.to_string(), cx)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
registry
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.reset(cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn quit(_: &Quit, cx: &mut App) {
|
pub fn quit(_: &Quit, cx: &mut App) {
|
||||||
log::info!("Gracefully quitting the application . . .");
|
log::info!("Gracefully quitting the application . . .");
|
||||||
cx.quit();
|
cx.quit();
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use auto_update::AutoUpdater;
|
use auto_update::AutoUpdater;
|
||||||
use client_keys::ClientKeys;
|
|
||||||
use common::display::RenderedProfile;
|
use common::display::RenderedProfile;
|
||||||
use common::event::EventUtils;
|
use common::event::EventUtils;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
@@ -17,10 +16,11 @@ use i18n::{shared_t, t};
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use registry::keystore::KeyItem;
|
||||||
use registry::{Registry, RegistryEvent};
|
use registry::{Registry, RegistryEvent};
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH};
|
use states::constants::{BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH};
|
||||||
use states::state::{AuthRequest, SignalKind, UnwrappingStatus};
|
use states::state::{AuthRequest, SignalKind, UnwrappingStatus};
|
||||||
use states::{app_state, default_nip17_relays, default_nip65_relays};
|
use states::{app_state, default_nip17_relays, default_nip65_relays};
|
||||||
use theme::{ActiveTheme, Theme, ThemeMode};
|
use theme::{ActiveTheme, Theme, ThemeMode};
|
||||||
@@ -36,7 +36,7 @@ use ui::notification::Notification;
|
|||||||
use ui::popup_menu::PopupMenuExt;
|
use ui::popup_menu::PopupMenuExt;
|
||||||
use ui::{h_flex, v_flex, ContextModal, Disableable, IconName, Root, Sizable, StyledExt};
|
use ui::{h_flex, v_flex, ContextModal, Disableable, IconName, Root, Sizable, StyledExt};
|
||||||
|
|
||||||
use crate::actions::{DarkMode, Logout, ReloadMetadata, Settings};
|
use crate::actions::{reset, DarkMode, Logout, ReloadMetadata, Settings};
|
||||||
use crate::views::compose::compose_button;
|
use crate::views::compose::compose_button;
|
||||||
use crate::views::setup_relay::SetupRelay;
|
use crate::views::setup_relay::SetupRelay;
|
||||||
use crate::views::{
|
use crate::views::{
|
||||||
@@ -82,7 +82,6 @@ pub struct ChatSpace {
|
|||||||
|
|
||||||
impl ChatSpace {
|
impl ChatSpace {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let client_keys = ClientKeys::global(cx);
|
|
||||||
let registry = Registry::global(cx);
|
let registry = Registry::global(cx);
|
||||||
let status = registry.read(cx).unwrapping_status.clone();
|
let status = registry.read(cx).unwrapping_status.clone();
|
||||||
|
|
||||||
@@ -101,20 +100,46 @@ impl ChatSpace {
|
|||||||
);
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe the client keys and show an alert modal if they fail to initialize
|
// Observe the keystore
|
||||||
cx.observe_in(&client_keys, window, |this, keys, window, cx| {
|
cx.observe_in(®istry, window, |this, registry, window, cx| {
|
||||||
if !keys.read(cx).has_keys() {
|
let has_keyring = registry.read(cx).initialized_keystore;
|
||||||
this.render_client_keys_modal(window, cx);
|
let use_filestore = registry.read(cx).is_using_file_keystore();
|
||||||
} else {
|
let not_logged_in = registry.read(cx).signer_pubkey().is_none();
|
||||||
this.load_local_account(window, cx);
|
|
||||||
|
if use_filestore && not_logged_in {
|
||||||
|
this.render_keyring_installation(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_keyring && not_logged_in {
|
||||||
|
let keystore = registry.read(cx).keystore();
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let result = keystore
|
||||||
|
.read_credentials(&KeyItem::User.to_string(), cx)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
match result {
|
||||||
|
Ok(Some((user, secret))) => {
|
||||||
|
let public_key = PublicKey::parse(&user).unwrap();
|
||||||
|
let secret = String::from_utf8(secret).unwrap();
|
||||||
|
this.set_account_layout(public_key, secret, window, cx);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
this.set_onboarding_layout(window, cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe the global registry
|
// Observe the global registry's events
|
||||||
cx.observe_in(&status, window, move |this, status, window, cx| {
|
cx.observe_in(&status, window, move |this, status, window, cx| {
|
||||||
let registry = Registry::global(cx);
|
|
||||||
let status = status.read(cx);
|
let status = status.read(cx);
|
||||||
let all_panels = this.get_all_panel_ids(cx);
|
let all_panels = this.get_all_panel_ids(cx);
|
||||||
|
|
||||||
@@ -122,7 +147,7 @@ impl ChatSpace {
|
|||||||
status,
|
status,
|
||||||
UnwrappingStatus::Processing | UnwrappingStatus::Complete
|
UnwrappingStatus::Processing | UnwrappingStatus::Complete
|
||||||
) {
|
) {
|
||||||
registry.update(cx, |this, cx| {
|
Registry::global(cx).update(cx, |this, cx| {
|
||||||
this.load_rooms(window, cx);
|
this.load_rooms(window, cx);
|
||||||
this.refresh_rooms(all_panels, cx);
|
this.refresh_rooms(all_panels, cx);
|
||||||
});
|
});
|
||||||
@@ -510,12 +535,12 @@ impl ChatSpace {
|
|||||||
|
|
||||||
fn set_account_layout(
|
fn set_account_layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
public_key: PublicKey,
|
||||||
secret: String,
|
secret: String,
|
||||||
profile: Profile,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let panel = Arc::new(account::init(profile, secret, window, cx));
|
let panel = Arc::new(account::init(public_key, secret, window, cx));
|
||||||
let center = DockItem::panel(panel);
|
let center = DockItem::panel(panel);
|
||||||
|
|
||||||
self.dock.update(cx, |this, cx| {
|
self.dock.update(cx, |this, cx| {
|
||||||
@@ -556,44 +581,6 @@ impl ChatSpace {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_local_account(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let task = cx.background_spawn(async move {
|
|
||||||
let client = app_state().client();
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::ApplicationSpecificData)
|
|
||||||
.identifier(ACCOUNT_IDENTIFIER)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
|
||||||
let metadata = client
|
|
||||||
.database()
|
|
||||||
.metadata(event.pubkey)
|
|
||||||
.await?
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
Ok((event.content, Profile::new(event.pubkey, metadata)))
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Empty"))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
if let Ok((secret, profile)) = task.await {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
this.set_account_layout(secret, profile, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
} else {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
this.set_onboarding_layout(window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_settings(&mut self, _ev: &Settings, window: &mut Window, cx: &mut Context<Self>) {
|
fn on_settings(&mut self, _ev: &Settings, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let view = preferences::init(window, cx);
|
let view = preferences::init(window, cx);
|
||||||
|
|
||||||
@@ -659,24 +646,7 @@ impl ChatSpace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_sign_out(&mut self, _e: &Logout, _window: &mut Window, cx: &mut Context<Self>) {
|
fn on_sign_out(&mut self, _e: &Logout, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
cx.background_spawn(async move {
|
reset(cx);
|
||||||
let states = app_state();
|
|
||||||
let client = states.client();
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::ApplicationSpecificData)
|
|
||||||
.identifier(ACCOUNT_IDENTIFIER);
|
|
||||||
|
|
||||||
// Delete account
|
|
||||||
client.database().delete(filter).await.ok();
|
|
||||||
|
|
||||||
// Reset the nostr client
|
|
||||||
client.reset().await;
|
|
||||||
|
|
||||||
// Notify the channel about the signer being unset
|
|
||||||
states.signal().send(SignalKind::SignerUnset).await;
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_open_pubkey(&mut self, ev: &OpenPublicKey, window: &mut Window, cx: &mut Context<Self>) {
|
fn on_open_pubkey(&mut self, ev: &OpenPublicKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
@@ -734,6 +704,32 @@ impl ChatSpace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_keyring_installation(&mut self, window: &mut Window, cx: &mut App) {
|
||||||
|
window.open_modal(cx, move |this, _window, cx| {
|
||||||
|
this.overlay_closable(false)
|
||||||
|
.show_close(false)
|
||||||
|
.keyboard(false)
|
||||||
|
.alert()
|
||||||
|
.button_props(ModalButtonProps::default().ok_text(t!("common.continue")))
|
||||||
|
.title(shared_t!("keyring_disable.label"))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child(shared_t!("keyring_disable.body_1"))
|
||||||
|
.child(shared_t!("keyring_disable.body_2"))
|
||||||
|
.child(shared_t!("keyring_disable.body_3"))
|
||||||
|
.child(shared_t!("keyring_disable.body_4"))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().danger_foreground)
|
||||||
|
.child(shared_t!("keyring_disable.body_5")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) {
|
fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) {
|
||||||
let relays = default_nip65_relays();
|
let relays = default_nip65_relays();
|
||||||
|
|
||||||
@@ -936,53 +932,6 @@ impl ChatSpace {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_client_keys_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
window.open_modal(cx, move |this, _window, cx| {
|
|
||||||
this.overlay_closable(false)
|
|
||||||
.show_close(false)
|
|
||||||
.keyboard(false)
|
|
||||||
.confirm()
|
|
||||||
.button_props(
|
|
||||||
ModalButtonProps::default()
|
|
||||||
.cancel_text(t!("startup.create_new_keys"))
|
|
||||||
.ok_text(t!("common.allow")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w_full()
|
|
||||||
.h_40()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.text_center()
|
|
||||||
.text_sm()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.font_semibold()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(shared_t!("startup.client_keys_warning")),
|
|
||||||
)
|
|
||||||
.child(shared_t!("startup.client_keys_desc")),
|
|
||||||
)
|
|
||||||
.on_cancel(|_, _window, cx| {
|
|
||||||
ClientKeys::global(cx).update(cx, |this, cx| {
|
|
||||||
this.new_keys(cx);
|
|
||||||
});
|
|
||||||
// true: Close modal
|
|
||||||
true
|
|
||||||
})
|
|
||||||
.on_ok(|_, window, cx| {
|
|
||||||
ClientKeys::global(cx).update(cx, |this, cx| {
|
|
||||||
this.load(window, cx);
|
|
||||||
});
|
|
||||||
// true: Close modal
|
|
||||||
true
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_titlebar_left_side(
|
fn render_titlebar_left_side(
|
||||||
&mut self,
|
&mut self,
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
|
|||||||
@@ -83,9 +83,6 @@ fn main() {
|
|||||||
// Initialize components
|
// Initialize components
|
||||||
ui::init(cx);
|
ui::init(cx);
|
||||||
|
|
||||||
// Initialize client keys
|
|
||||||
client_keys::init(cx);
|
|
||||||
|
|
||||||
// Initialize app registry
|
// Initialize app registry
|
||||||
registry::init(cx);
|
registry::init(cx);
|
||||||
|
|
||||||
|
|||||||
@@ -1,66 +1,68 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
|
||||||
use client_keys::ClientKeys;
|
|
||||||
use common::display::RenderedProfile;
|
use common::display::RenderedProfile;
|
||||||
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,
|
||||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
||||||
RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription, Task,
|
RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription, Task,
|
||||||
WeakEntity, Window,
|
Window,
|
||||||
};
|
};
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use nostr_sdk::prelude::*;
|
use registry::keystore::KeyItem;
|
||||||
|
use registry::Registry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::app_state;
|
use states::app_state;
|
||||||
use states::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
|
use states::constants::BUNKER_TIMEOUT;
|
||||||
use states::state::SignalKind;
|
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
use ui::input::{InputState, TextInput};
|
|
||||||
use ui::notification::Notification;
|
|
||||||
use ui::popup_menu::PopupMenu;
|
use ui::popup_menu::PopupMenu;
|
||||||
use ui::{h_flex, v_flex, ContextModal, Sizable, StyledExt};
|
use ui::{h_flex, v_flex, ContextModal, Sizable, StyledExt};
|
||||||
|
|
||||||
use crate::actions::CoopAuthUrlHandler;
|
use crate::actions::{reset, CoopAuthUrlHandler};
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
profile: Profile,
|
public_key: PublicKey,
|
||||||
secret: String,
|
secret: String,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Entity<Account> {
|
) -> Entity<Account> {
|
||||||
cx.new(|cx| Account::new(secret, profile, window, cx))
|
cx.new(|cx| Account::new(public_key, secret, window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
profile: Profile,
|
public_key: PublicKey,
|
||||||
stored_secret: String,
|
secret: String,
|
||||||
is_bunker: bool,
|
|
||||||
loading: bool,
|
loading: bool,
|
||||||
|
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
image_cache: Entity<RetainAllImageCache>,
|
image_cache: Entity<RetainAllImageCache>,
|
||||||
|
|
||||||
|
/// Event subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
|
|
||||||
|
/// Background tasks
|
||||||
_tasks: SmallVec<[Task<()>; 1]>,
|
_tasks: SmallVec<[Task<()>; 1]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Account {
|
impl Account {
|
||||||
fn new(secret: String, profile: Profile, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(
|
||||||
let is_bunker = secret.starts_with("bunker://");
|
public_key: PublicKey,
|
||||||
|
secret: String,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let tasks = smallvec![];
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Clear the local state when user closes the account panel
|
// Clear the local state when user closes the account panel
|
||||||
cx.on_release_in(window, move |this, window, cx| {
|
cx.on_release_in(window, move |this, window, cx| {
|
||||||
this.stored_secret.clear();
|
|
||||||
this.image_cache.update(cx, |this, cx| {
|
this.image_cache.update(cx, |this, cx| {
|
||||||
this.clear(window, cx);
|
this.clear(window, cx);
|
||||||
});
|
});
|
||||||
@@ -68,212 +70,116 @@ impl Account {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
profile,
|
public_key,
|
||||||
is_bunker,
|
secret,
|
||||||
stored_secret: secret,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
name: "Account".into(),
|
name: "Account".into(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
image_cache: RetainAllImageCache::new(cx),
|
image_cache: RetainAllImageCache::new(cx),
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
_tasks: smallvec![],
|
_tasks: tasks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
if self.is_bunker {
|
// Try to login with bunker
|
||||||
if let Ok(uri) = NostrConnectURI::parse(&self.stored_secret) {
|
if self.secret.starts_with("bunker://") {
|
||||||
self.nostr_connect(uri, window, cx);
|
match NostrConnectURI::parse(&self.secret) {
|
||||||
}
|
Ok(uri) => {
|
||||||
} else if let Ok(enc) = EncryptedSecretKey::from_bech32(&self.stored_secret) {
|
self.login_with_bunker(uri, window, cx);
|
||||||
self.keys(enc, window, cx);
|
|
||||||
} else {
|
|
||||||
window.push_notification("Cannot continue with current account", cx);
|
|
||||||
self.set_loading(false, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nostr_connect(&mut self, uri: NostrConnectURI, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let client_keys = ClientKeys::global(cx);
|
|
||||||
let app_keys = client_keys.read(cx).keys();
|
|
||||||
|
|
||||||
let timeout = Duration::from_secs(BUNKER_TIMEOUT);
|
|
||||||
let mut signer = NostrConnect::new(uri, app_keys, timeout, None).unwrap();
|
|
||||||
|
|
||||||
// Handle auth url with the default browser
|
|
||||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
|
||||||
|
|
||||||
self._tasks.push(
|
|
||||||
// Handle connection in the background
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let client = app_state().client();
|
|
||||||
|
|
||||||
match signer.bunker_uri().await {
|
|
||||||
Ok(_) => {
|
|
||||||
// Set the client's signer with the current nostr connect instance
|
|
||||||
client.set_signer(signer).await;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
this.set_loading(false, cx);
|
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}),
|
Err(e) => {
|
||||||
);
|
window.push_notification(e.to_string(), cx);
|
||||||
}
|
self.set_loading(false, cx);
|
||||||
|
}
|
||||||
fn keys(&mut self, enc: EncryptedSecretKey, window: &mut Window, cx: &mut Context<Self>) {
|
}
|
||||||
let pwd_input: Entity<InputState> = cx.new(|cx| InputState::new(window, cx).masked(true));
|
|
||||||
let weak_input = pwd_input.downgrade();
|
|
||||||
|
|
||||||
let error: Entity<Option<SharedString>> = cx.new(|_| None);
|
|
||||||
let weak_error = error.downgrade();
|
|
||||||
|
|
||||||
let entity = cx.weak_entity();
|
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, cx| {
|
|
||||||
let entity = entity.clone();
|
|
||||||
let entity_clone = entity.clone();
|
|
||||||
let weak_input = weak_input.clone();
|
|
||||||
let weak_error = weak_error.clone();
|
|
||||||
|
|
||||||
this.overlay_closable(false)
|
|
||||||
.show_close(false)
|
|
||||||
.keyboard(false)
|
|
||||||
.confirm()
|
|
||||||
.on_cancel(move |_, _window, cx| {
|
|
||||||
entity
|
|
||||||
.update(cx, |this, cx| {
|
|
||||||
this.set_loading(false, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// true to close the modal
|
|
||||||
true
|
|
||||||
})
|
|
||||||
.on_ok(move |_, window, cx| {
|
|
||||||
let weak_error = weak_error.clone();
|
|
||||||
let password = weak_input
|
|
||||||
.read_with(cx, |state, _cx| state.value().to_owned())
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
entity_clone
|
|
||||||
.update(cx, |this, cx| {
|
|
||||||
this.verify_keys(enc, password, weak_error, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// false to keep the modal open
|
|
||||||
false
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w_full()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
|
||||||
.text_sm()
|
|
||||||
.child(shared_t!("login.password_to_decrypt"))
|
|
||||||
.child(TextInput::new(&pwd_input).small())
|
|
||||||
.when_some(error.read(cx).as_ref(), |this, error| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.italic()
|
|
||||||
.text_color(cx.theme().danger_foreground)
|
|
||||||
.child(error.clone()),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_keys(
|
|
||||||
&mut self,
|
|
||||||
enc: EncryptedSecretKey,
|
|
||||||
password: Option<SharedString>,
|
|
||||||
error: WeakEntity<Option<SharedString>>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let Some(password) = password else {
|
|
||||||
error
|
|
||||||
.update(cx, |this, cx| {
|
|
||||||
*this = Some("Password is required".into());
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if password.is_empty() {
|
// Fall back to login with keys
|
||||||
error
|
match SecretKey::parse(&self.secret) {
|
||||||
.update(cx, |this, cx| {
|
Ok(secret) => {
|
||||||
*this = Some("Password cannot be empty".into());
|
self.login_with_keys(secret, cx);
|
||||||
cx.notify();
|
}
|
||||||
})
|
Err(e) => {
|
||||||
.ok();
|
window.push_notification(e.to_string(), cx);
|
||||||
return;
|
self.set_loading(false, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let task: Task<Result<SecretKey, Error>> = cx.background_spawn(async move {
|
fn login_with_bunker(
|
||||||
let secret = enc.decrypt(&password)?;
|
&mut self,
|
||||||
Ok(secret)
|
uri: NostrConnectURI,
|
||||||
});
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let keystore = Registry::global(cx).read(cx).keystore();
|
||||||
|
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
// Handle connection in the background
|
||||||
match task.await {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
Ok(secret) => {
|
let result = keystore
|
||||||
cx.update(|window, cx| {
|
.read_credentials(&KeyItem::Bunker.to_string(), cx)
|
||||||
window.close_all_modals(cx);
|
.await;
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
let client = app_state().client();
|
this.update_in(cx, |this, window, cx| {
|
||||||
let keys = Keys::new(secret);
|
match result {
|
||||||
|
Ok(Some((_, content))) => {
|
||||||
|
let secret = SecretKey::from_slice(&content).unwrap();
|
||||||
|
let keys = Keys::new(secret);
|
||||||
|
let timeout = Duration::from_secs(BUNKER_TIMEOUT);
|
||||||
|
let mut signer = NostrConnect::new(uri, keys, timeout, None).unwrap();
|
||||||
|
|
||||||
// Set the client's signer with the current keys
|
// Handle auth url with the default browser
|
||||||
client.set_signer(keys).await
|
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||||
}
|
|
||||||
Err(e) => {
|
// Connect to the remote signer
|
||||||
error
|
this._tasks.push(
|
||||||
.update(cx, |this, cx| {
|
// Handle connection in the background
|
||||||
*this = Some(e.to_string().into());
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
cx.notify();
|
let client = app_state().client();
|
||||||
})
|
|
||||||
.ok();
|
match signer.bunker_uri().await {
|
||||||
}
|
Ok(_) => {
|
||||||
};
|
client.set_signer(signer).await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
|
this.set_loading(false, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
window.push_notification(t!("login.keyring_required"), cx);
|
||||||
|
this.set_loading(false, cx);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
|
this.set_loading(false, cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn logout(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
fn login_with_keys(&mut self, secret: SecretKey, cx: &mut Context<Self>) {
|
||||||
self._tasks.push(
|
let keys = Keys::new(secret);
|
||||||
// Reset the nostr client in the background
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let states = app_state();
|
|
||||||
let client = states.client();
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
// Update the signer
|
||||||
.kind(Kind::ApplicationSpecificData)
|
cx.background_spawn(async move {
|
||||||
.identifier(ACCOUNT_IDENTIFIER);
|
let client = app_state().client();
|
||||||
|
client.set_signer(keys).await;
|
||||||
// Delete account
|
})
|
||||||
client.database().delete(filter).await.ok();
|
.detach();
|
||||||
|
|
||||||
// Unset the client's signer
|
|
||||||
client.unset_signer().await;
|
|
||||||
|
|
||||||
// Notify the channel about the signer being unset
|
|
||||||
states.signal().send(SignalKind::SignerUnset).await;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
@@ -310,6 +216,10 @@ impl Focusable for Account {
|
|||||||
|
|
||||||
impl Render for Account {
|
impl Render for Account {
|
||||||
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 registry = Registry::global(cx);
|
||||||
|
let profile = registry.read(cx).get_person(&self.public_key, cx);
|
||||||
|
let bunker = self.secret.starts_with("bunker://");
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.image_cache(self.image_cache.clone())
|
.image_cache(self.image_cache.clone())
|
||||||
.relative()
|
.relative()
|
||||||
@@ -367,8 +277,8 @@ impl Render for Account {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(!self.loading, |this| {
|
.when(!self.loading, |this| {
|
||||||
let avatar = self.profile.avatar(true);
|
let avatar = profile.avatar(true);
|
||||||
let name = self.profile.display_name();
|
let name = profile.display_name();
|
||||||
|
|
||||||
this.child(
|
this.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -381,7 +291,7 @@ impl Render for Account {
|
|||||||
.child(Avatar::new(avatar).size(rems(1.5)))
|
.child(Avatar::new(avatar).size(rems(1.5)))
|
||||||
.child(div().pb_px().font_semibold().child(name)),
|
.child(div().pb_px().font_semibold().child(name)),
|
||||||
)
|
)
|
||||||
.child(div().when(self.is_bunker, |this| {
|
.child(div().when(bunker, |this| {
|
||||||
let label = SharedString::from("Nostr Connect");
|
let label = SharedString::from("Nostr Connect");
|
||||||
|
|
||||||
this.child(
|
this.child(
|
||||||
@@ -407,9 +317,9 @@ impl Render for Account {
|
|||||||
Button::new("logout")
|
Button::new("logout")
|
||||||
.label(t!("user.sign_out"))
|
.label(t!("user.sign_out"))
|
||||||
.ghost()
|
.ghost()
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
.on_click(|_, _window, cx| {
|
||||||
this.logout(window, cx);
|
reset(cx);
|
||||||
})),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::fs;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use dirs::document_dir;
|
use dirs::document_dir;
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
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,
|
||||||
@@ -15,7 +14,6 @@ use ui::input::{InputState, TextInput};
|
|||||||
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable};
|
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable};
|
||||||
|
|
||||||
pub struct BackupKeys {
|
pub struct BackupKeys {
|
||||||
password: Entity<InputState>,
|
|
||||||
pubkey_input: Entity<InputState>,
|
pubkey_input: Entity<InputState>,
|
||||||
secret_input: Entity<InputState>,
|
secret_input: Entity<InputState>,
|
||||||
error: Option<SharedString>,
|
error: Option<SharedString>,
|
||||||
@@ -27,8 +25,6 @@ impl BackupKeys {
|
|||||||
let Ok(npub) = keys.public_key.to_bech32();
|
let Ok(npub) = keys.public_key.to_bech32();
|
||||||
let Ok(nsec) = keys.secret_key().to_bech32();
|
let Ok(nsec) = keys.secret_key().to_bech32();
|
||||||
|
|
||||||
let password = cx.new(|cx| InputState::new(window, cx).masked(true));
|
|
||||||
|
|
||||||
let pubkey_input = cx.new(|cx| {
|
let pubkey_input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
@@ -42,7 +38,6 @@ impl BackupKeys {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
password,
|
|
||||||
pubkey_input,
|
pubkey_input,
|
||||||
secret_input,
|
secret_input,
|
||||||
error: None,
|
error: None,
|
||||||
@@ -50,18 +45,8 @@ impl BackupKeys {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn password(&self, cx: &Context<Self>) -> String {
|
|
||||||
self.password.read(cx).value().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 document_dir = document_dir().expect("Failed to get document directory");
|
||||||
let password = self.password.read(cx).value().to_string();
|
|
||||||
|
|
||||||
if password.is_empty() {
|
|
||||||
self.set_error(t!("login.password_is_required"), window, cx);
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let path = cx.prompt_for_new_path(&document_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();
|
||||||
@@ -190,21 +175,5 @@ impl Render for BackupKeys {
|
|||||||
.child(shared_t!("new_account.backup_secret_note")),
|
.child(shared_t!("new_account.backup_secret_note")),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(divider(cx))
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(shared_t!("login.set_password"))
|
|
||||||
.child(TextInput::new(&self.password).small())
|
|
||||||
.when_some(self.error.as_ref(), |this, error| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.italic()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().danger_foreground)
|
|
||||||
.child(error.clone()),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use client_keys::ClientKeys;
|
use anyhow::anyhow;
|
||||||
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, Task,
|
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
||||||
Window,
|
|
||||||
};
|
};
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
|
use registry::keystore::KeyItem;
|
||||||
|
use registry::Registry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::app_state;
|
use states::app_state;
|
||||||
use states::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
|
use states::constants::BUNKER_TIMEOUT;
|
||||||
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};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
|
use ui::notification::Notification;
|
||||||
use ui::popup_menu::PopupMenu;
|
use ui::popup_menu::PopupMenu;
|
||||||
use ui::{v_flex, ContextModal, Disableable, Sizable, StyledExt};
|
use ui::{v_flex, ContextModal, Disableable, StyledExt};
|
||||||
|
|
||||||
use crate::actions::CoopAuthUrlHandler;
|
use crate::actions::CoopAuthUrlHandler;
|
||||||
|
|
||||||
@@ -26,15 +28,19 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Login {
|
pub struct Login {
|
||||||
input: Entity<InputState>,
|
key_input: Entity<InputState>,
|
||||||
|
pass_input: Entity<InputState>,
|
||||||
error: Entity<Option<SharedString>>,
|
error: Entity<Option<SharedString>>,
|
||||||
countdown: Entity<Option<u64>>,
|
countdown: Entity<Option<u64>>,
|
||||||
|
require_password: bool,
|
||||||
logging_in: bool,
|
logging_in: bool,
|
||||||
// Panel
|
|
||||||
|
/// Panel
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
#[allow(unused)]
|
|
||||||
subscriptions: SmallVec<[Subscription; 1]>,
|
/// Event subscriptions
|
||||||
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Login {
|
impl Login {
|
||||||
@@ -43,29 +49,42 @@ impl Login {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("nsec... or bunker://..."));
|
let key_input = cx.new(|cx| InputState::new(window, cx));
|
||||||
|
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
|
|
||||||
let error = cx.new(|_| None);
|
let error = cx.new(|_| None);
|
||||||
let countdown = cx.new(|_| None);
|
let countdown = cx.new(|_| None);
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
// Subscribe to key input events and process login when the user presses enter
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
cx.subscribe_in(&input, window, |this, _e, event, window, cx| {
|
// Subscribe to key input events and process login when the user presses enter
|
||||||
if let InputEvent::PressEnter { .. } = event {
|
cx.subscribe_in(&key_input, window, |this, input, event, window, cx| {
|
||||||
this.login(window, cx);
|
match event {
|
||||||
}
|
InputEvent::PressEnter { .. } => {
|
||||||
|
this.login(window, cx);
|
||||||
|
}
|
||||||
|
InputEvent::Change => {
|
||||||
|
if input.read(cx).value().starts_with("ncryptsec1") {
|
||||||
|
this.require_password = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
input,
|
key_input,
|
||||||
|
pass_input,
|
||||||
error,
|
error,
|
||||||
countdown,
|
countdown,
|
||||||
subscriptions,
|
|
||||||
name: "Login".into(),
|
name: "Login".into(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
logging_in: false,
|
logging_in: false,
|
||||||
|
require_password: false,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,197 +96,34 @@ impl Login {
|
|||||||
// Prevent duplicate login requests
|
// Prevent duplicate login requests
|
||||||
self.set_logging_in(true, cx);
|
self.set_logging_in(true, cx);
|
||||||
|
|
||||||
// Disable the input
|
let value = self.key_input.read(cx).value();
|
||||||
self.input.update(cx, |this, cx| {
|
let password = self.pass_input.read(cx).value();
|
||||||
this.set_loading(true, cx);
|
|
||||||
this.set_disabled(true, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Content can be secret key or bunker://
|
if value.starts_with("bunker://") {
|
||||||
match self.input.read(cx).value().to_string() {
|
self.login_with_bunker(&value, window, cx);
|
||||||
s if s.starts_with("nsec1") => self.ask_for_password(s, window, cx),
|
|
||||||
s if s.starts_with("ncryptsec1") => self.ask_for_password(s, window, cx),
|
|
||||||
s if s.starts_with("bunker://") => self.login_with_bunker(s, window, cx),
|
|
||||||
_ => self.set_error(t!("login.invalid_key"), window, cx),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ask_for_password(&mut self, content: String, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let current_view = cx.entity().downgrade();
|
|
||||||
let is_ncryptsec = content.starts_with("ncryptsec1");
|
|
||||||
|
|
||||||
let pwd_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
|
||||||
let weak_pwd_input = pwd_input.downgrade();
|
|
||||||
|
|
||||||
let confirm_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
|
||||||
let weak_confirm_input = confirm_input.downgrade();
|
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, cx| {
|
|
||||||
let weak_pwd_input = weak_pwd_input.clone();
|
|
||||||
let weak_confirm_input = weak_confirm_input.clone();
|
|
||||||
|
|
||||||
let view_cancel = current_view.clone();
|
|
||||||
let view_ok = current_view.clone();
|
|
||||||
|
|
||||||
let label: SharedString = if !is_ncryptsec {
|
|
||||||
t!("login.set_password").into()
|
|
||||||
} else {
|
|
||||||
t!("login.password_to_decrypt").into()
|
|
||||||
};
|
|
||||||
|
|
||||||
let description: SharedString = if is_ncryptsec {
|
|
||||||
t!("login.password_description").into()
|
|
||||||
} else {
|
|
||||||
t!("login.password_description_full").into()
|
|
||||||
};
|
|
||||||
|
|
||||||
this.overlay_closable(false)
|
|
||||||
.show_close(false)
|
|
||||||
.keyboard(false)
|
|
||||||
.confirm()
|
|
||||||
.on_cancel(move |_, window, cx| {
|
|
||||||
view_cancel
|
|
||||||
.update(cx, |this, cx| {
|
|
||||||
this.set_error(t!("login.password_is_required"), window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
true
|
|
||||||
})
|
|
||||||
.on_ok(move |_, window, cx| {
|
|
||||||
let value = weak_pwd_input
|
|
||||||
.read_with(cx, |state, _cx| state.value().to_owned())
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
let confirm = weak_confirm_input
|
|
||||||
.read_with(cx, |state, _cx| state.value().to_owned())
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
view_ok
|
|
||||||
.update(cx, |this, cx| {
|
|
||||||
this.verify_password(value, confirm, is_ncryptsec, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
true
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w_full()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_2()
|
|
||||||
.text_sm()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
|
||||||
.child(label)
|
|
||||||
.child(TextInput::new(&pwd_input).small()),
|
|
||||||
)
|
|
||||||
.when(content.starts_with("nsec1"), |this| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
|
||||||
.child(SharedString::new(t!("login.confirm_password")))
|
|
||||||
.child(TextInput::new(&confirm_input).small()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.italic()
|
|
||||||
.text_color(cx.theme().text_placeholder)
|
|
||||||
.child(description),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_password(
|
|
||||||
&mut self,
|
|
||||||
password: Option<SharedString>,
|
|
||||||
confirm: Option<SharedString>,
|
|
||||||
is_ncryptsec: bool,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let Some(password) = password else {
|
|
||||||
self.set_error(t!("login.password_is_required"), window, cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if password.is_empty() {
|
|
||||||
self.set_error(t!("login.password_is_required"), window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip verification if key is ncryptsec
|
|
||||||
if is_ncryptsec {
|
|
||||||
self.login_with_keys(password.to_string(), window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(confirm) = confirm else {
|
|
||||||
self.set_error(t!("login.must_confirm_password"), window, cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if confirm.is_empty() {
|
|
||||||
self.set_error(t!("login.must_confirm_password"), window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if password != confirm {
|
|
||||||
self.set_error(t!("login.password_not_match"), window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.login_with_keys(password.to_string(), window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn login_with_keys(&mut self, password: String, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let value = self.input.read(cx).value().to_string();
|
|
||||||
|
|
||||||
let secret_key = if value.starts_with("nsec1") {
|
|
||||||
SecretKey::parse(&value).ok()
|
|
||||||
} else if value.starts_with("ncryptsec1") {
|
} else if value.starts_with("ncryptsec1") {
|
||||||
EncryptedSecretKey::from_bech32(&value)
|
self.login_with_password(&value, &password, cx);
|
||||||
.map(|enc| enc.decrypt(&password).ok())
|
} else if value.starts_with("nsec1") {
|
||||||
.unwrap_or_default()
|
if let Ok(secret) = SecretKey::parse(&value) {
|
||||||
|
let keys = Keys::new(secret);
|
||||||
|
self.login_with_keys(keys, cx);
|
||||||
|
} else {
|
||||||
|
self.set_error("Invalid", cx);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
self.set_error("Invalid", cx);
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(secret_key) = secret_key {
|
|
||||||
let keys = Keys::new(secret_key);
|
|
||||||
|
|
||||||
// Encrypt and save user secret key to disk
|
|
||||||
self.write_keys_to_disk(&keys, password, cx);
|
|
||||||
|
|
||||||
// Set the client's signer with the current keys
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let client = app_state().client();
|
|
||||||
client.set_signer(keys).await;
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
} else {
|
|
||||||
self.set_error(t!("login.key_invalid"), window, cx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn login_with_bunker(&mut self, content: String, window: &mut Window, cx: &mut Context<Self>) {
|
fn login_with_bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Ok(uri) = NostrConnectURI::parse(content) else {
|
let Ok(uri) = NostrConnectURI::parse(content) else {
|
||||||
self.set_error(t!("login.bunker_invalid"), window, cx);
|
self.set_error(t!("login.bunker_invalid"), cx);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let client_keys = ClientKeys::global(cx);
|
let app_keys = Keys::generate();
|
||||||
let app_keys = client_keys.read(cx).keys();
|
|
||||||
|
|
||||||
let timeout = Duration::from_secs(BUNKER_TIMEOUT);
|
let timeout = Duration::from_secs(BUNKER_TIMEOUT);
|
||||||
let mut signer = NostrConnect::new(uri, app_keys, timeout, None).unwrap();
|
let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap();
|
||||||
|
|
||||||
// Handle auth url with the default browser
|
// Handle auth url with the default browser
|
||||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||||
@@ -293,103 +149,152 @@ impl Login {
|
|||||||
|
|
||||||
// Handle connection
|
// Handle connection
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
match signer.bunker_uri().await {
|
let result = signer.bunker_uri().await;
|
||||||
Ok(uri) => {
|
|
||||||
this.update(cx, |this, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.write_uri_to_disk(signer, uri, cx);
|
match result {
|
||||||
})
|
Ok(uri) => {
|
||||||
.ok();
|
this.save_connection(&app_keys, &uri, window, cx);
|
||||||
}
|
this.connect(signer, cx);
|
||||||
Err(error) => {
|
}
|
||||||
this.update_in(cx, |this, window, cx| {
|
Err(e) => {
|
||||||
this.set_error(error.to_string(), window, cx);
|
window.push_notification(Notification::error(e.to_string()), cx);
|
||||||
// Force reset the client keys
|
}
|
||||||
//
|
};
|
||||||
// This step is necessary to ensure that user can retry the connection
|
})
|
||||||
client_keys.update(cx, |this, cx| {
|
.ok();
|
||||||
this.force_new_keys(cx);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_uri_to_disk(
|
fn save_connection(
|
||||||
&mut self,
|
&mut self,
|
||||||
signer: NostrConnect,
|
keys: &Keys,
|
||||||
uri: NostrConnectURI,
|
uri: &NostrConnectURI,
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let mut uri_without_secret = uri.to_string();
|
|
||||||
|
|
||||||
// Clear the secret parameter in the URI if it exists
|
|
||||||
if let Some(secret) = uri.secret() {
|
|
||||||
uri_without_secret = uri_without_secret.replace(secret, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
|
|
||||||
let client = app_state().client();
|
|
||||||
|
|
||||||
// Update the client's signer
|
|
||||||
client.set_signer(signer).await;
|
|
||||||
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
|
|
||||||
let event = EventBuilder::new(Kind::ApplicationSpecificData, uri_without_secret)
|
|
||||||
.tags(vec![Tag::identifier(ACCOUNT_IDENTIFIER)])
|
|
||||||
.build(public_key)
|
|
||||||
.sign(&Keys::generate())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Save the event to the database
|
|
||||||
client.database().save_event(&event).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
task.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_keys_to_disk(&self, keys: &Keys, password: String, cx: &mut Context<Self>) {
|
|
||||||
let keys = keys.to_owned();
|
|
||||||
let public_key = keys.public_key();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
if let Ok(enc_key) =
|
|
||||||
EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown)
|
|
||||||
{
|
|
||||||
let client = app_state().client();
|
|
||||||
let value = enc_key.to_bech32().unwrap();
|
|
||||||
let keys = Keys::generate();
|
|
||||||
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
|
|
||||||
let kind = Kind::ApplicationSpecificData;
|
|
||||||
|
|
||||||
let builder = EventBuilder::new(kind, value)
|
|
||||||
.tags(tags)
|
|
||||||
.build(public_key)
|
|
||||||
.sign(&keys)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(event) = builder {
|
|
||||||
if let Err(e) = client.database().save_event(&event).await {
|
|
||||||
log::error!("Failed to save event: {e}");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_error(
|
|
||||||
&mut self,
|
|
||||||
message: impl Into<SharedString>,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
let keystore = Registry::global(cx).read(cx).keystore();
|
||||||
|
let username = keys.public_key().to_hex();
|
||||||
|
let secret = keys.secret_key().to_secret_bytes();
|
||||||
|
let mut clean_uri = uri.to_string();
|
||||||
|
|
||||||
|
// Clear the secret parameter in the URI if it exists
|
||||||
|
if let Some(s) = uri.secret() {
|
||||||
|
clean_uri = clean_uri.replace(s, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let user_url = KeyItem::User.to_string();
|
||||||
|
let bunker_url = KeyItem::Bunker.to_string();
|
||||||
|
let user_password = clean_uri.into_bytes();
|
||||||
|
|
||||||
|
// Write bunker uri to keyring for further connection
|
||||||
|
if let Err(e) = keystore
|
||||||
|
.write_credentials(&user_url, "bunker", &user_password, cx)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
this.update_in(cx, |_, window, cx| {
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the app keys for further connection
|
||||||
|
if let Err(e) = keystore
|
||||||
|
.write_credentials(&bunker_url, &username, &secret, cx)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
this.update_in(cx, |_, window, cx| {
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect(&mut self, signer: NostrConnect, cx: &mut Context<Self>) {
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let client = app_state().client();
|
||||||
|
client.set_signer(signer).await;
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn login_with_password(&mut self, content: &str, pwd: &str, cx: &mut Context<Self>) {
|
||||||
|
if pwd.is_empty() {
|
||||||
|
self.set_error("Password is required", cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(enc) = EncryptedSecretKey::from_bech32(content) else {
|
||||||
|
self.set_error("Secret Key is invalid", cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let password = pwd.to_owned();
|
||||||
|
|
||||||
|
// Decrypt in the background to ensure it doesn't block the UI
|
||||||
|
let task = cx.background_spawn(async move {
|
||||||
|
if let Ok(content) = enc.decrypt(&password) {
|
||||||
|
Ok(Keys::new(content))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Invalid password"))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let result = task.await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
match result {
|
||||||
|
Ok(keys) => {
|
||||||
|
this.login_with_keys(keys, cx);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.set_error(e.to_string(), cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn login_with_keys(&mut self, keys: Keys, cx: &mut Context<Self>) {
|
||||||
|
let keystore = Registry::global(cx).read(cx).keystore();
|
||||||
|
let username = keys.public_key().to_hex();
|
||||||
|
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let bunker_url = KeyItem::User.to_string();
|
||||||
|
|
||||||
|
// Write the app keys for further connection
|
||||||
|
if let Err(e) = keystore
|
||||||
|
.write_credentials(&bunker_url, &username, &secret, cx)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_error(e.to_string(), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the signer
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let client = app_state().client();
|
||||||
|
client.set_signer(keys).await;
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
S: Into<SharedString>,
|
||||||
|
{
|
||||||
// Reset the log in state
|
// Reset the log in state
|
||||||
self.set_logging_in(false, cx);
|
self.set_logging_in(false, cx);
|
||||||
|
|
||||||
@@ -402,13 +307,6 @@ impl Login {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re enable the input
|
|
||||||
self.input.update(cx, |this, cx| {
|
|
||||||
this.set_value("", window, cx);
|
|
||||||
this.set_loading(false, cx);
|
|
||||||
this.set_disabled(false, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear the error message after 3 secs
|
// Clear the error message after 3 secs
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
cx.background_executor().timer(Duration::from_secs(3)).await;
|
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||||
@@ -493,7 +391,25 @@ impl Render for Login {
|
|||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.child(TextInput::new(&self.input))
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child("nsec or bunker://")
|
||||||
|
.child(TextInput::new(&self.key_input)),
|
||||||
|
)
|
||||||
|
.when(self.require_password, |this| {
|
||||||
|
this.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child("Password:")
|
||||||
|
.child(TextInput::new(&self.pass_input)),
|
||||||
|
)
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
Button::new("login")
|
Button::new("login")
|
||||||
.label(t!("common.continue"))
|
.label(t!("common.continue"))
|
||||||
@@ -513,13 +429,13 @@ impl Render for Login {
|
|||||||
.child(shared_t!("login.approve_message", i = i)),
|
.child(shared_t!("login.approve_message", i = i)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when_some(self.error.read(cx).clone(), |this, error| {
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_center()
|
.text_center()
|
||||||
.text_color(cx.theme().danger_foreground)
|
.text_color(cx.theme().danger_foreground)
|
||||||
.child(error),
|
.child(error.clone()),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ use gpui::{
|
|||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use registry::keystore::KeyItem;
|
||||||
|
use registry::Registry;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use states::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS};
|
use states::constants::BOOTSTRAP_RELAYS;
|
||||||
use states::{app_state, default_nip17_relays, default_nip65_relays};
|
use states::{app_state, default_nip17_relays, default_nip65_relays};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
@@ -81,21 +83,17 @@ impl NewAccount {
|
|||||||
.on_ok(move |_, window, cx| {
|
.on_ok(move |_, window, cx| {
|
||||||
weak_view
|
weak_view
|
||||||
.update(cx, |this, cx| {
|
.update(cx, |this, cx| {
|
||||||
let password = this.password(cx);
|
|
||||||
let current_view = current_view.clone();
|
let current_view = current_view.clone();
|
||||||
|
|
||||||
if let Some(task) = this.backup(window, cx) {
|
if let Some(task) = this.backup(window, cx) {
|
||||||
cx.spawn_in(window, async move |_, cx| {
|
cx.spawn_in(window, async move |_, cx| {
|
||||||
task.await;
|
task.await;
|
||||||
|
|
||||||
cx.update(|window, cx| {
|
current_view
|
||||||
current_view
|
.update(cx, |this, cx| {
|
||||||
.update(cx, |this, cx| {
|
this.set_signer(cx);
|
||||||
this.set_signer(password, window, cx);
|
})
|
||||||
})
|
.ok();
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
@@ -107,10 +105,13 @@ impl NewAccount {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_signer(&mut self, password: String, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_signer(&mut self, cx: &mut Context<Self>) {
|
||||||
window.close_modal(cx);
|
let keystore = Registry::global(cx).read(cx).keystore();
|
||||||
|
|
||||||
let keys = self.temp_keys.read(cx).clone();
|
let keys = self.temp_keys.read(cx).clone();
|
||||||
|
let username = keys.public_key().to_hex();
|
||||||
|
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
||||||
|
|
||||||
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 mut metadata = Metadata::new().display_name(name.clone()).name(name);
|
let mut metadata = Metadata::new().display_name(name.clone()).name(name);
|
||||||
@@ -119,81 +120,59 @@ impl NewAccount {
|
|||||||
metadata = metadata.picture(url);
|
metadata = metadata.picture(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Encrypt and save user secret key to disk
|
cx.spawn(async move |_, cx| {
|
||||||
self.write_keys_to_disk(&keys, password, cx);
|
let url = KeyItem::User.to_string();
|
||||||
|
|
||||||
// Set the client's signer with the current keys
|
// Write the app keys for further connection
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
keystore
|
||||||
let client = app_state().client();
|
.write_credentials(&url, &username, &secret, cx)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
// Update the signer
|
||||||
// Set the client's signer with the current keys
|
// Set the client's signer with the current keys
|
||||||
client.set_signer(keys).await;
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
|
|
||||||
// Verify the signer
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
|
|
||||||
// Construct a NIP-65 event
|
|
||||||
let event = EventBuilder::new(Kind::RelayList, "")
|
|
||||||
.tags(default_nip65_relays().iter().map(|(url, metadata)| {
|
|
||||||
Tag::relay_metadata(url.to_owned(), metadata.to_owned())
|
|
||||||
}))
|
|
||||||
.sign(&signer)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Set NIP-65 relays
|
|
||||||
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
|
|
||||||
|
|
||||||
// Construct a NIP-17 event
|
|
||||||
let event = EventBuilder::new(Kind::InboxRelays, "")
|
|
||||||
.tags(
|
|
||||||
default_nip17_relays()
|
|
||||||
.iter()
|
|
||||||
.map(|url| Tag::relay(url.to_owned())),
|
|
||||||
)
|
|
||||||
.sign(&signer)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Set NIP-17 relays
|
|
||||||
client.send_event(&event).await?;
|
|
||||||
|
|
||||||
// Construct a metadata event
|
|
||||||
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
|
|
||||||
|
|
||||||
// Set metadata
|
|
||||||
client.send_event(&event).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
task.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_keys_to_disk(&self, keys: &Keys, password: String, cx: &mut Context<Self>) {
|
|
||||||
let keys = keys.to_owned();
|
|
||||||
let public_key = keys.public_key();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
if let Ok(enc_key) =
|
|
||||||
EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown)
|
|
||||||
{
|
|
||||||
let client = app_state().client();
|
let client = app_state().client();
|
||||||
let value = enc_key.to_bech32().unwrap();
|
|
||||||
let keys = Keys::generate();
|
|
||||||
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
|
|
||||||
let kind = Kind::ApplicationSpecificData;
|
|
||||||
|
|
||||||
let builder = EventBuilder::new(kind, value)
|
// Set the client's signer with the current keys
|
||||||
.tags(tags)
|
client.set_signer(keys).await;
|
||||||
.build(public_key)
|
|
||||||
.sign(&keys)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(event) = builder {
|
// Verify the signer
|
||||||
if let Err(e) = client.database().save_event(&event).await {
|
let signer = client.signer().await?;
|
||||||
log::error!("Failed to save event: {e}");
|
|
||||||
};
|
// Construct a NIP-65 event
|
||||||
}
|
let event = EventBuilder::new(Kind::RelayList, "")
|
||||||
}
|
.tags(
|
||||||
|
default_nip65_relays()
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|(url, metadata)| Tag::relay_metadata(url, metadata)),
|
||||||
|
)
|
||||||
|
.sign(&signer)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Set NIP-65 relays
|
||||||
|
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
|
||||||
|
|
||||||
|
// Construct a NIP-17 event
|
||||||
|
let event = EventBuilder::new(Kind::InboxRelays, "")
|
||||||
|
.tags(default_nip17_relays().iter().cloned().map(Tag::relay))
|
||||||
|
.sign(&signer)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Set NIP-17 relays
|
||||||
|
client.send_event(&event).await?;
|
||||||
|
|
||||||
|
// Construct a metadata event
|
||||||
|
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
|
||||||
|
|
||||||
|
// Set metadata
|
||||||
|
client.send_event(&event).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
task.detach();
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use client_keys::ClientKeys;
|
|
||||||
use common::display::TextUtils;
|
use common::display::TextUtils;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, px, relative, svg, AnyElement, App, AppContext, ClipboardItem, Context, Entity,
|
div, img, px, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||||
EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
|
FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement, Render,
|
||||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window,
|
SharedString, StatefulInteractiveElement, Styled, Task, Window,
|
||||||
};
|
};
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
|
use registry::keystore::KeyItem;
|
||||||
|
use registry::Registry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::app_state;
|
use states::app_state;
|
||||||
use states::constants::{ACCOUNT_IDENTIFIER, APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
use states::constants::{APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
||||||
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};
|
||||||
@@ -21,7 +22,7 @@ use ui::notification::Notification;
|
|||||||
use ui::popup_menu::PopupMenu;
|
use ui::popup_menu::PopupMenu;
|
||||||
use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt};
|
use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt};
|
||||||
|
|
||||||
use crate::chatspace;
|
use crate::chatspace::{self};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||||
Onboarding::new(window, cx)
|
Onboarding::new(window, cx)
|
||||||
@@ -59,14 +60,14 @@ impl NostrConnectApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Onboarding {
|
pub struct Onboarding {
|
||||||
nostr_connect_uri: Entity<NostrConnectURI>,
|
app_keys: Keys,
|
||||||
nostr_connect: Entity<Option<NostrConnect>>,
|
qr_code: Option<Arc<Image>>,
|
||||||
qr_code: Entity<Option<Arc<Image>>>,
|
|
||||||
connecting: bool,
|
/// Panel
|
||||||
// Panel
|
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
_subscriptions: SmallVec<[Subscription; 2]>,
|
|
||||||
|
/// Background tasks
|
||||||
_tasks: SmallVec<[Task<()>; 1]>,
|
_tasks: SmallVec<[Task<()>; 1]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,145 +77,101 @@ impl Onboarding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let nostr_connect = cx.new(|_| None);
|
let app_keys = Keys::generate();
|
||||||
let qr_code = cx.new(|_| None);
|
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||||
|
|
||||||
|
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||||
|
let uri = NostrConnectURI::client(app_keys.public_key(), vec![relay], APP_NAME);
|
||||||
|
let qr_code = uri.to_string().to_qr();
|
||||||
|
|
||||||
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||||
//
|
//
|
||||||
// Direct connection initiated by the client
|
// Direct connection initiated by the client
|
||||||
let nostr_connect_uri = cx.new(|cx| {
|
let signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap();
|
||||||
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
|
||||||
let app_keys = ClientKeys::read_global(cx).keys();
|
|
||||||
NostrConnectURI::client(app_keys.public_key(), vec![relay], APP_NAME)
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
// Clean up when the current view is released
|
tasks.push(
|
||||||
subscriptions.push(cx.on_release_in(window, |this, window, cx| {
|
// Wait for nostr connect
|
||||||
this.shutdown_nostr_connect(window, cx);
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Set Nostr Connect after the view is initialized
|
|
||||||
cx.defer_in(window, |this, window, cx| {
|
|
||||||
this.set_connect(window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
nostr_connect,
|
|
||||||
nostr_connect_uri,
|
|
||||||
qr_code,
|
|
||||||
connecting: false,
|
|
||||||
name: "Onboarding".into(),
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
_subscriptions: subscriptions,
|
|
||||||
_tasks: smallvec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_connecting(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.connecting = true;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let uri = self.nostr_connect_uri.read(cx).clone();
|
|
||||||
let app_keys = ClientKeys::read_global(cx).keys();
|
|
||||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
|
||||||
|
|
||||||
self.qr_code.update(cx, |this, cx| {
|
|
||||||
*this = uri.to_string().to_qr();
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.nostr_connect.update(cx, |this, cx| {
|
|
||||||
*this = NostrConnect::new(uri, app_keys, timeout, None).ok();
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
|
|
||||||
self._tasks.push(
|
|
||||||
// Wait for Nostr Connect approval
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let connect = this.read_with(cx, |this, cx| this.nostr_connect.read(cx).clone());
|
let result = signer.bunker_uri().await;
|
||||||
|
|
||||||
if let Ok(Some(signer)) = connect {
|
this.update_in(cx, |this, window, cx| {
|
||||||
match signer.bunker_uri().await {
|
match result {
|
||||||
Ok(uri) => {
|
Ok(uri) => {
|
||||||
this.update(cx, |this, cx| {
|
this.save_connection(&uri, window, cx);
|
||||||
this.set_connecting(cx);
|
this.connect(signer, cx);
|
||||||
this.write_uri_to_disk(signer, uri, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update_in(cx, |_, window, cx| {
|
window.push_notification(Notification::error(e.to_string()), cx);
|
||||||
window.push_notification(
|
|
||||||
Notification::error(e.to_string()).title("Nostr Connect"),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
})
|
||||||
|
.ok();
|
||||||
}),
|
}),
|
||||||
)
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
qr_code,
|
||||||
|
app_keys,
|
||||||
|
name: "Onboarding".into(),
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
_tasks: tasks,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_uri_to_disk(
|
fn save_connection(
|
||||||
&mut self,
|
&mut self,
|
||||||
signer: NostrConnect,
|
uri: &NostrConnectURI,
|
||||||
uri: NostrConnectURI,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let mut uri_without_secret = uri.to_string();
|
let keystore = Registry::global(cx).read(cx).keystore();
|
||||||
|
let username = self.app_keys.public_key().to_hex();
|
||||||
|
let secret = self.app_keys.secret_key().to_secret_bytes();
|
||||||
|
let mut clean_uri = uri.to_string();
|
||||||
|
|
||||||
// Clear the secret parameter in the URI if it exists
|
// Clear the secret parameter in the URI if it exists
|
||||||
if let Some(secret) = uri.secret() {
|
if let Some(s) = uri.secret() {
|
||||||
uri_without_secret = uri_without_secret.replace(secret, "");
|
clean_uri = clean_uri.replace(s, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let client = app_state().client();
|
let user_url = KeyItem::User.to_string();
|
||||||
|
let bunker_url = KeyItem::Bunker.to_string();
|
||||||
|
let user_password = clean_uri.into_bytes();
|
||||||
|
|
||||||
// Update the client's signer
|
// Write bunker uri to keyring for further connection
|
||||||
client.set_signer(signer).await;
|
if let Err(e) = keystore
|
||||||
|
.write_credentials(&user_url, "bunker", &user_password, cx)
|
||||||
let signer = client.signer().await?;
|
.await
|
||||||
let public_key = signer.get_public_key().await?;
|
{
|
||||||
|
this.update_in(cx, |_, window, cx| {
|
||||||
let event = EventBuilder::new(Kind::ApplicationSpecificData, uri_without_secret)
|
window.push_notification(e.to_string(), cx);
|
||||||
.tags(vec![Tag::identifier(ACCOUNT_IDENTIFIER)])
|
|
||||||
.build(public_key)
|
|
||||||
.sign(&Keys::generate())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Save the event to the database
|
|
||||||
client.database().save_event(&event).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
task.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn copy_uri(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
|
||||||
self.nostr_connect_uri.read(cx).to_string(),
|
|
||||||
));
|
|
||||||
window.push_notification(t!("common.copied"), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shutdown_nostr_connect(&mut self, _window: &mut Window, cx: &mut App) {
|
|
||||||
if !self.connecting {
|
|
||||||
if let Some(signer) = self.nostr_connect.read(cx).clone() {
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
log::info!("Shutting down Nostr Connect");
|
|
||||||
signer.shutdown().await;
|
|
||||||
})
|
})
|
||||||
.detach();
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Write the app keys for further connection
|
||||||
|
if let Err(e) = keystore
|
||||||
|
.write_credentials(&bunker_url, &username, &secret, cx)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
this.update_in(cx, |_, window, cx| {
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect(&mut self, signer: NostrConnect, cx: &mut Context<Self>) {
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let client = app_state().client();
|
||||||
|
client.set_signer(signer).await;
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_apps(&self, cx: &Context<Self>) -> impl IntoIterator<Item = impl IntoElement> {
|
fn render_apps(&self, cx: &Context<Self>) -> impl IntoIterator<Item = impl IntoElement> {
|
||||||
@@ -368,23 +325,14 @@ impl Render for Onboarding {
|
|||||||
.gap_5()
|
.gap_5()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.when_some(self.qr_code.read(cx).as_ref(), |this, qr| {
|
.when_some(self.qr_code.as_ref(), |this, qr| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
img(qr.clone())
|
||||||
.id("")
|
.size(px(256.))
|
||||||
.child(
|
.rounded_xl()
|
||||||
img(qr.clone())
|
.shadow_lg()
|
||||||
.size(px(256.))
|
.border_1()
|
||||||
.rounded_xl()
|
.border_color(cx.theme().element_active),
|
||||||
.shadow_lg()
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().element_active),
|
|
||||||
)
|
|
||||||
.on_click(cx.listener(
|
|
||||||
move |this, _e, window, cx| {
|
|
||||||
this.copy_uri(window, cx)
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ smallvec.workspace = true
|
|||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
fuzzy-matcher = "0.3.7"
|
fuzzy-matcher = "0.3.7"
|
||||||
rustls = "0.23.23"
|
rustls = "0.23.23"
|
||||||
|
|||||||
191
crates/registry/src/keystore.rs
Normal file
191
crates/registry/src/keystore.rs
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
use std::any::Any;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use futures::FutureExt as _;
|
||||||
|
use gpui::AsyncApp;
|
||||||
|
use states::paths::config_dir;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum KeyItem {
|
||||||
|
User,
|
||||||
|
Bunker,
|
||||||
|
Client,
|
||||||
|
Encryption,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for KeyItem {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::User => write!(f, "coop-user"),
|
||||||
|
Self::Bunker => write!(f, "coop-bunker"),
|
||||||
|
Self::Client => write!(f, "coop-client"),
|
||||||
|
Self::Encryption => write!(f, "coop-encryption"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<KeyItem> for String {
|
||||||
|
fn from(item: KeyItem) -> Self {
|
||||||
|
item.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait KeyStore: Any + Send + Sync {
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
|
||||||
|
/// Reads the credentials from the provider.
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn read_credentials<'a>(
|
||||||
|
&'a self,
|
||||||
|
url: &'a str,
|
||||||
|
cx: &'a AsyncApp,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>>;
|
||||||
|
|
||||||
|
/// Writes the credentials to the provider.
|
||||||
|
fn write_credentials<'a>(
|
||||||
|
&'a self,
|
||||||
|
url: &'a str,
|
||||||
|
username: &'a str,
|
||||||
|
password: &'a [u8],
|
||||||
|
cx: &'a AsyncApp,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
|
||||||
|
|
||||||
|
/// Deletes the credentials from the provider.
|
||||||
|
fn delete_credentials<'a>(
|
||||||
|
&'a self,
|
||||||
|
url: &'a str,
|
||||||
|
cx: &'a AsyncApp,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A credentials provider that stores credentials in the system keychain.
|
||||||
|
pub struct KeyringProvider;
|
||||||
|
|
||||||
|
impl KeyStore for KeyringProvider {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"keyring"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_credentials<'a>(
|
||||||
|
&'a self,
|
||||||
|
url: &'a str,
|
||||||
|
cx: &'a AsyncApp,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
|
||||||
|
async move { cx.update(|cx| cx.read_credentials(url))?.await }.boxed_local()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_credentials<'a>(
|
||||||
|
&'a self,
|
||||||
|
url: &'a str,
|
||||||
|
username: &'a str,
|
||||||
|
password: &'a [u8],
|
||||||
|
cx: &'a AsyncApp,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||||
|
async move {
|
||||||
|
cx.update(move |cx| cx.write_credentials(url, username, password))?
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
.boxed_local()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_credentials<'a>(
|
||||||
|
&'a self,
|
||||||
|
url: &'a str,
|
||||||
|
cx: &'a AsyncApp,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||||
|
async move { cx.update(move |cx| cx.delete_credentials(url))?.await }.boxed_local()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A credentials provider that stores credentials in a local file.
|
||||||
|
pub struct FileProvider {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileProvider {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let path = config_dir().join(".keys");
|
||||||
|
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { path }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_credentials(&self) -> Result<HashMap<String, (String, Vec<u8>)>> {
|
||||||
|
let json = std::fs::read(&self.path)?;
|
||||||
|
let credentials: HashMap<String, (String, Vec<u8>)> = serde_json::from_slice(&json)?;
|
||||||
|
|
||||||
|
Ok(credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_credentials(&self, credentials: &HashMap<String, (String, Vec<u8>)>) -> Result<()> {
|
||||||
|
let json = serde_json::to_string(credentials)?;
|
||||||
|
std::fs::write(&self.path, json)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FileProvider {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyStore for FileProvider {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"file"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_credentials<'a>(
|
||||||
|
&'a self,
|
||||||
|
url: &'a str,
|
||||||
|
_cx: &'a AsyncApp,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
|
||||||
|
async move {
|
||||||
|
Ok(self
|
||||||
|
.load_credentials()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.get(url)
|
||||||
|
.cloned())
|
||||||
|
}
|
||||||
|
.boxed_local()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_credentials<'a>(
|
||||||
|
&'a self,
|
||||||
|
url: &'a str,
|
||||||
|
username: &'a str,
|
||||||
|
password: &'a [u8],
|
||||||
|
_cx: &'a AsyncApp,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||||
|
async move {
|
||||||
|
let mut credentials = self.load_credentials().unwrap_or_default();
|
||||||
|
credentials.insert(url.to_string(), (username.to_string(), password.to_vec()));
|
||||||
|
|
||||||
|
self.save_credentials(&credentials)
|
||||||
|
}
|
||||||
|
.boxed_local()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_credentials<'a>(
|
||||||
|
&'a self,
|
||||||
|
url: &'a str,
|
||||||
|
_cx: &'a AsyncApp,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||||
|
async move {
|
||||||
|
let mut credentials = self.load_credentials()?;
|
||||||
|
credentials.remove(url);
|
||||||
|
|
||||||
|
self.save_credentials(&credentials)
|
||||||
|
}
|
||||||
|
.boxed_local()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use common::event::EventUtils;
|
use common::event::EventUtils;
|
||||||
@@ -14,13 +15,19 @@ use room::RoomKind;
|
|||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::app_state;
|
use states::app_state;
|
||||||
|
use states::constants::KEYRING_URL;
|
||||||
use states::state::UnwrappingStatus;
|
use states::state::UnwrappingStatus;
|
||||||
|
|
||||||
|
use crate::keystore::{FileProvider, KeyStore, KeyringProvider};
|
||||||
use crate::room::Room;
|
use crate::room::Room;
|
||||||
|
|
||||||
|
pub mod keystore;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod room;
|
pub mod room;
|
||||||
|
|
||||||
|
pub static DISABLE_KEYRING: LazyLock<bool> =
|
||||||
|
LazyLock::new(|| std::env::var("DISABLE_KEYRING").is_ok_and(|value| !value.is_empty()));
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
Registry::set_global(cx.new(Registry::new), cx);
|
Registry::set_global(cx.new(Registry::new), cx);
|
||||||
}
|
}
|
||||||
@@ -36,7 +43,6 @@ pub enum RegistryEvent {
|
|||||||
NewRequest(RoomKind),
|
NewRequest(RoomKind),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Registry {
|
pub struct Registry {
|
||||||
/// Collection of all chat rooms
|
/// Collection of all chat rooms
|
||||||
pub rooms: Vec<Entity<Room>>,
|
pub rooms: Vec<Entity<Room>>,
|
||||||
@@ -47,11 +53,17 @@ pub struct Registry {
|
|||||||
/// Status of the unwrapping process
|
/// Status of the unwrapping process
|
||||||
pub unwrapping_status: Entity<UnwrappingStatus>,
|
pub unwrapping_status: Entity<UnwrappingStatus>,
|
||||||
|
|
||||||
|
/// Key Store for storing credentials
|
||||||
|
pub keystore: Arc<dyn KeyStore>,
|
||||||
|
|
||||||
|
/// Whether the keystore has been initialized
|
||||||
|
pub initialized_keystore: bool,
|
||||||
|
|
||||||
/// Public Key of the currently activated signer
|
/// Public Key of the currently activated signer
|
||||||
signer_pubkey: Option<PublicKey>,
|
signer_pubkey: Option<PublicKey>,
|
||||||
|
|
||||||
/// Tasks for asynchronous operations
|
/// Tasks for asynchronous operations
|
||||||
_tasks: SmallVec<[Task<()>; 1]>,
|
_tasks: SmallVec<[Task<()>; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<RegistryEvent> for Registry {}
|
impl EventEmitter<RegistryEvent> for Registry {}
|
||||||
@@ -75,8 +87,38 @@ impl Registry {
|
|||||||
/// Create a new registry instance
|
/// Create a new registry instance
|
||||||
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||||
let unwrapping_status = cx.new(|_| UnwrappingStatus::default());
|
let unwrapping_status = cx.new(|_| UnwrappingStatus::default());
|
||||||
|
let read_credential = cx.read_credentials(KEYRING_URL);
|
||||||
|
let initialized_keystore = cfg!(debug_assertions) || *DISABLE_KEYRING;
|
||||||
|
let keystore: Arc<dyn KeyStore> = if cfg!(debug_assertions) || *DISABLE_KEYRING {
|
||||||
|
Arc::new(FileProvider::default())
|
||||||
|
} else {
|
||||||
|
Arc::new(KeyringProvider)
|
||||||
|
};
|
||||||
|
|
||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
|
if !(cfg!(debug_assertions) || *DISABLE_KEYRING) {
|
||||||
|
tasks.push(
|
||||||
|
// Verify the keyring access
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let result = read_credential.await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
if let Err(e) = result {
|
||||||
|
log::error!("Keyring error: {e}");
|
||||||
|
// For Linux:
|
||||||
|
// The user has not installed secret service on their system
|
||||||
|
// Fall back to the file provider
|
||||||
|
this.keystore = Arc::new(FileProvider::default());
|
||||||
|
}
|
||||||
|
this.initialized_keystore = true;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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| {
|
||||||
@@ -96,6 +138,8 @@ impl Registry {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
unwrapping_status,
|
unwrapping_status,
|
||||||
|
keystore,
|
||||||
|
initialized_keystore,
|
||||||
rooms: vec![],
|
rooms: vec![],
|
||||||
persons: HashMap::new(),
|
persons: HashMap::new(),
|
||||||
signer_pubkey: None,
|
signer_pubkey: None,
|
||||||
@@ -122,6 +166,16 @@ impl Registry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the keystore.
|
||||||
|
pub fn keystore(&self) -> Arc<dyn KeyStore> {
|
||||||
|
Arc::clone(&self.keystore)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the keystore is a file keystore.
|
||||||
|
pub fn is_using_file_keystore(&self) -> bool {
|
||||||
|
self.keystore.name() == "file"
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the public key of the currently activated signer.
|
/// Returns the public key of the currently activated signer.
|
||||||
pub fn signer_pubkey(&self) -> Option<PublicKey> {
|
pub fn signer_pubkey(&self) -> Option<PublicKey> {
|
||||||
self.signer_pubkey
|
self.signer_pubkey
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ setting_accessors! {
|
|||||||
pub contact_bypass: bool,
|
pub contact_bypass: bool,
|
||||||
pub auto_login: bool,
|
pub auto_login: bool,
|
||||||
pub auto_auth: bool,
|
pub auto_auth: bool,
|
||||||
|
pub disable_keyring: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@@ -62,6 +63,7 @@ pub struct Settings {
|
|||||||
pub contact_bypass: bool,
|
pub contact_bypass: bool,
|
||||||
pub auto_login: bool,
|
pub auto_login: bool,
|
||||||
pub auto_auth: bool,
|
pub auto_auth: bool,
|
||||||
|
pub disable_keyring: bool,
|
||||||
pub authenticated_relays: Vec<RelayUrl>,
|
pub authenticated_relays: Vec<RelayUrl>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@ impl Default for Settings {
|
|||||||
contact_bypass: true,
|
contact_bypass: true,
|
||||||
auto_login: false,
|
auto_login: false,
|
||||||
auto_auth: true,
|
auto_auth: true,
|
||||||
|
disable_keyring: false,
|
||||||
authenticated_relays: vec![],
|
authenticated_relays: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ pub const APP_NAME: &str = "Coop";
|
|||||||
pub const APP_ID: &str = "su.reya.coop";
|
pub const APP_ID: &str = "su.reya.coop";
|
||||||
pub const APP_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDc4MkNFRkQ2RkVGQURGNzUKUldSMTMvcisxdThzZUZraHc4Vno3NVNJek81VkJFUEV3MkJweGFxQXhpekdSU1JIekpqMG4yemMK";
|
pub const APP_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDc4MkNFRkQ2RkVGQURGNzUKUldSMTMvcisxdThzZUZraHc4Vno3NVNJek81VkJFUEV3MkJweGFxQXhpekdSU1JIekpqMG4yemMK";
|
||||||
pub const APP_UPDATER_ENDPOINT: &str = "https://coop-updater.reya.su/";
|
pub const APP_UPDATER_ENDPOINT: &str = "https://coop-updater.reya.su/";
|
||||||
pub const KEYRING_URL: &str = "Coop Safe Storage";
|
|
||||||
|
|
||||||
pub const ACCOUNT_IDENTIFIER: &str = "coop:user";
|
pub const KEYRING_URL: &str = "Coop Safe Storage";
|
||||||
pub const SETTINGS_IDENTIFIER: &str = "coop:settings";
|
pub const SETTINGS_IDENTIFIER: &str = "coop:settings";
|
||||||
|
|
||||||
/// Bootstrap Relays.
|
/// Bootstrap Relays.
|
||||||
|
|||||||
@@ -60,6 +60,20 @@ common:
|
|||||||
configure:
|
configure:
|
||||||
en: "Configure"
|
en: "Configure"
|
||||||
|
|
||||||
|
keyring_disable:
|
||||||
|
label:
|
||||||
|
en: "Keyring is disabled"
|
||||||
|
body_1:
|
||||||
|
en: "Coop cannot access the Keyring Service on your system."
|
||||||
|
body_2:
|
||||||
|
en: "By design, Coop uses Keyring to store your credentials."
|
||||||
|
body_3:
|
||||||
|
en: "Without access to Keyring, Coop will store your credentials as plain text."
|
||||||
|
body_4:
|
||||||
|
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."
|
||||||
|
|
||||||
auto_update:
|
auto_update:
|
||||||
updating:
|
updating:
|
||||||
en: "Installing the new update..."
|
en: "Installing the new update..."
|
||||||
@@ -102,12 +116,6 @@ onboarding:
|
|||||||
ext_login_note:
|
ext_login_note:
|
||||||
en: "You will need to keep your default browser open."
|
en: "You will need to keep your default browser open."
|
||||||
|
|
||||||
proxy:
|
|
||||||
label:
|
|
||||||
en: "Waiting for approval"
|
|
||||||
description:
|
|
||||||
en: "Open your default browser and approve the connection request in your Nostr Signer extension"
|
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
label:
|
label:
|
||||||
en: "Authentication Required"
|
en: "Authentication Required"
|
||||||
@@ -116,20 +124,6 @@ auth:
|
|||||||
requests:
|
requests:
|
||||||
en: "You have %{u} pending authentication requests"
|
en: "You have %{u} pending authentication requests"
|
||||||
|
|
||||||
startup:
|
|
||||||
client_keys_warning:
|
|
||||||
en: "Warning"
|
|
||||||
client_keys_desc:
|
|
||||||
en: "Allow Coop to read the client keys stored in Keychain to continue"
|
|
||||||
create_new_keys:
|
|
||||||
en: "Create New Keys"
|
|
||||||
auto_login_in_progress:
|
|
||||||
en: "Auto login in progress"
|
|
||||||
stuck:
|
|
||||||
en: "Stuck?"
|
|
||||||
reset:
|
|
||||||
en: "Reset"
|
|
||||||
|
|
||||||
new_account:
|
new_account:
|
||||||
title:
|
title:
|
||||||
en: "Create a new identity"
|
en: "Create a new identity"
|
||||||
@@ -181,6 +175,8 @@ login:
|
|||||||
en: "Bunker is not valid"
|
en: "Bunker is not valid"
|
||||||
logging_in:
|
logging_in:
|
||||||
en: "Logging in..."
|
en: "Logging in..."
|
||||||
|
keyring_required:
|
||||||
|
en: "You must allow Coop access to the keyring to continue."
|
||||||
|
|
||||||
mailbox:
|
mailbox:
|
||||||
modal:
|
modal:
|
||||||
|
|||||||
Reference in New Issue
Block a user