Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e01d2fbef3 | ||
|
|
c3d677ca81 |
@@ -202,11 +202,11 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ar_archive_writer"
|
name = "ar_archive_writer"
|
||||||
version = "0.5.3"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6"
|
checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"object 0.39.1",
|
"object",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -493,12 +493,6 @@ version = "0.5.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
|
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "atomic-destructor"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7d919cb60ba95c87ba42777e9e246c4e8d658057299b437b7512531ce0a09a23"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -546,10 +540,17 @@ dependencies = [
|
|||||||
name = "auto_update"
|
name = "auto_update"
|
||||||
version = "1.0.0-beta5"
|
version = "1.0.0-beta5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"common",
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui-updater",
|
|
||||||
"instant",
|
"instant",
|
||||||
"log",
|
"log",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"smallvec",
|
||||||
|
"smol",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -634,7 +635,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
"object 0.37.3",
|
"object",
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
@@ -838,21 +839,6 @@ dependencies = [
|
|||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "browser-signer-proxy"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"atomic-destructor",
|
|
||||||
"event-listener",
|
|
||||||
"nostr",
|
|
||||||
"opaquerr",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"smol",
|
|
||||||
"tracing",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bstr"
|
name = "bstr"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
@@ -1101,7 +1087,6 @@ dependencies = [
|
|||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"person",
|
"person",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"regex",
|
|
||||||
"serde",
|
"serde",
|
||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@@ -1246,7 +1231,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui_util",
|
"gpui_util",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
@@ -1368,35 +1353,6 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cookie"
|
|
||||||
version = "0.18.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
|
||||||
dependencies = [
|
|
||||||
"percent-encoding",
|
|
||||||
"time",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cookie_store"
|
|
||||||
version = "0.22.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
|
|
||||||
dependencies = [
|
|
||||||
"cookie",
|
|
||||||
"document-features",
|
|
||||||
"idna",
|
|
||||||
"indexmap",
|
|
||||||
"log",
|
|
||||||
"serde",
|
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
|
||||||
"time",
|
|
||||||
"url",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coop"
|
name = "coop"
|
||||||
version = "1.0.0-beta5"
|
version = "1.0.0-beta5"
|
||||||
@@ -1412,7 +1368,6 @@ dependencies = [
|
|||||||
"gpui_platform",
|
"gpui_platform",
|
||||||
"gpui_windows",
|
"gpui_windows",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
|
||||||
"person",
|
"person",
|
||||||
"reqwest_client",
|
"reqwest_client",
|
||||||
"settings",
|
"settings",
|
||||||
@@ -1760,7 +1715,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_refineable"
|
name = "derive_refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2656,7 +2611,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -2735,26 +2690,10 @@ dependencies = [
|
|||||||
"zed-scap",
|
"zed-scap",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gpui-updater"
|
|
||||||
version = "0.0.6"
|
|
||||||
source = "git+https://github.com/AprilNEA/gpui-updater?tag=v0.0.6#e8a85768793ac3238b0c1bc1f32d6a1544667ec3"
|
|
||||||
dependencies = [
|
|
||||||
"gpui",
|
|
||||||
"minisign-verify",
|
|
||||||
"semver",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"sha2 0.10.9",
|
|
||||||
"thiserror 2.0.19",
|
|
||||||
"tracing",
|
|
||||||
"ureq",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_linux"
|
name = "gpui_linux"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_unix",
|
"accesskit_unix",
|
||||||
@@ -2806,7 +2745,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_macos"
|
name = "gpui_macos"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_macos",
|
"accesskit_macos",
|
||||||
@@ -2855,7 +2794,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_macros"
|
name = "gpui_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2866,7 +2805,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_platform"
|
name = "gpui_platform"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -2879,7 +2818,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_shared_string"
|
name = "gpui_shared_string"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2889,7 +2828,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_tokio"
|
name = "gpui_tokio"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -2900,7 +2839,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_util"
|
name = "gpui_util"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"log",
|
"log",
|
||||||
@@ -2910,7 +2849,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_web"
|
name = "gpui_web"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
@@ -2933,7 +2872,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_wgpu"
|
name = "gpui_wgpu"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -2963,7 +2902,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_windows"
|
name = "gpui_windows"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_windows",
|
"accesskit_windows",
|
||||||
@@ -3253,7 +3192,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#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
@@ -3273,7 +3212,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#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -4048,6 +3987,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "maybe-rayon"
|
name = "maybe-rayon"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -4071,7 +4019,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen",
|
"bindgen",
|
||||||
@@ -4144,12 +4092,6 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minisign-verify"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -4270,8 +4212,8 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.45.0-alpha.8"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d0a1d67d3c9e5cf9710807a6a414c155a5f47215"
|
source = "git+https://github.com/rust-nostr/nostr#affbc9e8f918ead0f1857a6c7d4afdc16f812a6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -4295,8 +4237,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-blossom"
|
name = "nostr-blossom"
|
||||||
version = "0.45.0-alpha.8"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d0a1d67d3c9e5cf9710807a6a414c155a5f47215"
|
source = "git+https://github.com/rust-nostr/nostr#affbc9e8f918ead0f1857a6c7d4afdc16f812a6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bitcoin_hashes",
|
"bitcoin_hashes",
|
||||||
@@ -4308,8 +4250,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.45.0-alpha.8"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d0a1d67d3c9e5cf9710807a6a414c155a5f47215"
|
source = "git+https://github.com/rust-nostr/nostr#affbc9e8f918ead0f1857a6c7d4afdc16f812a6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -4322,8 +4264,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.45.0-alpha.8"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d0a1d67d3c9e5cf9710807a6a414c155a5f47215"
|
source = "git+https://github.com/rust-nostr/nostr#affbc9e8f918ead0f1857a6c7d4afdc16f812a6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"btreecap",
|
"btreecap",
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
@@ -4333,8 +4275,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip"
|
name = "nostr-gossip"
|
||||||
version = "0.45.0-alpha.8"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d0a1d67d3c9e5cf9710807a6a414c155a5f47215"
|
source = "git+https://github.com/rust-nostr/nostr#affbc9e8f918ead0f1857a6c7d4afdc16f812a6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nostr",
|
"nostr",
|
||||||
"opaquerr",
|
"opaquerr",
|
||||||
@@ -4342,8 +4284,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip-memory"
|
name = "nostr-gossip-memory"
|
||||||
version = "0.45.0-alpha.8"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d0a1d67d3c9e5cf9710807a6a414c155a5f47215"
|
source = "git+https://github.com/rust-nostr/nostr#affbc9e8f918ead0f1857a6c7d4afdc16f812a6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -4354,8 +4296,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.45.0-alpha.8"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d0a1d67d3c9e5cf9710807a6a414c155a5f47215"
|
source = "git+https://github.com/rust-nostr/nostr#affbc9e8f918ead0f1857a6c7d4afdc16f812a6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"flume 0.12.0",
|
"flume 0.12.0",
|
||||||
@@ -4368,8 +4310,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-memory"
|
name = "nostr-memory"
|
||||||
version = "0.45.0-alpha.8"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d0a1d67d3c9e5cf9710807a6a414c155a5f47215"
|
source = "git+https://github.com/rust-nostr/nostr#affbc9e8f918ead0f1857a6c7d4afdc16f812a6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"btreecap",
|
"btreecap",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4379,8 +4321,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.45.0-alpha.8"
|
version = "0.45.0-alpha.7"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#d0a1d67d3c9e5cf9710807a6a414c155a5f47215"
|
source = "git+https://github.com/rust-nostr/nostr#affbc9e8f918ead0f1857a6c7d4afdc16f812a6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -4789,15 +4731,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "object"
|
|
||||||
version = "0.39.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
@@ -4991,7 +4924,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#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5011,6 +4944,7 @@ dependencies = [
|
|||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"state",
|
"state",
|
||||||
|
"urlencoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5802,7 +5736,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
]
|
]
|
||||||
@@ -5884,7 +5818,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#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -6199,7 +6133,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "scheduler"
|
name = "scheduler"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-task",
|
"async-task",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
@@ -6747,7 +6681,6 @@ name = "state"
|
|||||||
version = "1.0.0-beta5"
|
version = "1.0.0-beta5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"browser-signer-proxy",
|
|
||||||
"common",
|
"common",
|
||||||
"flume 0.11.1",
|
"flume 0.11.1",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -6826,7 +6759,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#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heapless 0.9.3",
|
"heapless 0.9.3",
|
||||||
"log",
|
"log",
|
||||||
@@ -7181,7 +7114,6 @@ dependencies = [
|
|||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"time-core",
|
"time-core",
|
||||||
"time-macros",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7190,16 +7122,6 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time-macros"
|
|
||||||
version = "0.2.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
|
|
||||||
dependencies = [
|
|
||||||
"num-conv",
|
|
||||||
"time-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiny-keccak"
|
name = "tiny-keccak"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
@@ -7546,10 +7468,14 @@ version = "0.3.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex-automata",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
@@ -7773,37 +7699,6 @@ version = "0.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ureq"
|
|
||||||
version = "3.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
|
|
||||||
dependencies = [
|
|
||||||
"base64",
|
|
||||||
"cookie_store",
|
|
||||||
"log",
|
|
||||||
"percent-encoding",
|
|
||||||
"rustls",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"ureq-proto",
|
|
||||||
"utf8-zero",
|
|
||||||
"webpki-roots 1.0.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ureq-proto"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
|
|
||||||
dependencies = [
|
|
||||||
"base64",
|
|
||||||
"http",
|
|
||||||
"httparse",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -7817,6 +7712,12 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "usvg"
|
name = "usvg"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
@@ -7850,12 +7751,6 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "utf8-zero"
|
|
||||||
version = "0.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -7865,7 +7760,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "util_macros"
|
name = "util_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"perf",
|
"perf",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -9117,8 +9012,6 @@ name = "workspace"
|
|||||||
version = "1.0.0-beta5"
|
version = "1.0.0-beta5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"auto_update",
|
|
||||||
"browser-signer-proxy",
|
|
||||||
"chat",
|
"chat",
|
||||||
"chat_ui",
|
"chat_ui",
|
||||||
"common",
|
"common",
|
||||||
@@ -9197,9 +9090,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xcursor"
|
name = "xcursor"
|
||||||
version = "0.3.11"
|
version = "0.3.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23"
|
checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xim-ctext"
|
name = "xim-ctext"
|
||||||
@@ -9600,7 +9493,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zlog"
|
name = "zlog"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -9617,7 +9510,7 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ztracing"
|
name = "ztracing"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -9628,7 +9521,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ztracing_macro"
|
name = "ztracing_macro"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5e1fd392f67e27fa1da91bad43eef7db1a5dec23"
|
source = "git+https://github.com/zed-industries/zed#ae394f3d474f4996d2cdef6ee97551fdb6748acd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zune-core"
|
name = "zune-core"
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ schemars = "1"
|
|||||||
smallvec = "1.14.0"
|
smallvec = "1.14.0"
|
||||||
smol = "2"
|
smol = "2"
|
||||||
webbrowser = "1.0.4"
|
webbrowser = "1.0.4"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] }
|
||||||
errno = { version = "0.3.14", default-features = false }
|
errno = { version = "0.3.14", default-features = false }
|
||||||
instant = "0.1"
|
instant = "0.1"
|
||||||
|
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
[workspace]
|
|
||||||
resolver = "2"
|
|
||||||
members = ["crates/*", "desktop", "web"]
|
|
||||||
default-members = ["desktop"]
|
|
||||||
|
|
||||||
[workspace.package]
|
|
||||||
version = "1.0.0-beta5"
|
|
||||||
edition = "2024"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
|
||||||
# GPUI
|
|
||||||
gpui = { git = "https://github.com/zed-industries/zed" }
|
|
||||||
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "x11", "wayland"] }
|
|
||||||
gpui_linux = { git = "https://github.com/zed-industries/zed" }
|
|
||||||
gpui_windows = { git = "https://github.com/zed-industries/zed" }
|
|
||||||
gpui_macos = { git = "https://github.com/zed-industries/zed" }
|
|
||||||
gpui_tokio = { git = "https://github.com/zed-industries/zed" }
|
|
||||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
|
||||||
|
|
||||||
# Nostr
|
|
||||||
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
|
|
||||||
nostr-memory = { git = "https://github.com/rust-nostr/nostr" }
|
|
||||||
nostr-blossom = { git = "https://github.com/rust-nostr/nostr" }
|
|
||||||
nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" }
|
|
||||||
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
|
||||||
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
|
|
||||||
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44" ] }
|
|
||||||
|
|
||||||
# Others
|
|
||||||
anyhow = "1.0.44"
|
|
||||||
chrono = { version = "0.4.38", features = ["wasmbind"] }
|
|
||||||
futures = "0.3"
|
|
||||||
itertools = "0.13.0"
|
|
||||||
log = "0.4"
|
|
||||||
oneshot = "0.1.10"
|
|
||||||
flume = { version = "0.11.1", default-features = false, features = ["async", "select"] }
|
|
||||||
rust-embed = { version = "8.5", features = ["include-exclude"] }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
schemars = "1"
|
|
||||||
smallvec = "1.14.0"
|
|
||||||
smol = "2"
|
|
||||||
webbrowser = "1.0.4"
|
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
|
||||||
errno = { version = "0.3.14", default-features = false }
|
|
||||||
instant = "0.1"
|
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
# Use stacker's psm version which may have better WASM support
|
|
||||||
psm = { git = "https://github.com/rust-lang/stacker", branch = "master" }
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
strip = true
|
|
||||||
opt-level = "z"
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
panic = "abort"
|
|
||||||
|
|
||||||
[profile.profiling]
|
|
||||||
inherits = "release"
|
|
||||||
debug = true
|
|
||||||
@@ -1,5 +1,125 @@
|
|||||||
|

|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/reyakov/coop/actions/workflows/rust.yml">
|
||||||
|
<img alt="Actions" src="https://github.com/reyakov/coop/actions/workflows/rust.yml/badge.svg">
|
||||||
|
</a>
|
||||||
|
<img alt="GitHub repo size" src="https://img.shields.io/github/repo-size/reyakov/coop">
|
||||||
|
<img alt="GitHub issues" src="https://img.shields.io/github/issues-raw/reyakov/coop">
|
||||||
|
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/reyakov/coop">
|
||||||
|
</p>
|
||||||
|
|
||||||
Coop is a simple, fast, and reliable nostr client for secure messaging across all platforms.
|
Coop is a simple, fast, and reliable nostr client for secure messaging across all platforms.
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
<p float="left">
|
||||||
|
<img src="/docs/mac_01.png" width="250" />
|
||||||
|
<img src="/docs/mac_02.png" width="250" />
|
||||||
|
<img src="/docs/mac_03.png" width="250" />
|
||||||
|
<img src="/docs/mac_04.png" width="250" />
|
||||||
|
<img src="/docs/mac_05.png" width="250" />
|
||||||
|
<img src="/docs/mac_06.png" width="250" />
|
||||||
|
<img src="/docs/mac_07.png" width="250" />
|
||||||
|
<img src="/docs/mac_08.png" width="250" />
|
||||||
|
<img src="/docs/mac_09.png" width="250" />
|
||||||
|
<img src="/docs/linux_01.png" width="250" />
|
||||||
|
<img src="/docs/linux_02.png" width="250" />
|
||||||
|
<img src="/docs/linux_03.png" width="250" />
|
||||||
|
<img src="/docs/linux_04.png" width="250" />
|
||||||
|
<img src="/docs/linux_05.png" width="250" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
To install Coop, follow these steps:
|
||||||
|
|
||||||
|
1. **Download the Latest Release**:
|
||||||
|
|
||||||
|
- Visit the [Coop Releases page on GitHub](https://github.com/reyakov/coop/releases).
|
||||||
|
- Download the package that matches your operating system (Windows, macOS, or Linux).
|
||||||
|
|
||||||
|
2. **Install**:
|
||||||
|
|
||||||
|
- **Windows**: Run the downloaded `.exe` installer and follow the on-screen instructions.
|
||||||
|
- **macOS**: Open the downloaded `.dmg` file and drag Coop to your Applications folder.
|
||||||
|
- **Linux**: Run the downloaded `.flatpak` or `.snap` installer and follow the on-screen instructions.
|
||||||
|
|
||||||
|
3. **Run Coop**:
|
||||||
|
- Launch Coop from your Applications folder (macOS) or by double-clicking the executable (Windows/Linux).
|
||||||
|
|
||||||
|
For more detailed instructions, refer to the [Release Notes](#) on GitHub.
|
||||||
|
|
||||||
|
### Developing Coop
|
||||||
|
|
||||||
|
Coop is built using Rust and GPUI. All Nostr related stuffs handled by [Rust Nostr SDK](https://github.com/rust-nostr/nostr)
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
|
||||||
|
- **Rust Toolchain**: Ensure you have Rust installed. If not, you can install it using [rustup](https://rustup.rs/).
|
||||||
|
- **Cargo**: Rust's package manager, which comes bundled with the Rust installation.
|
||||||
|
- **Git**: To clone the repository and manage version control.
|
||||||
|
|
||||||
|
#### Setting Up the Development Environment
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/reyakov/coop.git
|
||||||
|
cd coop
|
||||||
|
```
|
||||||
|
|
||||||
|
2.1 Install Linux dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./script/linux
|
||||||
|
```
|
||||||
|
|
||||||
|
2.2 Install FreeBSD dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./script/freebsd
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install Rust dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run the app:
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Building for Production
|
||||||
|
|
||||||
|
To build Coop for production, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
This will generate an optimized binary in the `target/release` directory.
|
||||||
|
|
||||||
|
#### Contributing Code
|
||||||
|
|
||||||
|
If you'd like to contribute to Coop, please follow these steps:
|
||||||
|
|
||||||
|
1. Fork the repository.
|
||||||
|
2. Create a new branch for your feature or bugfix.
|
||||||
|
3. Make your changes and ensure all tests pass.
|
||||||
|
4. Submit a pull request with a detailed description of your changes.
|
||||||
|
|
||||||
|
For more information, see the [Contributing](#contributing) section.
|
||||||
|
|
||||||
|
#### Additional Resources
|
||||||
|
|
||||||
|
- [Rust Nostr](https://github.com/rust-nostr/nostr/)
|
||||||
|
- [GPUI](https://www.gpui.rs/)
|
||||||
|
- [GPUI Components](https://github.com/longbridge/gpui-component/)
|
||||||
|
- [Coop Issue Tracker](https://github.com/reyakov/coop/issues/)
|
||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
Copyright (C) 2025 Ren Amamiya & other Coop contributors
|
Copyright (C) 2025 Ren Amamiya & other Coop contributors
|
||||||
|
|||||||
@@ -5,8 +5,18 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
instant.workspace = true
|
instant.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
gpui-updater = { git = "https://github.com/AprilNEA/gpui-updater", tag = "v0.0.6", features = ["gpui"] }
|
semver = "1.0.27"
|
||||||
|
tempfile = "3.23.0"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
smol.workspace = true
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
#![cfg(not(target_arch = "wasm32"))]
|
#![cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, SharedString, Subscription, Window};
|
use std::ffi::OsString;
|
||||||
use gpui_updater::{EngineConfig, GitHubSource, UpdateStatus, Updater, Version};
|
use std::path::{Path, PathBuf};
|
||||||
use instant::{Duration, Instant};
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||||
|
use gpui::http_client::{AsyncBody, HttpClient};
|
||||||
|
use gpui::{
|
||||||
|
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task,
|
||||||
|
Window,
|
||||||
|
};
|
||||||
|
use instant::Duration;
|
||||||
|
use semver::Version;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use smallvec::{SmallVec, smallvec};
|
||||||
|
use smol::fs::File;
|
||||||
|
use smol::io::AsyncReadExt;
|
||||||
|
use smol::process::Command;
|
||||||
|
|
||||||
|
const GITHUB_API_URL: &str = "https://api.github.com";
|
||||||
const COOP_UPDATE_EXPLANATION: &str = "COOP_UPDATE_EXPLANATION";
|
const COOP_UPDATE_EXPLANATION: &str = "COOP_UPDATE_EXPLANATION";
|
||||||
|
|
||||||
fn get_github_repo_owner() -> String {
|
fn get_github_repo_owner() -> String {
|
||||||
@@ -15,15 +30,12 @@ fn get_github_repo_name() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_flatpak_installation() -> bool {
|
fn is_flatpak_installation() -> bool {
|
||||||
|
// Check if app is installed via Flatpak
|
||||||
std::env::var("FLATPAK_ID").is_ok() || std::env::var(COOP_UPDATE_EXPLANATION).is_ok()
|
std::env::var("FLATPAK_ID").is_ok() || std::env::var(COOP_UPDATE_EXPLANATION).is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the auto-update system.
|
|
||||||
///
|
|
||||||
/// Skips initialization when running as a Flatpak (updates are handled by the
|
|
||||||
/// Flatpak distribution channel). Otherwise creates the global [`AutoUpdater`]
|
|
||||||
/// entity and schedules a check for updates after a 2-minute delay.
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) {
|
pub fn init(window: &mut Window, cx: &mut App) {
|
||||||
|
// Skip auto-update initialization if installed via Flatpak
|
||||||
if is_flatpak_installation() {
|
if is_flatpak_installation() {
|
||||||
log::info!("Skipping auto-update initialization: App is installed via Flatpak");
|
log::info!("Skipping auto-update initialization: App is installed via Flatpak");
|
||||||
return;
|
return;
|
||||||
@@ -36,145 +48,516 @@ struct GlobalAutoUpdater(Entity<AutoUpdater>);
|
|||||||
|
|
||||||
impl Global for GlobalAutoUpdater {}
|
impl Global for GlobalAutoUpdater {}
|
||||||
|
|
||||||
/// Observable auto-update status — re-exported from [`gpui_updater::UpdateStatus`].
|
#[cfg(not(target_os = "windows"))]
|
||||||
pub use gpui_updater::UpdateStatus as AutoUpdateStatus;
|
struct InstallerDir(tempfile::TempDir);
|
||||||
|
|
||||||
/// The global auto-updater entity.
|
#[cfg(not(target_os = "windows"))]
|
||||||
///
|
impl InstallerDir {
|
||||||
/// Wraps [`gpui_updater::Updater`] with Coop-specific configuration
|
async fn new() -> Result<Self, Error> {
|
||||||
/// (GitHub repo, Flatpak detection, delayed auto-check).
|
Ok(Self(
|
||||||
///
|
tempfile::Builder::new()
|
||||||
/// Retrieve the global instance via [`AutoUpdater::global`].
|
.prefix("coop-auto-update")
|
||||||
|
.tempdir()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
self.0.path()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
struct InstallerDir(PathBuf);
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
impl InstallerDir {
|
||||||
|
async fn new() -> Result<Self, Error> {
|
||||||
|
let installer_dir = std::env::current_exe()?
|
||||||
|
.parent()
|
||||||
|
.context("No parent dir for Coop.exe")?
|
||||||
|
.join("updates");
|
||||||
|
|
||||||
|
if smol::fs::metadata(&installer_dir).await.is_ok() {
|
||||||
|
smol::fs::remove_dir_all(&installer_dir).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
smol::fs::create_dir(&installer_dir).await?;
|
||||||
|
|
||||||
|
Ok(Self(installer_dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
self.0.as_path()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MacOsUnmounter<'a> {
|
||||||
|
mount_path: PathBuf,
|
||||||
|
background_executor: &'a BackgroundExecutor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for MacOsUnmounter<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mount_path = std::mem::take(&mut self.mount_path);
|
||||||
|
|
||||||
|
self.background_executor
|
||||||
|
.spawn(async move {
|
||||||
|
let unmount_output = Command::new("hdiutil")
|
||||||
|
.args(["detach", "-force"])
|
||||||
|
.arg(&mount_path)
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match unmount_output {
|
||||||
|
Ok(output) if output.status.success() => {
|
||||||
|
log::info!("Successfully unmounted the disk image");
|
||||||
|
}
|
||||||
|
Ok(output) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to unmount disk image: {:?}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("Error while trying to unmount disk image: {:?}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum AutoUpdateStatus {
|
||||||
|
Idle,
|
||||||
|
Checking,
|
||||||
|
Checked { download_url: String },
|
||||||
|
Installing,
|
||||||
|
Updated,
|
||||||
|
Errored { msg: Box<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<AutoUpdateStatus> for AutoUpdateStatus {
|
||||||
|
fn as_ref(&self) -> &AutoUpdateStatus {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AutoUpdateStatus {
|
||||||
|
pub fn is_updating(&self) -> bool {
|
||||||
|
matches!(self, Self::Checked { .. } | Self::Installing)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_updated(&self) -> bool {
|
||||||
|
matches!(self, Self::Updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checked(download_url: String) -> Self {
|
||||||
|
Self::Checked { download_url }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error(e: String) -> Self {
|
||||||
|
Self::Errored { msg: Box::new(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct GitHubRelease {
|
||||||
|
pub tag_name: String,
|
||||||
|
pub assets: Vec<GitHubAsset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct GitHubAsset {
|
||||||
|
pub name: String,
|
||||||
|
pub browser_download_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct AutoUpdater {
|
pub struct AutoUpdater {
|
||||||
/// The underlying gpui-updater entity that does the heavy lifting.
|
/// Current status of the auto updater
|
||||||
pub updater: Entity<Updater>,
|
pub status: AutoUpdateStatus,
|
||||||
/// Currently running app version.
|
|
||||||
|
/// Current version of the application
|
||||||
pub version: Version,
|
pub version: Version,
|
||||||
/// Keeps the observer subscription alive.
|
|
||||||
_subscription: Subscription,
|
/// Event subscriptions
|
||||||
/// When the last error was recorded, so we can reset to idle after 5s.
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
error_time: Option<Instant>,
|
|
||||||
|
/// Background tasks
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AutoUpdater {
|
impl AutoUpdater {
|
||||||
/// Retrieve the global auto updater instance.
|
/// Retrieve the global auto updater instance
|
||||||
pub fn global(cx: &App) -> Entity<Self> {
|
pub fn global(cx: &App) -> Entity<Self> {
|
||||||
cx.global::<GlobalAutoUpdater>().0.clone()
|
cx.global::<GlobalAutoUpdater>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the global auto updater instance
|
||||||
fn set_global(state: Entity<Self>, cx: &mut App) {
|
fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||||
cx.set_global(GlobalAutoUpdater(state));
|
cx.set_global(GlobalAutoUpdater(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
let repo_owner = get_github_repo_owner();
|
subscriptions.push(
|
||||||
let repo_name = get_github_repo_name();
|
// Observe the status
|
||||||
|
cx.observe_self(|this, cx| {
|
||||||
|
if let AutoUpdateStatus::Checked { download_url } = this.status.clone() {
|
||||||
|
this.download_and_install(&download_url, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
let source =
|
// Run at the end of current cycle
|
||||||
GitHubSource::new(&repo_owner, &repo_name).asset_contains(match std::env::consts::OS {
|
cx.defer_in(window, |this, _window, cx| {
|
||||||
"macos" => "macos",
|
this.check(cx);
|
||||||
"linux" => "linux",
|
|
||||||
_ => "",
|
|
||||||
});
|
|
||||||
|
|
||||||
let updater: Entity<Updater> =
|
|
||||||
cx.new(|cx| Updater::new(source, EngineConfig::new(version.clone()), cx));
|
|
||||||
|
|
||||||
// When an update becomes available, automatically download and install it.
|
|
||||||
let subscription = cx.observe(&updater, |this: &mut AutoUpdater, _updater, cx| {
|
|
||||||
let status = this.updater.read(cx).status().clone();
|
|
||||||
|
|
||||||
if matches!(status, UpdateStatus::Available(_)) {
|
|
||||||
this.updater.update(cx, |updater, cx| {
|
|
||||||
updater.download_and_install(cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(status, UpdateStatus::Errored(_)) {
|
|
||||||
this.error_time = Some(Instant::now());
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
cx.background_executor().timer(Duration::from_secs(5)).await;
|
|
||||||
this.update(cx, |_this, cx| cx.notify()).ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
} else {
|
|
||||||
this.error_time = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Schedule an auto-check after a 2-minute delay (deferred to run at the
|
|
||||||
// end of the current frame so the window is fully set up).
|
|
||||||
cx.defer_in(window, |_this, _window, cx| {
|
|
||||||
let duration = Duration::from_secs(120);
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
cx.background_executor().timer(duration).await;
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.updater.update(cx, |updater, cx| {
|
|
||||||
updater.check(cx);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
updater,
|
status: AutoUpdateStatus::Idle,
|
||||||
version,
|
version,
|
||||||
_subscription: subscription,
|
tasks: vec![],
|
||||||
error_time: None,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn idle(&self, cx: &App) -> bool {
|
fn set_status(&mut self, status: AutoUpdateStatus, cx: &mut Context<Self>) {
|
||||||
let status = self.updater.read(cx).status();
|
self.status = status;
|
||||||
if status == &UpdateStatus::Idle {
|
cx.notify();
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if matches!(status, UpdateStatus::Errored(_))
|
|
||||||
&& self
|
|
||||||
.error_time
|
|
||||||
.is_some_and(|t| t.elapsed() >= Duration::from_secs(5))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status(&self, cx: &App) -> SharedString {
|
fn check(&mut self, cx: &mut Context<Self>) {
|
||||||
let status = self.updater.read(cx).status();
|
let version = self.version.clone();
|
||||||
|
let duration = Duration::from_secs(120);
|
||||||
|
let task = self.check_for_updates(version, cx);
|
||||||
|
|
||||||
match status {
|
// Check for updates after 2 minutes
|
||||||
UpdateStatus::Idle => "Up to date".into(),
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
UpdateStatus::Checking => "Checking for updates…".into(),
|
cx.background_executor().timer(duration).await;
|
||||||
UpdateStatus::UpToDate => "Up to date".into(),
|
|
||||||
UpdateStatus::Available(version) => format!("Version {version} available").into(),
|
// Update the status to checking
|
||||||
UpdateStatus::Downloading { downloaded, total } => {
|
this.update(cx, |this, cx| {
|
||||||
let total_mb = total.map(|t| t as f64 / 1_048_576.0);
|
this.set_status(AutoUpdateStatus::Checking, cx);
|
||||||
let downloaded_mb = *downloaded as f64 / 1_048_576.0;
|
})?;
|
||||||
match total_mb {
|
|
||||||
Some(t) => format!("Downloading {downloaded_mb:.1} / {t:.1} MB").into(),
|
match task.await {
|
||||||
None => format!("Downloading {downloaded_mb:.1} MB").into(),
|
Ok(download_url) => {
|
||||||
|
// Update the status to checked with download URL
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_status(AutoUpdateStatus::checked(download_url), cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to check for updates: {e}");
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_status(AutoUpdateStatus::Idle, cx);
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UpdateStatus::Installing => "Installing update…".into(),
|
|
||||||
UpdateStatus::Staged(version) => {
|
Ok(())
|
||||||
format!("Version {version} ready — restart to apply").into()
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_for_updates(&self, version: Version, cx: &App) -> Task<Result<String, Error>> {
|
||||||
|
let http_client = cx.http_client();
|
||||||
|
let repo_owner = get_github_repo_owner();
|
||||||
|
let repo_name = get_github_repo_name();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let url = format!(
|
||||||
|
"{}/repos/{}/{}/releases/latest",
|
||||||
|
GITHUB_API_URL, repo_owner, repo_name
|
||||||
|
);
|
||||||
|
|
||||||
|
let async_body = AsyncBody::default();
|
||||||
|
let mut body = Vec::new();
|
||||||
|
let mut response = http_client.get(&url, async_body, false).await?;
|
||||||
|
|
||||||
|
// Read the response body into a vector
|
||||||
|
response.body_mut().read_to_end(&mut body).await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(anyhow!("GitHub API returned error: {}", response.status()));
|
||||||
}
|
}
|
||||||
UpdateStatus::Errored(msg) => {
|
|
||||||
if self
|
// Parse the response body as JSON
|
||||||
.error_time
|
let release: GitHubRelease = serde_json::from_slice(&body)?;
|
||||||
.is_some_and(|t| t.elapsed() >= Duration::from_secs(5))
|
|
||||||
{
|
// Parse version from tag (remove 'v' prefix if present)
|
||||||
"Up to date".into()
|
let tag_version = release.tag_name.trim_start_matches('v');
|
||||||
} else {
|
let new_version = Version::parse(tag_version).context(format!(
|
||||||
format!("Update failed: {msg}").into()
|
"Failed to parse version from tag: {}",
|
||||||
|
release.tag_name
|
||||||
|
))?;
|
||||||
|
|
||||||
|
if new_version > version {
|
||||||
|
// Find the appropriate asset for the current platform
|
||||||
|
let current_os = std::env::consts::OS;
|
||||||
|
let asset_name = match current_os {
|
||||||
|
"macos" => "Coop.dmg",
|
||||||
|
"linux" => "coop.tar.gz",
|
||||||
|
"windows" => "Coop.exe",
|
||||||
|
_ => return Err(anyhow!("Unsupported OS: {}", current_os)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let download_url = release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|asset| asset.name == asset_name)
|
||||||
|
.map(|asset| asset.browser_download_url.clone())
|
||||||
|
.context(format!(
|
||||||
|
"No {} asset found in release {}",
|
||||||
|
asset_name, release.tag_name
|
||||||
|
))?;
|
||||||
|
|
||||||
|
Ok(download_url)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"No update available. Current: {}, Latest: {}",
|
||||||
|
version,
|
||||||
|
new_version
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_and_install(&mut self, download_url: &str, cx: &mut Context<Self>) {
|
||||||
|
let http_client = cx.http_client();
|
||||||
|
let download_url = download_url.to_string();
|
||||||
|
|
||||||
|
let task: Task<Result<(InstallerDir, PathBuf), Error>> = cx.background_spawn(async move {
|
||||||
|
let installer_dir = InstallerDir::new().await?;
|
||||||
|
let target_path = Self::target_path(&installer_dir).await?;
|
||||||
|
|
||||||
|
// Download the release
|
||||||
|
download(&download_url, &target_path, http_client).await?;
|
||||||
|
|
||||||
|
Ok((installer_dir, target_path))
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(
|
||||||
|
// Install the new release
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_status(AutoUpdateStatus::Installing, cx);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match task.await {
|
||||||
|
Ok((installer_dir, target_path)) => {
|
||||||
|
if Self::install(installer_dir, target_path, cx).await.is_ok() {
|
||||||
|
// Update the status to updated
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_status(AutoUpdateStatus::Updated, cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Update the status to error including the error message
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_status(AutoUpdateStatus::error(e.to_string()), cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf, Error> {
|
||||||
|
let filename = match std::env::consts::OS {
|
||||||
|
"macos" => anyhow::Ok("Coop.dmg"),
|
||||||
|
"linux" => Ok("coop.tar.gz"),
|
||||||
|
"windows" => Ok("Coop.exe"),
|
||||||
|
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(installer_dir.path().join(filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install(
|
||||||
|
installer_dir: InstallerDir,
|
||||||
|
target_path: PathBuf,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
match std::env::consts::OS {
|
||||||
|
"macos" => install_release_macos(&installer_dir, target_path, cx).await,
|
||||||
|
"linux" => install_release_linux(&installer_dir, target_path, cx).await,
|
||||||
|
"windows" => install_release_windows(target_path).await,
|
||||||
|
unsupported_os => anyhow::bail!("Not supported: {unsupported_os}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn download(
|
||||||
|
url: &str,
|
||||||
|
target_path: &std::path::Path,
|
||||||
|
client: Arc<dyn HttpClient>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let body = AsyncBody::default();
|
||||||
|
let mut target_file = File::create(&target_path).await?;
|
||||||
|
let mut response = client.get(url, body, true).await?;
|
||||||
|
|
||||||
|
// Copy the response body to the target file
|
||||||
|
smol::io::copy(response.body_mut(), &mut target_file).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_release_macos(
|
||||||
|
temp_dir: &InstallerDir,
|
||||||
|
downloaded_dmg: PathBuf,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let running_app_path = cx.update(|cx| cx.app_path())?;
|
||||||
|
let running_app_filename = running_app_path
|
||||||
|
.file_name()
|
||||||
|
.with_context(|| format!("invalid running app path {running_app_path:?}"))?;
|
||||||
|
|
||||||
|
let mount_path = temp_dir.path().join("Coop");
|
||||||
|
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
|
||||||
|
|
||||||
|
mounted_app_path.push("/");
|
||||||
|
|
||||||
|
let output = Command::new("hdiutil")
|
||||||
|
.args(["attach", "-nobrowse"])
|
||||||
|
.arg(&downloaded_dmg)
|
||||||
|
.arg("-mountroot")
|
||||||
|
.arg(temp_dir.path())
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
anyhow::ensure!(
|
||||||
|
output.status.success(),
|
||||||
|
"failed to mount: {:?}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
|
||||||
|
let _unmounter = MacOsUnmounter {
|
||||||
|
mount_path: mount_path.clone(),
|
||||||
|
background_executor: cx.background_executor(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = Command::new("rsync")
|
||||||
|
.args(["-av", "--delete"])
|
||||||
|
.arg(&mounted_app_path)
|
||||||
|
.arg(&running_app_path)
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
anyhow::ensure!(
|
||||||
|
output.status.success(),
|
||||||
|
"failed to copy app: {:?}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_release_linux(
|
||||||
|
temp_dir: &InstallerDir,
|
||||||
|
downloaded_tar_gz: PathBuf,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let running_app_path = cx.update(|cx| cx.app_path())?;
|
||||||
|
|
||||||
|
// Extract the tar.gz file
|
||||||
|
let extracted = temp_dir.path().join("coop");
|
||||||
|
smol::fs::create_dir_all(&extracted)
|
||||||
|
.await
|
||||||
|
.context("failed to create directory to extract update")?;
|
||||||
|
|
||||||
|
let output = Command::new("tar")
|
||||||
|
.arg("-xzf")
|
||||||
|
.arg(&downloaded_tar_gz)
|
||||||
|
.arg("-C")
|
||||||
|
.arg(&extracted)
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
anyhow::ensure!(
|
||||||
|
output.status.success(),
|
||||||
|
"failed to extract {:?} to {:?}: {:?}",
|
||||||
|
downloaded_tar_gz,
|
||||||
|
extracted,
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the extracted app directory
|
||||||
|
let mut entries = smol::fs::read_dir(&extracted).await?;
|
||||||
|
let mut app_dir = None;
|
||||||
|
|
||||||
|
use smol::stream::StreamExt;
|
||||||
|
|
||||||
|
while let Some(entry) = entries.next().await {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
app_dir = Some(path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let from = app_dir.context("No app directory found in archive")?;
|
||||||
|
|
||||||
|
// Copy to the current installation directory
|
||||||
|
let output = Command::new("rsync")
|
||||||
|
.args(["-av", "--delete"])
|
||||||
|
.arg(&from)
|
||||||
|
.arg(
|
||||||
|
running_app_path
|
||||||
|
.parent()
|
||||||
|
.context("No parent directory for app")?,
|
||||||
|
)
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
anyhow::ensure!(
|
||||||
|
output.status.success(),
|
||||||
|
"failed to copy app from {:?} to {:?}: {:?}",
|
||||||
|
from,
|
||||||
|
running_app_path.parent(),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<(), Error> {
|
||||||
|
//const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
|
||||||
|
let system_root = std::env::var("SYSTEMROOT");
|
||||||
|
let powershell_path = system_root.as_ref().map_or_else(
|
||||||
|
|_| "powershell.exe".to_string(),
|
||||||
|
|p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut installer_path = std::ffi::OsString::new();
|
||||||
|
installer_path.push("\"");
|
||||||
|
installer_path.push(&downloaded_installer);
|
||||||
|
installer_path.push("\"");
|
||||||
|
|
||||||
|
let output = Command::new(powershell_path)
|
||||||
|
//.creation_flags(CREATE_NO_WINDOW)
|
||||||
|
.args(["-NoProfile", "-WindowStyle", "Hidden"])
|
||||||
|
.args(["Start-Process"])
|
||||||
|
.arg(installer_path)
|
||||||
|
.arg("-ArgumentList")
|
||||||
|
.args(["/P", "/R"])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
anyhow::ensure!(
|
||||||
|
output.status.success(),
|
||||||
|
"failed to start installer: {:?}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "browser-signer-proxy"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition.workspace = true
|
|
||||||
description = "Nostr browser signer (NIP-07) proxy using smol async runtime"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/nostrdevkit/nostr"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
atomic-destructor = "0.2"
|
|
||||||
event-listener = "5"
|
|
||||||
nostr.workspace = true
|
|
||||||
opaquerr = "0.1"
|
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
smol.workspace = true
|
|
||||||
tracing = { version = "0.1", features = ["std"] }
|
|
||||||
uuid = { version = "1.23", features = ["serde", "v4"] }
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# browser-signer-proxy
|
|
||||||
|
|
||||||
Proxy to use Nostr Browser signer ([NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md)) in native applications.
|
|
||||||
|
|
||||||
This is a re-implementation of [`nostr-browser-signer-proxy`](https://github.com/nostrdevkit/nostr/tree/master/signer/nostr-browser-signer-proxy)
|
|
||||||
using the [`smol`](https://github.com/smol-rs/smol) async runtime instead of tokio.
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
This crate provides a local HTTP proxy that communicates with a NIP-07 browser extension
|
|
||||||
(e.g., Alby, nos2x) running in a browser tab. Native applications can use this proxy to
|
|
||||||
request public keys, sign events, and perform NIP-04/NIP-44 encryption/decryption through
|
|
||||||
the browser extension.
|
|
||||||
|
|
||||||
The HTTP server is implemented with a minimal, dependency-free approach using `smol::net::TcpListener`
|
|
||||||
and manual HTTP/1.1 parsing — avoiding heavy HTTP framework dependencies entirely.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use browser_signer_proxy::prelude::*;
|
|
||||||
|
|
||||||
async fn example() -> Result<(), Error> {
|
|
||||||
// Create the proxy with default options (localhost:7400)
|
|
||||||
let proxy = BrowserSignerProxy::new(BrowserSignerProxyOptions::default());
|
|
||||||
|
|
||||||
// Open the proxy URL in a browser
|
|
||||||
webbrowser::open(&proxy.url())?;
|
|
||||||
|
|
||||||
// Start the proxy server
|
|
||||||
proxy.start().await?;
|
|
||||||
|
|
||||||
// Use it as an async Nostr signer
|
|
||||||
let public_key = proxy.get_public_key_async().await?;
|
|
||||||
println!("Connected with public key: {public_key}");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Differences from the tokio-based version
|
|
||||||
|
|
||||||
| Feature | tokio (original) | smol (this crate) |
|
|
||||||
|---|---|---|
|
|
||||||
| Async runtime | `tokio` | `smol` |
|
|
||||||
| HTTP server | `hyper` | `smol::net::TcpListener` + manual HTTP/1.1 |
|
|
||||||
| Mutex | `tokio::sync::Mutex` | `smol::lock::Mutex` |
|
|
||||||
| Shutdown signal | `tokio::sync::Notify` | `event_listener::Event` |
|
|
||||||
| Request-response channel | `tokio::sync::oneshot` | `smol::channel::bounded(1)` |
|
|
||||||
| Timeout | `tokio::time::timeout` | `smol::future::or` + `smol::Timer` |
|
|
||||||
| Task spawning | `tokio::spawn` | `smol::spawn` |
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is distributed under the MIT software license.
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Coop — Web Signer Proxy</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@800;900&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--brand: #F8FF37;
|
|
||||||
--ink: #111111;
|
|
||||||
--ink-soft: #333333;
|
|
||||||
--muted: #666666;
|
|
||||||
--paper: #FFFFFF;
|
|
||||||
--edge: rgba(17, 17, 17, 0.14);
|
|
||||||
--radius-sm: 1rem;
|
|
||||||
--radius-md: 1.5rem;
|
|
||||||
--radius-lg: 2.5rem;
|
|
||||||
--green: #2E8B57;
|
|
||||||
--red: #D32F2F;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
color: var(--ink);
|
|
||||||
background: var(--brand);
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--paper);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 2.5rem;
|
|
||||||
max-width: 440px;
|
|
||||||
width: 100%;
|
|
||||||
box-shadow: 0 8px 0 rgba(17, 17, 17, 0.12), 0 2px 20px rgba(17, 17, 17, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.6rem;
|
|
||||||
margin-bottom: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo__mark {
|
|
||||||
width: 2.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
background: var(--ink);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: "Nunito", system-ui, sans-serif;
|
|
||||||
font-weight: 900;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: var(--brand);
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo__text {
|
|
||||||
font-family: "Nunito", system-ui, sans-serif;
|
|
||||||
font-weight: 900;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading {
|
|
||||||
font-family: "Nunito", system-ui, sans-serif;
|
|
||||||
font-weight: 900;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
line-height: 1.15;
|
|
||||||
margin: 0 0 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: var(--ink-soft);
|
|
||||||
margin: 0 0 1.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
transition: background 300ms ease, color 300ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status--checking {
|
|
||||||
background: rgba(17, 17, 17, 0.05);
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status--connected {
|
|
||||||
background: rgba(46, 139, 87, 0.1);
|
|
||||||
color: var(--green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status--error {
|
|
||||||
background: rgba(211, 47, 47, 0.08);
|
|
||||||
color: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status__dot {
|
|
||||||
width: 0.7rem;
|
|
||||||
height: 0.7rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status--checking .status__dot {
|
|
||||||
background: var(--muted);
|
|
||||||
animation: pulse 1.2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status--connected .status__dot {
|
|
||||||
background: var(--green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status--error .status__dot {
|
|
||||||
background: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 0.4; transform: scale(0.85); }
|
|
||||||
50% { opacity: 1; transform: scale(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
padding-top: 1.25rem;
|
|
||||||
border-top: 1px solid var(--edge);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--muted);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint strong {
|
|
||||||
color: var(--ink-soft);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="card">
|
|
||||||
<h1 class="heading">Web Signer</h1>
|
|
||||||
|
|
||||||
<p class="subtitle">
|
|
||||||
This page connects the app to your Nostr Web Signer extension so you can sign in and use Coop securely.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div id="nip07-status" class="status status--checking">
|
|
||||||
<div class="status__dot"></div>
|
|
||||||
<span id="nip07-status-text">Checking extension…</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hint">
|
|
||||||
<strong>Keep this tab open</strong> while using the app — it automatically handles sign-in requests in the background.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="proxy.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
let isPolling = false;
|
|
||||||
|
|
||||||
async function pollForRequests() {
|
|
||||||
if (isPolling) return;
|
|
||||||
isPolling = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/pending');
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
console.log('Polled for requests, got:', data);
|
|
||||||
|
|
||||||
// Process any new requests
|
|
||||||
if (data.requests && data.requests.length > 0) {
|
|
||||||
console.log(`Processing ${data.requests.length} requests`);
|
|
||||||
for (const request of data.requests) {
|
|
||||||
await handleNip07Request(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Polling error:', error);
|
|
||||||
updateStatus('Error: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
isPolling = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleNip07Request(request) {
|
|
||||||
console.log('Handling request:', request);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let result;
|
|
||||||
|
|
||||||
if (!window.nostr) {
|
|
||||||
throw new Error('NIP-07 extension not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (request.method) {
|
|
||||||
case 'get_public_key':
|
|
||||||
console.log('Calling nostr.getPublicKey()');
|
|
||||||
result = await window.nostr.getPublicKey();
|
|
||||||
console.log('Got public key:', result);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'sign_event':
|
|
||||||
console.log('Calling nostr.signEvent() with:', request.params);
|
|
||||||
result = await window.nostr.signEvent(request.params);
|
|
||||||
console.log('Got signed event:', result);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'nip04_encrypt':
|
|
||||||
console.log('Calling nostr.nip04.encrypt()');
|
|
||||||
result = await window.nostr.nip04.encrypt(
|
|
||||||
request.params.public_key,
|
|
||||||
request.params.content
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'nip04_decrypt':
|
|
||||||
console.log('Calling nostr.nip04.decrypt()');
|
|
||||||
result = await window.nostr.nip04.decrypt(
|
|
||||||
request.params.public_key,
|
|
||||||
request.params.content
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'nip44_encrypt':
|
|
||||||
console.log('Calling nostr.nip44.encrypt()');
|
|
||||||
result = await window.nostr.nip44.encrypt(
|
|
||||||
request.params.public_key,
|
|
||||||
request.params.content
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'nip44_decrypt':
|
|
||||||
console.log('Calling nostr.nip44.decrypt()');
|
|
||||||
result = await window.nostr.nip44.decrypt(
|
|
||||||
request.params.public_key,
|
|
||||||
request.params.content
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown method: ${request.method}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send response back to server
|
|
||||||
const responsePayload = {
|
|
||||||
id: request.id,
|
|
||||||
result: result,
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Sending response:', responsePayload);
|
|
||||||
|
|
||||||
await fetch('/api/response', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(responsePayload)
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Response sent successfully');
|
|
||||||
updateStatus('Request processed successfully', 'connected');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error handling request:', error);
|
|
||||||
|
|
||||||
// Send error response back to server
|
|
||||||
const errorPayload = {
|
|
||||||
id: request.id,
|
|
||||||
result: null,
|
|
||||||
error: error.message
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Sending error response:', errorPayload);
|
|
||||||
|
|
||||||
await fetch('/api/response', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(errorPayload)
|
|
||||||
});
|
|
||||||
|
|
||||||
updateStatus('Error: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateStatus(message, state) {
|
|
||||||
const container = document.getElementById('nip07-status');
|
|
||||||
const textEl = document.getElementById('nip07-status-text');
|
|
||||||
if (container && textEl) {
|
|
||||||
container.className = 'status status--' + state;
|
|
||||||
textEl.textContent = message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start polling when page loads
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
console.log('NIP-07 Proxy loaded');
|
|
||||||
|
|
||||||
// Check if NIP-07 extension is available
|
|
||||||
if (window.nostr) {
|
|
||||||
console.log('NIP-07 extension detected');
|
|
||||||
updateStatus('Connected — ready', 'connected');
|
|
||||||
} else {
|
|
||||||
console.log('NIP-07 extension not found');
|
|
||||||
updateStatus('No NIP-07 extension found', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start polling every 500 ms
|
|
||||||
setInterval(pollForRequests, 500);
|
|
||||||
});
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// Copyright (c) 2022-2023 Yuki Kishimoto
|
|
||||||
// Copyright (c) 2023-2025 Rust Nostr Developers
|
|
||||||
// Distributed under the MIT software license
|
|
||||||
|
|
||||||
//! Error types for the browser signer proxy.
|
|
||||||
|
|
||||||
opaquerr::define_kind! {
|
|
||||||
/// Nostr browser signer proxy error kind.
|
|
||||||
pub ErrorKind {
|
|
||||||
/// Nostr protocol error.
|
|
||||||
Protocol => "nostr protocol error",
|
|
||||||
/// I/O error.
|
|
||||||
IO => "I/O error",
|
|
||||||
/// JSON error.
|
|
||||||
Json => "JSON error",
|
|
||||||
/// The operation timed out.
|
|
||||||
Timeout => "timeout",
|
|
||||||
/// The operation cannot be completed in the current state.
|
|
||||||
State => "invalid state",
|
|
||||||
/// Anything not covered by the stable categories above.
|
|
||||||
Other => "other error",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
opaquerr::define_error! {
|
|
||||||
/// Nostr browser signer proxy error.
|
|
||||||
pub Error(ErrorKind)
|
|
||||||
|
|
||||||
from {
|
|
||||||
nostr::error::Error => ErrorKind::Protocol,
|
|
||||||
std::io::Error => ErrorKind::IO,
|
|
||||||
serde_json::Error => ErrorKind::Json,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error {
|
|
||||||
pub(crate) fn generic<S>(message: S) -> Self
|
|
||||||
where
|
|
||||||
S: Into<String>,
|
|
||||||
{
|
|
||||||
Self::new(ErrorKind::Other, message.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn timeout() -> Self {
|
|
||||||
Self::simple(ErrorKind::Timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn shutdown() -> Self {
|
|
||||||
Self::with_static_message(ErrorKind::State, "server is shutdown")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,764 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::future::Future;
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
use atomic_destructor::{AtomicDestroyer, AtomicDestructor};
|
|
||||||
use event_listener::Event as ShutdownEvent;
|
|
||||||
use nostr::prelude::*;
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use serde::{Deserialize, Serialize, Serializer};
|
|
||||||
use serde_json::{Value, json};
|
|
||||||
use smol::channel;
|
|
||||||
use smol::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
|
|
||||||
use smol::lock::Mutex;
|
|
||||||
use smol::net::{TcpListener, TcpStream};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
mod error;
|
|
||||||
pub mod prelude;
|
|
||||||
|
|
||||||
pub use self::error::Error;
|
|
||||||
|
|
||||||
const DEFAULT_HTML: &str = include_str!("../index.html");
|
|
||||||
const JS: &str = include_str!("../proxy.js");
|
|
||||||
|
|
||||||
type PendingResponseMap = HashMap<Uuid, channel::Sender<Result<Value, String>>>;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct Message {
|
|
||||||
id: Uuid,
|
|
||||||
error: Option<String>,
|
|
||||||
result: Option<Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Message {
|
|
||||||
fn into_result(self) -> Result<Value, String> {
|
|
||||||
if let Some(error) = self.error {
|
|
||||||
Err(error)
|
|
||||||
} else {
|
|
||||||
Ok(self.result.unwrap_or(Value::Null))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
enum RequestMethod {
|
|
||||||
GetPublicKey,
|
|
||||||
SignEvent,
|
|
||||||
Nip04Encrypt,
|
|
||||||
Nip04Decrypt,
|
|
||||||
Nip44Encrypt,
|
|
||||||
Nip44Decrypt,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RequestMethod {
|
|
||||||
fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::GetPublicKey => "get_public_key",
|
|
||||||
Self::SignEvent => "sign_event",
|
|
||||||
Self::Nip04Encrypt => "nip04_encrypt",
|
|
||||||
Self::Nip04Decrypt => "nip04_decrypt",
|
|
||||||
Self::Nip44Encrypt => "nip44_encrypt",
|
|
||||||
Self::Nip44Decrypt => "nip44_decrypt",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for RequestMethod {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(self.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
struct RequestData {
|
|
||||||
id: Uuid,
|
|
||||||
method: RequestMethod,
|
|
||||||
params: Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RequestData {
|
|
||||||
#[inline]
|
|
||||||
fn new(method: RequestMethod, params: Value) -> Self {
|
|
||||||
Self {
|
|
||||||
id: Uuid::new_v4(),
|
|
||||||
method,
|
|
||||||
params,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Requests<'a> {
|
|
||||||
requests: &'a [RequestData],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Requests<'a> {
|
|
||||||
#[inline]
|
|
||||||
fn new(requests: &'a [RequestData]) -> Self {
|
|
||||||
Self { requests }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn len(&self) -> usize {
|
|
||||||
self.requests.len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Params for NIP-04 and NIP-44 encryption/decryption
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct CryptoParams<'a> {
|
|
||||||
public_key: &'a PublicKey,
|
|
||||||
content: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> CryptoParams<'a> {
|
|
||||||
#[inline]
|
|
||||||
fn new(public_key: &'a PublicKey, content: &'a str) -> Self {
|
|
||||||
Self {
|
|
||||||
public_key,
|
|
||||||
content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ProxyState {
|
|
||||||
/// Requests waiting to be picked up by browser
|
|
||||||
pub outgoing_requests: Mutex<Vec<RequestData>>,
|
|
||||||
/// Map of request ID to response sender
|
|
||||||
pub pending_responses: Mutex<PendingResponseMap>,
|
|
||||||
/// Last time the client asked for the pending requests
|
|
||||||
pub last_pending_request: Arc<AtomicU64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration options for [`BrowserSignerProxy`].
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct BrowserSignerProxyOptions {
|
|
||||||
/// Request timeout for the signer extension. Default is 30 seconds.
|
|
||||||
pub timeout: Duration,
|
|
||||||
/// Proxy server IP address and port. Default is `127.0.0.1:7400`.
|
|
||||||
pub addr: SocketAddr,
|
|
||||||
/// Custom HTML page.
|
|
||||||
// NOTE: not `Option` to move it between threads without reference counter
|
|
||||||
pub custom_html: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct InnerBrowserSignerProxy {
|
|
||||||
/// Configuration options for the proxy
|
|
||||||
options: BrowserSignerProxyOptions,
|
|
||||||
/// Internal state of the proxy including request queues
|
|
||||||
state: Arc<ProxyState>,
|
|
||||||
/// Notification trigger for graceful shutdown
|
|
||||||
shutdown: Arc<ShutdownEvent>,
|
|
||||||
/// Flag to indicate if the server is shutdown
|
|
||||||
is_shutdown: Arc<AtomicBool>,
|
|
||||||
/// Flag indicating if the server is started
|
|
||||||
is_started: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AtomicDestroyer for InnerBrowserSignerProxy {
|
|
||||||
fn on_destroy(&self) {
|
|
||||||
self.shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InnerBrowserSignerProxy {
|
|
||||||
#[inline]
|
|
||||||
fn is_shutdown(&self) -> bool {
|
|
||||||
self.is_shutdown.load(Ordering::SeqCst)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shutdown(&self) {
|
|
||||||
// Mark the server as shutdown
|
|
||||||
self.is_shutdown.store(true, Ordering::SeqCst);
|
|
||||||
|
|
||||||
// Notify all waiters that the proxy is shutting down
|
|
||||||
self.shutdown.notify(usize::MAX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Nostr Browser Signer Proxy
|
|
||||||
///
|
|
||||||
/// Proxy to use Nostr Browser signer (NIP-07) in native applications.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct BrowserSignerProxy {
|
|
||||||
inner: AtomicDestructor<InnerBrowserSignerProxy>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for BrowserSignerProxyOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
timeout: Duration::from_secs(30),
|
|
||||||
// 7 for NIP-07 and 400 because the NIP title is 40 bytes :)
|
|
||||||
addr: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 7400)),
|
|
||||||
custom_html: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BrowserSignerProxyOptions {
|
|
||||||
/// Sets the timeout duration.
|
|
||||||
pub const fn timeout(mut self, timeout: Duration) -> Self {
|
|
||||||
self.timeout = timeout;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the IP address.
|
|
||||||
pub const fn ip_addr(mut self, new_ip: IpAddr) -> Self {
|
|
||||||
self.addr = SocketAddr::new(new_ip, self.addr.port());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the port number.
|
|
||||||
pub const fn port(mut self, new_port: u16) -> Self {
|
|
||||||
self.addr = SocketAddr::new(self.addr.ip(), new_port);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a custom html page.
|
|
||||||
///
|
|
||||||
/// The page must include `/proxy.js` script (`<script src="/proxy.js"></script>`)
|
|
||||||
/// which will handle communication with the server and update the element
|
|
||||||
/// with id `nip07-proxy-status` with the status.
|
|
||||||
pub const fn custom_html_page(mut self, custom_html: &'static str) -> Self {
|
|
||||||
self.custom_html = custom_html;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BrowserSignerProxy {
|
|
||||||
/// Construct a new browser signer proxy
|
|
||||||
pub fn new(options: BrowserSignerProxyOptions) -> Self {
|
|
||||||
let state = ProxyState {
|
|
||||||
outgoing_requests: Mutex::new(Vec::new()),
|
|
||||||
pending_responses: Mutex::new(HashMap::new()),
|
|
||||||
last_pending_request: Arc::new(AtomicU64::new(0)),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
inner: AtomicDestructor::new(InnerBrowserSignerProxy {
|
|
||||||
options,
|
|
||||||
state: Arc::new(state),
|
|
||||||
shutdown: Arc::new(ShutdownEvent::new()),
|
|
||||||
is_shutdown: Arc::new(AtomicBool::new(false)),
|
|
||||||
is_started: Arc::new(AtomicBool::new(false)),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Indicates whether the server is currently running.
|
|
||||||
#[inline]
|
|
||||||
pub fn is_started(&self) -> bool {
|
|
||||||
self.inner.is_started.load(Ordering::SeqCst)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if there is an open browser tab ready to respond to requests by
|
|
||||||
/// verifying the time since the last pending request.
|
|
||||||
#[inline]
|
|
||||||
pub fn is_session_active(&self) -> bool {
|
|
||||||
current_time() - self.inner.state.last_pending_request.load(Ordering::SeqCst) < 2
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the signer proxy webpage URL
|
|
||||||
#[inline]
|
|
||||||
pub fn url(&self) -> String {
|
|
||||||
format!("http://{}", self.inner.options.addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start the proxy server.
|
|
||||||
///
|
|
||||||
/// If this is not called explicitly, the server will be automatically
|
|
||||||
/// started on the first interaction with the signer.
|
|
||||||
pub async fn start(&self) -> Result<(), Error> {
|
|
||||||
// Ensure is not shutdown
|
|
||||||
if self.inner.is_shutdown() {
|
|
||||||
return Err(Error::shutdown());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark the proxy as started and check if was already started
|
|
||||||
let is_started: bool = self.inner.is_started.swap(true, Ordering::SeqCst);
|
|
||||||
|
|
||||||
// Immediately return if already started
|
|
||||||
if is_started {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let listener: TcpListener = match TcpListener::bind(self.inner.options.addr).await {
|
|
||||||
Ok(listener) => listener,
|
|
||||||
Err(e) => {
|
|
||||||
// Undo the started flag if binding fails
|
|
||||||
self.inner.is_started.store(false, Ordering::SeqCst);
|
|
||||||
return Err(Error::from(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let addr: SocketAddr = self.inner.options.addr;
|
|
||||||
let state: Arc<ProxyState> = self.inner.state.clone();
|
|
||||||
let custom_html: &'static str = self.inner.options.custom_html;
|
|
||||||
let shutdown: Arc<ShutdownEvent> = self.inner.shutdown.clone();
|
|
||||||
|
|
||||||
smol::spawn(async move {
|
|
||||||
tracing::info!("Starting proxy server on {addr}");
|
|
||||||
|
|
||||||
loop {
|
|
||||||
// Race between accepting a new connection and shutdown signal
|
|
||||||
let shutdown_listener = shutdown.listen();
|
|
||||||
|
|
||||||
enum AcceptEvent {
|
|
||||||
Connection(Result<(TcpStream, SocketAddr), std::io::Error>),
|
|
||||||
Shutdown,
|
|
||||||
}
|
|
||||||
|
|
||||||
let event = smol::future::or(
|
|
||||||
async { AcceptEvent::Connection(listener.accept().await) },
|
|
||||||
async {
|
|
||||||
shutdown_listener.await;
|
|
||||||
AcceptEvent::Shutdown
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match event {
|
|
||||||
AcceptEvent::Connection(Ok((stream, _))) => {
|
|
||||||
let state: Arc<ProxyState> = state.clone();
|
|
||||||
let shutdown: Arc<ShutdownEvent> = shutdown.clone();
|
|
||||||
|
|
||||||
smol::spawn(async move {
|
|
||||||
let shutdown_listener = shutdown.listen();
|
|
||||||
|
|
||||||
smol::future::or(
|
|
||||||
async {
|
|
||||||
handle_connection(stream, state, custom_html).await;
|
|
||||||
},
|
|
||||||
async {
|
|
||||||
shutdown_listener.await;
|
|
||||||
tracing::debug!(
|
|
||||||
"Closing connection, proxy server is shutting down."
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
AcceptEvent::Connection(Err(e)) => {
|
|
||||||
tracing::error!("Failed to accept connection: {e}");
|
|
||||||
}
|
|
||||||
AcceptEvent::Shutdown => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!("Proxy server shut down.");
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn store_pending_response(&self, id: Uuid, tx: channel::Sender<Result<Value, String>>) {
|
|
||||||
let mut pending_responses = self.inner.state.pending_responses.lock().await;
|
|
||||||
pending_responses.insert(id, tx);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn store_outgoing_request(&self, request: RequestData) {
|
|
||||||
let mut outgoing_requests = self.inner.state.outgoing_requests.lock().await;
|
|
||||||
outgoing_requests.push(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn request<T>(&self, method: RequestMethod, params: Value) -> Result<T, Error>
|
|
||||||
where
|
|
||||||
T: DeserializeOwned,
|
|
||||||
{
|
|
||||||
// Start the proxy if not already started
|
|
||||||
self.start().await?;
|
|
||||||
|
|
||||||
// Construct the request
|
|
||||||
let request: RequestData = RequestData::new(method, params);
|
|
||||||
|
|
||||||
// Create a bounded channel of size 1 as a oneshot replacement
|
|
||||||
let (tx, rx) = channel::bounded::<Result<Value, String>>(1);
|
|
||||||
|
|
||||||
// Store the response sender
|
|
||||||
self.store_pending_response(request.id, tx).await;
|
|
||||||
|
|
||||||
// Add to outgoing requests queue
|
|
||||||
self.store_outgoing_request(request).await;
|
|
||||||
|
|
||||||
// Wait for response with timeout
|
|
||||||
let response = race_timeout(self.inner.options.timeout, rx.recv()).await;
|
|
||||||
|
|
||||||
match response {
|
|
||||||
Ok(Ok(res)) => Ok(serde_json::from_value(res)?),
|
|
||||||
Ok(Err(error)) => Err(Error::generic(error)),
|
|
||||||
Err(TimeoutError) => Err(Error::timeout()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn _get_public_key(&self) -> Result<PublicKey, Error> {
|
|
||||||
self.request(RequestMethod::GetPublicKey, json!({})).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn _sign_event(&self, event: UnsignedEvent) -> Result<Event, Error> {
|
|
||||||
let event: Event = self
|
|
||||||
.request(RequestMethod::SignEvent, serde_json::to_value(event)?)
|
|
||||||
.await?;
|
|
||||||
event.verify()?;
|
|
||||||
Ok(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn _nip04_encrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
|
|
||||||
let params = CryptoParams::new(public_key, content);
|
|
||||||
self.request(RequestMethod::Nip04Encrypt, serde_json::to_value(params)?)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn _nip04_decrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
|
|
||||||
let params = CryptoParams::new(public_key, content);
|
|
||||||
self.request(RequestMethod::Nip04Decrypt, serde_json::to_value(params)?)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn _nip44_encrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
|
|
||||||
let params = CryptoParams::new(public_key, content);
|
|
||||||
self.request(RequestMethod::Nip44Encrypt, serde_json::to_value(params)?)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn _nip44_decrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
|
|
||||||
let params = CryptoParams::new(public_key, content);
|
|
||||||
self.request(RequestMethod::Nip44Decrypt, serde_json::to_value(params)?)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncGetPublicKey for BrowserSignerProxy {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn get_public_key_async(
|
|
||||||
&self,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Result<PublicKey, Self::Error>> + Send + '_>> {
|
|
||||||
Box::pin(async move { self._get_public_key().await })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncSignEvent for BrowserSignerProxy {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn sign_event_async(
|
|
||||||
&self,
|
|
||||||
unsigned: UnsignedEvent,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Result<Event, Self::Error>> + Send + '_>> {
|
|
||||||
Box::pin(async move { self._sign_event(unsigned).await })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncNip04 for BrowserSignerProxy {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn nip04_encrypt_async<'a>(
|
|
||||||
&'a self,
|
|
||||||
public_key: &'a PublicKey,
|
|
||||||
content: &'a str,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
|
|
||||||
Box::pin(async move { self._nip04_encrypt(public_key, content).await })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nip04_decrypt_async<'a>(
|
|
||||||
&'a self,
|
|
||||||
public_key: &'a PublicKey,
|
|
||||||
encrypted_content: &'a str,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
|
|
||||||
Box::pin(async move { self._nip04_decrypt(public_key, encrypted_content).await })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncNip44 for BrowserSignerProxy {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn nip44_encrypt_async<'a>(
|
|
||||||
&'a self,
|
|
||||||
public_key: &'a PublicKey,
|
|
||||||
content: &'a str,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
|
|
||||||
Box::pin(async move { self._nip44_encrypt(public_key, content).await })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nip44_decrypt_async<'a>(
|
|
||||||
&'a self,
|
|
||||||
public_key: &'a PublicKey,
|
|
||||||
payload: &'a str,
|
|
||||||
) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
|
|
||||||
Box::pin(async move { self._nip44_decrypt(public_key, payload).await })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Minimal HTTP server ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Handle a single HTTP connection.
|
|
||||||
async fn handle_connection(stream: TcpStream, state: Arc<ProxyState>, custom_html: &'static str) {
|
|
||||||
let mut reader = BufReader::new(stream);
|
|
||||||
|
|
||||||
// Read the request line
|
|
||||||
let mut request_line = String::new();
|
|
||||||
if reader.read_line(&mut request_line).await.is_err() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let request_line = request_line.trim_end().to_string();
|
|
||||||
|
|
||||||
// Parse method, path, and HTTP version from request line
|
|
||||||
let parts: Vec<&str> = request_line.split_whitespace().collect();
|
|
||||||
if parts.len() < 2 {
|
|
||||||
send_response(&mut reader, 400, "Bad Request", "", "").await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let method = parts[0].to_uppercase();
|
|
||||||
let path = parts[1].to_string();
|
|
||||||
|
|
||||||
// Read headers until empty line
|
|
||||||
let mut headers = Vec::new();
|
|
||||||
let mut content_length: usize = 0;
|
|
||||||
loop {
|
|
||||||
let mut line = String::new();
|
|
||||||
if reader.read_line(&mut line).await.is_err() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let line = line.trim_end().to_string();
|
|
||||||
if line.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if let Some(value) = line.strip_prefix("content-length:") {
|
|
||||||
content_length = value.trim().parse().unwrap_or(0);
|
|
||||||
} else if let Some(value) = line.strip_prefix("Content-Length:") {
|
|
||||||
content_length = value.trim().parse().unwrap_or(0);
|
|
||||||
}
|
|
||||||
headers.push(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
match (method.as_str(), path.as_str()) {
|
|
||||||
// Serve the HTML proxy page
|
|
||||||
("GET", "/") => {
|
|
||||||
let html = if custom_html.is_empty() {
|
|
||||||
DEFAULT_HTML
|
|
||||||
} else {
|
|
||||||
custom_html
|
|
||||||
};
|
|
||||||
send_response(&mut reader, 200, "OK", "text/html", html).await;
|
|
||||||
}
|
|
||||||
// Serve the JS proxy script
|
|
||||||
("GET", "/proxy.js") => {
|
|
||||||
send_response(&mut reader, 200, "OK", "application/javascript", JS).await;
|
|
||||||
}
|
|
||||||
// Browser polls this endpoint to get pending requests
|
|
||||||
("GET", "/api/pending") => {
|
|
||||||
state
|
|
||||||
.last_pending_request
|
|
||||||
.store(current_time(), Ordering::SeqCst);
|
|
||||||
|
|
||||||
let mut outgoing = state.outgoing_requests.lock().await;
|
|
||||||
|
|
||||||
let requests = Requests::new(&outgoing);
|
|
||||||
let json = match serde_json::to_string(&requests) {
|
|
||||||
Ok(j) => j,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Failed to serialize pending requests: {e}");
|
|
||||||
send_response(&mut reader, 500, "Internal Server Error", "", "").await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::debug!("Sending {} pending requests to browser", requests.len());
|
|
||||||
|
|
||||||
// Clear the outgoing requests after sending them
|
|
||||||
outgoing.clear();
|
|
||||||
|
|
||||||
send_response_cors_json(&mut reader, 200, "OK", &json).await;
|
|
||||||
}
|
|
||||||
// Receive response from browser extension
|
|
||||||
("POST", "/api/response") => {
|
|
||||||
let mut body_bytes = vec![0u8; content_length];
|
|
||||||
if content_length > 0 && reader.read_exact(&mut body_bytes).await.is_err() {
|
|
||||||
send_response(&mut reader, 400, "Bad Request", "", "").await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let message: Message = match serde_json::from_slice(&body_bytes) {
|
|
||||||
Ok(json) => json,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Failed to parse response body: {e}");
|
|
||||||
send_response(&mut reader, 400, "Invalid JSON", "", "").await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::debug!("Received response from browser: {message:?}");
|
|
||||||
|
|
||||||
let id: Uuid = message.id;
|
|
||||||
let mut pending = state.pending_responses.lock().await;
|
|
||||||
|
|
||||||
match pending.remove(&id) {
|
|
||||||
Some(sender) => {
|
|
||||||
// Use try_send since we already hold the lock
|
|
||||||
let _ = sender.try_send(message.into_result());
|
|
||||||
tracing::info!("Forwarded response for request {id}");
|
|
||||||
}
|
|
||||||
None => tracing::warn!("No pending request found for {id}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
send_response_cors(&mut reader, 200, "OK", "text/plain", "OK").await;
|
|
||||||
}
|
|
||||||
// CORS preflight
|
|
||||||
("OPTIONS", _) => {
|
|
||||||
let response = "HTTP/1.1 200 OK\r\n\
|
|
||||||
Access-Control-Allow-Origin: *\r\n\
|
|
||||||
Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n\
|
|
||||||
Access-Control-Allow-Headers: Content-Type\r\n\
|
|
||||||
Content-Length: 0\r\n\
|
|
||||||
Connection: close\r\n\
|
|
||||||
\r\n";
|
|
||||||
let _ = reader.get_mut().write_all(response.as_bytes()).await;
|
|
||||||
let _ = reader.get_mut().flush().await;
|
|
||||||
}
|
|
||||||
// 404 - not found
|
|
||||||
_ => {
|
|
||||||
send_response(&mut reader, 404, "Not Found", "", "").await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write an HTTP response to the stream.
|
|
||||||
async fn send_response(
|
|
||||||
stream: &mut (impl AsyncWriteExt + Unpin),
|
|
||||||
status: u16,
|
|
||||||
status_text: &str,
|
|
||||||
content_type: &str,
|
|
||||||
body: &str,
|
|
||||||
) {
|
|
||||||
let mut response = format!("HTTP/1.1 {status} {status_text}\r\n");
|
|
||||||
|
|
||||||
if !content_type.is_empty() {
|
|
||||||
response.push_str(&format!("Content-Type: {content_type}\r\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
response.push_str(&format!("Content-Length: {}\r\n", body.len()));
|
|
||||||
response.push_str("Access-Control-Allow-Origin: *\r\n");
|
|
||||||
response.push_str("Connection: close\r\n");
|
|
||||||
response.push_str("\r\n");
|
|
||||||
response.push_str(body);
|
|
||||||
|
|
||||||
let _ = stream.write_all(response.as_bytes()).await;
|
|
||||||
let _ = stream.flush().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a response with CORS headers and JSON content type.
|
|
||||||
async fn send_response_cors_json(
|
|
||||||
stream: &mut (impl AsyncWriteExt + Unpin),
|
|
||||||
status: u16,
|
|
||||||
status_text: &str,
|
|
||||||
body: &str,
|
|
||||||
) {
|
|
||||||
let mut response = format!("HTTP/1.1 {status} {status_text}\r\n");
|
|
||||||
response.push_str("Content-Type: application/json\r\n");
|
|
||||||
response.push_str(&format!("Content-Length: {}\r\n", body.len()));
|
|
||||||
response.push_str("Access-Control-Allow-Origin: *\r\n");
|
|
||||||
response.push_str("Connection: close\r\n");
|
|
||||||
response.push_str("\r\n");
|
|
||||||
response.push_str(body);
|
|
||||||
|
|
||||||
let _ = stream.write_all(response.as_bytes()).await;
|
|
||||||
let _ = stream.flush().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a response with CORS headers.
|
|
||||||
async fn send_response_cors(
|
|
||||||
stream: &mut (impl AsyncWriteExt + Unpin),
|
|
||||||
status: u16,
|
|
||||||
status_text: &str,
|
|
||||||
content_type: &str,
|
|
||||||
body: &str,
|
|
||||||
) {
|
|
||||||
let mut response = format!("HTTP/1.1 {status} {status_text}\r\n");
|
|
||||||
|
|
||||||
if !content_type.is_empty() {
|
|
||||||
response.push_str(&format!("Content-Type: {content_type}\r\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
response.push_str(&format!("Content-Length: {}\r\n", body.len()));
|
|
||||||
response.push_str("Access-Control-Allow-Origin: *\r\n");
|
|
||||||
response.push_str("Connection: close\r\n");
|
|
||||||
response.push_str("\r\n");
|
|
||||||
response.push_str(body);
|
|
||||||
|
|
||||||
let _ = stream.write_all(response.as_bytes()).await;
|
|
||||||
let _ = stream.flush().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Timeout helper ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// An error indicating that an operation timed out.
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct TimeoutError;
|
|
||||||
|
|
||||||
/// Races a channel receive against a duration.
|
|
||||||
///
|
|
||||||
/// Returns the channel value on success, or [`TimeoutError`] if the duration
|
|
||||||
/// elapses first or the channel is closed.
|
|
||||||
async fn race_timeout<T>(
|
|
||||||
duration: Duration,
|
|
||||||
recv: impl Future<Output = Result<T, channel::RecvError>>,
|
|
||||||
) -> Result<T, TimeoutError> {
|
|
||||||
enum Event<T> {
|
|
||||||
Value(T),
|
|
||||||
ChannelClosed,
|
|
||||||
Timeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
let event = smol::future::or(
|
|
||||||
async {
|
|
||||||
match recv.await {
|
|
||||||
Ok(value) => Event::Value(value),
|
|
||||||
Err(_) => Event::ChannelClosed,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async {
|
|
||||||
smol::Timer::after(duration).await;
|
|
||||||
Event::Timeout
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match event {
|
|
||||||
Event::Value(value) => Ok(value),
|
|
||||||
Event::ChannelClosed | Event::Timeout => Err(TimeoutError),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Utility ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Gets the current time in seconds since the Unix epoch (1970-01-01). If the
|
|
||||||
/// time is before the epoch, returns 0.
|
|
||||||
#[inline]
|
|
||||||
fn current_time() -> u64 {
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map(|d| d.as_secs())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
// Copyright (c) 2022-2023 Yuki Kishimoto
|
|
||||||
// Copyright (c) 2023-2025 Rust Nostr Developers
|
|
||||||
// Distributed under the MIT software license
|
|
||||||
|
|
||||||
//! Prelude
|
|
||||||
|
|
||||||
#![allow(unknown_lints)]
|
|
||||||
#![allow(ambiguous_glob_reexports)]
|
|
||||||
#![doc(hidden)]
|
|
||||||
|
|
||||||
pub use nostr::prelude::*;
|
|
||||||
|
|
||||||
pub use crate::error::{Error, ErrorKind};
|
|
||||||
pub use crate::*;
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::{BTreeSet, HashMap, HashSet, hash_map};
|
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, LazyLock, RwLock};
|
use std::sync::{Arc, LazyLock, RwLock};
|
||||||
|
|
||||||
@@ -80,9 +80,6 @@ pub struct ChatRegistry {
|
|||||||
/// Chat rooms
|
/// Chat rooms
|
||||||
rooms: Vec<Entity<Room>>,
|
rooms: Vec<Entity<Room>>,
|
||||||
|
|
||||||
/// O(1) room lookup by room ID
|
|
||||||
room_index: HashMap<u64, Entity<Room>>,
|
|
||||||
|
|
||||||
/// Events that failed to unwrap for any reason
|
/// Events that failed to unwrap for any reason
|
||||||
trash: Entity<BTreeSet<FailedMessage>>,
|
trash: Entity<BTreeSet<FailedMessage>>,
|
||||||
|
|
||||||
@@ -173,7 +170,6 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
rooms: vec![],
|
rooms: vec![],
|
||||||
room_index: HashMap::new(),
|
|
||||||
trash: cx.new(|_| BTreeSet::default()),
|
trash: cx.new(|_| BTreeSet::default()),
|
||||||
seen: Arc::new(RwLock::new(HashMap::default())),
|
seen: Arc::new(RwLock::new(HashMap::default())),
|
||||||
event_map: Arc::new(RwLock::new(HashMap::default())),
|
event_map: Arc::new(RwLock::new(HashMap::default())),
|
||||||
@@ -440,9 +436,12 @@ impl ChatRegistry {
|
|||||||
self.tracking.load(Ordering::Acquire)
|
self.tracking.load(Ordering::Acquire)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a weak reference to a room by its ID
|
/// Get a weak reference to a room by its ID.
|
||||||
pub fn room(&self, id: &u64, _cx: &App) -> Option<WeakEntity<Room>> {
|
pub fn room(&self, id: &u64, cx: &App) -> Option<WeakEntity<Room>> {
|
||||||
self.room_index.get(id).map(|room| room.downgrade())
|
self.rooms
|
||||||
|
.iter()
|
||||||
|
.find(|this| &this.read(cx).id == id)
|
||||||
|
.map(|this| this.downgrade())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all rooms based on the filter.
|
/// Get all rooms based on the filter.
|
||||||
@@ -512,11 +511,7 @@ impl ChatRegistry {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let room: Room = room.into().organize(&public_key);
|
let room: Room = room.into().organize(&public_key);
|
||||||
let room_id = room.id;
|
self.rooms.insert(0, cx.new(|_| room));
|
||||||
let entity = cx.new(|_| room);
|
|
||||||
|
|
||||||
self.room_index.insert(room_id, entity.clone());
|
|
||||||
self.rooms.insert(0, entity);
|
|
||||||
|
|
||||||
cx.emit(ChatEvent::Ping);
|
cx.emit(ChatEvent::Ping);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -529,11 +524,9 @@ impl ChatRegistry {
|
|||||||
// Get the room's ID.
|
// Get the room's ID.
|
||||||
let id = room.read(cx).id;
|
let id = room.read(cx).id;
|
||||||
|
|
||||||
// If the room is new, add it to the registry and index.
|
// If the room is new, add it to the registry.
|
||||||
if let hash_map::Entry::Vacant(e) = self.room_index.entry(id) {
|
if !self.rooms.iter().any(|r| r.read(cx).id == id) {
|
||||||
let entity = room.to_owned();
|
self.rooms.insert(0, room.to_owned());
|
||||||
e.insert(entity.clone());
|
|
||||||
self.rooms.insert(0, entity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit the open room event deferred to avoid re-entrant reads
|
// Emit the open room event deferred to avoid re-entrant reads
|
||||||
@@ -544,23 +537,17 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
/// Close a room.
|
/// Close a room.
|
||||||
pub fn close_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn close_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if self.room_index.contains_key(&id) {
|
if self.rooms.iter().any(|r| r.read(cx).id == id) {
|
||||||
self.room_index.remove(&id);
|
|
||||||
self.rooms.retain(|r| r.read(cx).id != id);
|
|
||||||
cx.defer_in(window, move |_this, _window, cx| {
|
cx.defer_in(window, move |_this, _window, cx| {
|
||||||
cx.emit(ChatEvent::CloseRoom(id));
|
cx.emit(ChatEvent::CloseRoom(id));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sort rooms by their created at. Only notifies if order changed.
|
/// Sort rooms by their created at.
|
||||||
pub fn sort(&mut self, cx: &mut Context<Self>) {
|
pub fn sort(&mut self, cx: &mut Context<Self>) {
|
||||||
let before: Vec<_> = self.rooms.iter().map(|ev| ev.read(cx).id).collect();
|
|
||||||
self.rooms.sort_by_key(|ev| Reverse(ev.read(cx).created_at));
|
self.rooms.sort_by_key(|ev| Reverse(ev.read(cx).created_at));
|
||||||
let after: Vec<_> = self.rooms.iter().map(|ev| ev.read(cx).id).collect();
|
cx.notify();
|
||||||
if before != after {
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finding rooms based on a query.
|
/// Finding rooms based on a query.
|
||||||
@@ -587,7 +574,6 @@ impl ChatRegistry {
|
|||||||
/// Reset the registry.
|
/// Reset the registry.
|
||||||
pub fn reset(&mut self, cx: &mut Context<Self>) {
|
pub fn reset(&mut self, cx: &mut Context<Self>) {
|
||||||
self.rooms.clear();
|
self.rooms.clear();
|
||||||
self.room_index.clear();
|
|
||||||
self.trash.update(cx, |this, cx| {
|
self.trash.update(cx, |this, cx| {
|
||||||
this.clear();
|
this.clear();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -615,9 +601,7 @@ impl ChatRegistry {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let new_room_id = new_room.id;
|
let new_room_id = new_room.id;
|
||||||
let entity = cx.new(|_| new_room);
|
self.rooms.push(cx.new(|_| new_room));
|
||||||
self.room_index.insert(new_room_id, entity.clone());
|
|
||||||
self.rooms.push(entity);
|
|
||||||
|
|
||||||
let new_index = self.rooms.len();
|
let new_index = self.rooms.len();
|
||||||
room_map.insert(new_room_id, new_index);
|
room_map.insert(new_room_id, new_index);
|
||||||
@@ -627,7 +611,13 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
/// Load all rooms from the database.
|
/// Load all rooms from the database.
|
||||||
pub fn get_rooms(&mut self, cx: &mut Context<Self>) {
|
pub fn get_rooms(&mut self, cx: &mut Context<Self>) {
|
||||||
let task = self.get_rooms_task(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
|
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = self.get_rooms_from_database(public_key, cx);
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
match task.await {
|
match task.await {
|
||||||
@@ -649,53 +639,61 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a task to load rooms from the database
|
/// Create a task to load rooms from the database
|
||||||
fn get_rooms_task(&self, cx: &App) -> Task<Result<HashSet<Room>, Error>> {
|
fn get_rooms_from_database(
|
||||||
|
&self,
|
||||||
|
public_key: PublicKey,
|
||||||
|
cx: &App,
|
||||||
|
) -> Task<Result<HashSet<Room>, Error>> {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let public_key = signer.get_public_key_async().await?;
|
|
||||||
let contacts = client
|
let contacts = client
|
||||||
.database()
|
.database()
|
||||||
.contacts_public_keys(public_key)
|
.contacts_public_keys(public_key)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Query all cached rumor events (works with both old and new cache formats)
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::ApplicationSpecificData)
|
.kind(Kind::ApplicationSpecificData)
|
||||||
.custom_tag(SingleLetterTag::lowercase(Alphabet::K), "14");
|
.custom_tag(SingleLetterTag::lowercase(Alphabet::K), "14");
|
||||||
|
|
||||||
let events = client.database().query(filter).await?;
|
let events = client.database().query(filter).await?;
|
||||||
|
|
||||||
|
let mut rooms: HashSet<Room> = HashSet::new();
|
||||||
let mut grouped: HashMap<u64, Vec<UnsignedEvent>> = HashMap::new();
|
let mut grouped: HashMap<u64, Vec<UnsignedEvent>> = HashMap::new();
|
||||||
|
|
||||||
for raw in events.into_iter() {
|
for raw in events.into_iter() {
|
||||||
if let Ok(rumor) = UnsignedEvent::from_json(&raw.content)
|
if let Ok(rumor) = UnsignedEvent::from_json(&raw.content)
|
||||||
&& rumor.tags.public_keys().next().is_some()
|
&& rumor.tags.public_keys().peekable().peek().is_some()
|
||||||
{
|
{
|
||||||
if rumor.pubkey != public_key
|
|
||||||
&& !rumor.tags.public_keys().any(|k| k == public_key)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
grouped.entry(rumor.uniq_id()).or_default().push(rumor);
|
grouped.entry(rumor.uniq_id()).or_default().push(rumor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut rooms = HashSet::with_capacity(grouped.len());
|
for (_id, mut messages) in grouped.into_iter() {
|
||||||
|
messages.sort_by_key(|m| Reverse(m.created_at));
|
||||||
|
|
||||||
for (_id, messages) in grouped.into_iter() {
|
// Always use the latest message
|
||||||
let latest = messages.iter().max_by_key(|m| m.created_at).unwrap();
|
let Some(latest) = messages.first() else {
|
||||||
let room = Room::from(latest).organize(&public_key);
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Construct the room from the latest message.
|
||||||
|
//
|
||||||
|
// Call `.organize` to ensure the current user is at the end of the list.
|
||||||
|
let mut room = Room::from(latest).organize(&public_key);
|
||||||
|
|
||||||
|
// Check if the user has responded to the room
|
||||||
let user_sent = messages.iter().any(|m| m.pubkey == public_key);
|
let user_sent = messages.iter().any(|m| m.pubkey == public_key);
|
||||||
|
|
||||||
|
// Check if public keys are from the user's contacts
|
||||||
let is_contact = room.members.iter().any(|k| contacts.contains(k));
|
let is_contact = room.members.iter().any(|k| contacts.contains(k));
|
||||||
|
|
||||||
let room = if user_sent || is_contact {
|
// Set the room's kind based on status
|
||||||
room.kind(RoomKind::Ongoing)
|
if user_sent || is_contact {
|
||||||
} else {
|
room = room.kind(RoomKind::Ongoing);
|
||||||
room
|
}
|
||||||
};
|
|
||||||
|
|
||||||
rooms.insert(room);
|
rooms.insert(room);
|
||||||
}
|
}
|
||||||
@@ -715,7 +713,7 @@ impl ChatRegistry {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.room_index.get(&message.room).cloned() {
|
match self.rooms.iter().find(|e| e.read(cx).id == message.room) {
|
||||||
Some(room) => {
|
Some(room) => {
|
||||||
room.update(cx, |this, cx| {
|
room.update(cx, |this, cx| {
|
||||||
if this.kind == RoomKind::Request && message.rumor.pubkey == public_key {
|
if this.kind == RoomKind::Request && message.rumor.pubkey == public_key {
|
||||||
|
|||||||
@@ -271,8 +271,8 @@ impl Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the members of the room
|
/// Returns the members of the room
|
||||||
pub fn members(&self) -> &[PublicKey] {
|
pub fn members(&self) -> Vec<PublicKey> {
|
||||||
&self.members
|
self.members.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if the room has more than two members (group)
|
/// Checks if the room has more than two members (group)
|
||||||
@@ -356,7 +356,7 @@ impl Room {
|
|||||||
pub fn connect(&self, cx: &App) -> Task<Result<(), Error>> {
|
pub fn connect(&self, cx: &App) -> Task<Result<(), Error>> {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let members = self.members().to_vec();
|
let members = self.members();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let opts = SubscribeAutoCloseOptions::default()
|
let opts = SubscribeAutoCloseOptions::default()
|
||||||
|
|||||||
@@ -25,5 +25,4 @@ serde.workspace = true
|
|||||||
|
|
||||||
linkify = "0.10.0"
|
linkify = "0.10.0"
|
||||||
pulldown-cmark = "0.13.1"
|
pulldown-cmark = "0.13.1"
|
||||||
regex = "1"
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::sync::{Arc, LazyLock, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
pub use actions::*;
|
pub use actions::*;
|
||||||
use anyhow::{Context as AnyhowContext, Error};
|
use anyhow::{Context as AnyhowContext, Error};
|
||||||
@@ -11,14 +11,12 @@ use gpui::{
|
|||||||
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||||
Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton,
|
Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton,
|
||||||
ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, SharedUri,
|
ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, SharedUri,
|
||||||
StatefulInteractiveElement, Styled, StyledImage, Subscription, SystemNotification,
|
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window, div,
|
||||||
SystemNotificationAction, Task, WeakEntity, Window, div, img, list, px, red, relative, svg,
|
img, list, px, red, relative, svg, white,
|
||||||
white,
|
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{Person, PersonRegistry};
|
use person::{Person, PersonRegistry};
|
||||||
use regex::Regex;
|
|
||||||
use settings::{AppSettings, SignerKind};
|
use settings::{AppSettings, SignerKind};
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{NostrRegistry, upload};
|
use state::{NostrRegistry, upload};
|
||||||
@@ -40,11 +38,6 @@ use crate::text::RenderedText;
|
|||||||
const REACTION_EMOJIS: &[&str] = &["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"];
|
const REACTION_EMOJIS: &[&str] = &["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"];
|
||||||
const COMPACT_REACTION_EMOJIS: &[&str] = &["👍", "❤️", "👀"];
|
const COMPACT_REACTION_EMOJIS: &[&str] = &["👍", "❤️", "👀"];
|
||||||
|
|
||||||
/// Regex matching strings that consist entirely of emoji characters,
|
|
||||||
/// zero-width joiners, variation selectors, and keycap combiners.
|
|
||||||
static EMOJI_RE: LazyLock<Regex> =
|
|
||||||
LazyLock::new(|| Regex::new(r"^[\p{Emoji}\u{200D}\u{FE0F}\u{20E3}]+$").unwrap());
|
|
||||||
|
|
||||||
mod actions;
|
mod actions;
|
||||||
mod text;
|
mod text;
|
||||||
|
|
||||||
@@ -66,9 +59,6 @@ pub struct ChatPanel {
|
|||||||
/// All messages (sorted by created_at)
|
/// All messages (sorted by created_at)
|
||||||
messages: Vec<Message>,
|
messages: Vec<Message>,
|
||||||
|
|
||||||
/// O(1) message lookup by EventId
|
|
||||||
message_index: HashMap<EventId, usize>,
|
|
||||||
|
|
||||||
/// All reactions
|
/// All reactions
|
||||||
reactions: BTreeMap<EventId, Vec<(SharedString, PublicKey)>>,
|
reactions: BTreeMap<EventId, Vec<(SharedString, PublicKey)>>,
|
||||||
|
|
||||||
@@ -176,7 +166,6 @@ impl ChatPanel {
|
|||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
id,
|
id,
|
||||||
messages,
|
messages,
|
||||||
message_index: HashMap::new(),
|
|
||||||
reactions: BTreeMap::new(),
|
reactions: BTreeMap::new(),
|
||||||
room,
|
room,
|
||||||
list_state,
|
list_state,
|
||||||
@@ -242,29 +231,23 @@ impl ChatPanel {
|
|||||||
while let Ok(status) = rx.recv_async().await {
|
while let Ok(status) = rx.recv_async().await {
|
||||||
{
|
{
|
||||||
let mut map = reports.write().unwrap();
|
let mut map = reports.write().unwrap();
|
||||||
let status_id = match &*status {
|
for reports in map.values_mut() {
|
||||||
SendStatus::Ok { id, .. } => *id,
|
for report in reports.iter_mut() {
|
||||||
SendStatus::Failed { id, .. } => *id,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find the matching report and update it (exit early on first match)
|
|
||||||
'outer: for reports_list in map.values_mut() {
|
|
||||||
for report in reports_list.iter_mut() {
|
|
||||||
let Some(output) = report.output.as_mut() else {
|
let Some(output) = report.output.as_mut() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if *output.id() != status_id {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match &*status {
|
match &*status {
|
||||||
SendStatus::Ok { relay, .. } => {
|
SendStatus::Ok { id, relay } => {
|
||||||
output.success.insert(relay.clone(), EventSendStatus::Sent);
|
if output.id() == id {
|
||||||
|
output.success.insert(relay.clone(), EventSendStatus::Sent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SendStatus::Failed { relay, message, .. } => {
|
SendStatus::Failed { id, relay, message } => {
|
||||||
output.failed.insert(relay.clone(), message.clone());
|
if output.id() == id {
|
||||||
|
output.failed.insert(relay.clone(), message.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break 'outer;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,44 +259,30 @@ impl ChatPanel {
|
|||||||
|
|
||||||
/// Subscribe to room events
|
/// Subscribe to room events
|
||||||
fn subscribe_room_events(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn subscribe_room_events(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(room) = self.room.upgrade() else {
|
if let Some(room) = self.room.upgrade() {
|
||||||
return;
|
self.subscriptions.push(cx.subscribe_in(
|
||||||
};
|
&room,
|
||||||
|
window,
|
||||||
self.subscriptions.push(cx.subscribe_in(
|
move |this, _room, event, window, cx| {
|
||||||
&room,
|
match event {
|
||||||
window,
|
RoomEvent::Incoming(message) => {
|
||||||
move |this, _room, event, window, cx| {
|
if message.rumor.kind == Kind::Reaction {
|
||||||
match event {
|
this.insert_reaction(&message.rumor, cx);
|
||||||
RoomEvent::Incoming(message) => {
|
} else {
|
||||||
if message.rumor.kind == Kind::Reaction {
|
this.insert_message(message, false, cx);
|
||||||
this.insert_reaction(&message.rumor, cx);
|
|
||||||
} else {
|
|
||||||
this.insert_message(message, false, cx);
|
|
||||||
|
|
||||||
if !window.is_window_active() {
|
|
||||||
cx.show_system_notification(SystemNotification {
|
|
||||||
tag: "message".into(),
|
|
||||||
title: "New Message".into(),
|
|
||||||
body: "You have a new message.".into(),
|
|
||||||
actions: vec![SystemNotificationAction {
|
|
||||||
id: "open".into(),
|
|
||||||
label: "Open".into(),
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
RoomEvent::Reload => {
|
||||||
RoomEvent::Reload => {
|
// Defer to avoid re-entrant read on Room while
|
||||||
// Defer to avoid re-entrant read on Room while
|
// emit_refresh holds a write lock (via refresh_rooms).
|
||||||
// emit_refresh holds a write lock (via refresh_rooms).
|
cx.defer_in(window, |this, window, cx| {
|
||||||
cx.defer_in(window, |this, window, cx| {
|
this.get_messages(window, cx);
|
||||||
this.get_messages(window, cx);
|
});
|
||||||
});
|
}
|
||||||
}
|
};
|
||||||
};
|
},
|
||||||
},
|
));
|
||||||
));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load all messages belonging to this room
|
/// Load all messages belonging to this room
|
||||||
@@ -382,17 +351,6 @@ impl ChatPanel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If replying to exactly one message with only a valid emoji,
|
|
||||||
// send as a reaction instead of a text message
|
|
||||||
if replies.len() == 1 && EMOJI_RE.is_match(&content) && self.attachments.read(cx).is_empty()
|
|
||||||
{
|
|
||||||
for reply in &replies {
|
|
||||||
self.send_reaction(&content, reply, window, cx);
|
|
||||||
}
|
|
||||||
self.clear(window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.send_message(&content, replies, false, window, cx);
|
self.send_message(&content, replies, false, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,10 +472,6 @@ impl ChatPanel {
|
|||||||
|
|
||||||
if let Err(pos) = self.messages.binary_search(&msg) {
|
if let Err(pos) = self.messages.binary_search(&msg) {
|
||||||
self.messages.insert(pos, msg);
|
self.messages.insert(pos, msg);
|
||||||
// Rebuild message index after insertion (indices from pos to end shift)
|
|
||||||
for (i, message) in self.messages.iter().enumerate().skip(pos) {
|
|
||||||
self.message_index.insert(message.id, i);
|
|
||||||
}
|
|
||||||
self.list_state.splice(old_len..old_len, 1);
|
self.list_state.splice(old_len..old_len, 1);
|
||||||
|
|
||||||
if scroll {
|
if scroll {
|
||||||
@@ -560,25 +514,22 @@ impl ChatPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a message has any reports
|
/// Check if a message has any reports
|
||||||
fn has_reports(&self, id: &EventId) -> bool {
|
fn has_reports(&self, id: &EventId, _cx: &App) -> bool {
|
||||||
self.reports_by_id.read().unwrap().contains_key(id)
|
self.reports_by_id.read().unwrap().get(id).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clone reports for a message (used for modal display, not called during render)
|
fn sent_reports(&self, id: &EventId, _cx: &App) -> Option<Vec<SendReport>> {
|
||||||
fn sent_reports(&self, id: &EventId) -> Option<Vec<SendReport>> {
|
|
||||||
self.reports_by_id.read().unwrap().get(id).cloned()
|
self.reports_by_id.read().unwrap().get(id).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a message by its ID (O(1) lookup)
|
/// Get a message by its ID
|
||||||
fn message(&self, id: &EventId) -> Option<&Message> {
|
fn message(&self, id: &EventId) -> Option<&Message> {
|
||||||
self.message_index
|
self.messages.iter().find(|msg| &msg.id == id)
|
||||||
.get(id)
|
|
||||||
.and_then(|&ix| self.messages.get(ix))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reaction by its target ID (returns reference, no allocation)
|
/// Get a reaction by its target ID
|
||||||
fn reaction(&self, id: &EventId) -> &[(SharedString, PublicKey)] {
|
fn reaction(&self, id: &EventId) -> Vec<(SharedString, PublicKey)> {
|
||||||
self.reactions.get(id).map(|v| v.as_slice()).unwrap_or(&[])
|
self.reactions.get(id).cloned().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a message has any reactions
|
/// Check if a message has any reactions
|
||||||
@@ -961,7 +912,7 @@ impl ChatPanel {
|
|||||||
let replies = message.replies_to.as_slice();
|
let replies = message.replies_to.as_slice();
|
||||||
let has_replies = !replies.is_empty();
|
let has_replies = !replies.is_empty();
|
||||||
let has_reactions = self.has_reaction(&id);
|
let has_reactions = self.has_reaction(&id);
|
||||||
let has_reports = self.has_reports(&id);
|
let has_reports = self.has_reports(&id, cx);
|
||||||
|
|
||||||
// Hide avatar setting
|
// Hide avatar setting
|
||||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||||
@@ -1144,7 +1095,7 @@ impl ChatPanel {
|
|||||||
|
|
||||||
// Group reactions by emoji and collect authors for each
|
// Group reactions by emoji and collect authors for each
|
||||||
let mut grouped: BTreeMap<SharedString, Vec<PublicKey>> = BTreeMap::new();
|
let mut grouped: BTreeMap<SharedString, Vec<PublicKey>> = BTreeMap::new();
|
||||||
for (emoji, author) in reactions {
|
for (emoji, author) in &reactions {
|
||||||
grouped.entry(emoji.clone()).or_default().push(*author);
|
grouped.entry(emoji.clone()).or_default().push(*author);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1176,7 +1127,7 @@ impl ChatPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_sent_reports(&self, id: &EventId, cx: &App) -> impl IntoElement {
|
fn render_sent_reports(&self, id: &EventId, cx: &App) -> impl IntoElement {
|
||||||
let reports = self.sent_reports(id);
|
let reports = self.sent_reports(id, cx);
|
||||||
|
|
||||||
let pending = reports
|
let pending = reports
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|||||||
@@ -15,3 +15,4 @@ anyhow.workspace = true
|
|||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
use instant::Duration;
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
use common::EventExt;
|
use common::EventExt;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
|
use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
|
||||||
use instant::Duration;
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{Announcement, BOOTSTRAP_RELAYS, NostrRegistry, TIMEOUT};
|
use state::{Announcement, BOOTSTRAP_RELAYS, NostrRegistry, TIMEOUT};
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use gpui::SharedString;
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::Announcement;
|
use state::Announcement;
|
||||||
|
|
||||||
|
const IMAGE_RESIZER: &str = "https://wsrv.nl";
|
||||||
|
|
||||||
/// Person
|
/// Person
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Person {
|
pub struct Person {
|
||||||
@@ -109,7 +111,13 @@ impl Person {
|
|||||||
.picture
|
.picture
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|picture| !picture.is_empty())
|
.filter(|picture| !picture.is_empty())
|
||||||
.map(|picture| picture.into())
|
.map(|picture| {
|
||||||
|
let encoded_picture = urlencoding::encode(picture);
|
||||||
|
let url = format!(
|
||||||
|
"{IMAGE_RESIZER}/?url={encoded_picture}&w=100&h=100&fit=cover&mask=circle&n=-1"
|
||||||
|
);
|
||||||
|
url.into()
|
||||||
|
})
|
||||||
.unwrap_or_else(|| "brand/avatar.png".into())
|
.unwrap_or_else(|| "brand/avatar.png".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ mime_guess = "2.0.4"
|
|||||||
nostr-memory.workspace = true
|
nostr-memory.workspace = true
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
browser-signer-proxy = { path = "../browser-signer-proxy" }
|
|
||||||
nostr-lmdb.workspace = true
|
nostr-lmdb.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
gpui_tokio.workspace = true
|
gpui_tokio.workspace = true
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use browser_signer_proxy::prelude::*;
|
|
||||||
use common::config_dir;
|
use common::config_dir;
|
||||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, Window};
|
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, Window};
|
||||||
use gpui_tokio::Tokio;
|
|
||||||
use instant::Duration;
|
use instant::Duration;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use nostr_gossip_memory::prelude::*;
|
use nostr_gossip_memory::prelude::*;
|
||||||
@@ -27,7 +24,7 @@ pub use nip4e::*;
|
|||||||
pub use nip05::*;
|
pub use nip05::*;
|
||||||
pub use signer::{CoopAuthUrlHandler, UniversalSigner};
|
pub use signer::{CoopAuthUrlHandler, UniversalSigner};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App, cli_key: Option<SecretKey>) {
|
pub fn init(window: &mut Window, cx: &mut App) {
|
||||||
// rustls uses the `aws_lc_rs` provider by default
|
// rustls uses the `aws_lc_rs` provider by default
|
||||||
// This only errors if the default provider has already
|
// This only errors if the default provider has already
|
||||||
// been installed. We can ignore this `Result`.
|
// been installed. We can ignore this `Result`.
|
||||||
@@ -40,7 +37,7 @@ pub fn init(window: &mut Window, cx: &mut App, cli_key: Option<SecretKey>) {
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
gpui_tokio::init(cx);
|
gpui_tokio::init(cx);
|
||||||
|
|
||||||
NostrRegistry::set_global(cx.new(|cx| NostrRegistry::new(window, cx, cli_key)), cx);
|
NostrRegistry::set_global(cx.new(|cx| NostrRegistry::new(window, cx)), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GlobalNostrRegistry(Entity<NostrRegistry>);
|
struct GlobalNostrRegistry(Entity<NostrRegistry>);
|
||||||
@@ -61,16 +58,16 @@ pub enum StateEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StateEvent {
|
impl StateEvent {
|
||||||
pub fn signer_changed(&self) -> bool {
|
|
||||||
matches!(self, StateEvent::SignerChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error<T>(error: T) -> Self
|
pub fn error<T>(error: T) -> Self
|
||||||
where
|
where
|
||||||
T: Into<String>,
|
T: Into<String>,
|
||||||
{
|
{
|
||||||
Self::Error(error.into())
|
Self::Error(error.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn signer_changed(&self) -> bool {
|
||||||
|
matches!(self, StateEvent::SignerChanged)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nostr Registry
|
/// Nostr Registry
|
||||||
@@ -103,7 +100,7 @@ impl NostrRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new nostr instance
|
/// Create a new nostr instance
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>, cli_key: Option<SecretKey>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let signer = UniversalSigner::new(Keys::generate());
|
let signer = UniversalSigner::new(Keys::generate());
|
||||||
let authenticator = SignerAuthenticator::new(signer.clone());
|
let authenticator = SignerAuthenticator::new(signer.clone());
|
||||||
|
|
||||||
@@ -136,10 +133,6 @@ impl NostrRegistry {
|
|||||||
|
|
||||||
if cfg!(target_arch = "wasm32") {
|
if cfg!(target_arch = "wasm32") {
|
||||||
cx.emit(StateEvent::NoSigner);
|
cx.emit(StateEvent::NoSigner);
|
||||||
} else if let Some(secret) = cli_key {
|
|
||||||
// Use CLI-provided key -- same path as get_user_credential
|
|
||||||
let keys = Keys::new(secret);
|
|
||||||
this.set_signer(keys, cx);
|
|
||||||
} else {
|
} else {
|
||||||
this.get_user_credential(cx);
|
this.get_user_credential(cx);
|
||||||
}
|
}
|
||||||
@@ -265,11 +258,6 @@ impl NostrRegistry {
|
|||||||
this.set_signer(signer, cx);
|
this.set_signer(signer, cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
} else if content == "proxy" {
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.connect_proxy(cx);
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -315,81 +303,6 @@ impl NostrRegistry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start the browser proxy
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub fn connect_proxy(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let proxy = BrowserSignerProxy::new(BrowserSignerProxyOptions::default());
|
|
||||||
let (tx, rx) = flume::bounded::<String>(1);
|
|
||||||
|
|
||||||
self.tasks.push(Tokio::spawn_result(cx, {
|
|
||||||
let proxy = proxy.clone();
|
|
||||||
async move {
|
|
||||||
// Start the proxy and get the web url
|
|
||||||
proxy.start().await?;
|
|
||||||
// Notify GPUI
|
|
||||||
let url = proxy.url();
|
|
||||||
tx.send(url).ok();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
self.tasks.push(Tokio::spawn_result(cx, {
|
|
||||||
let proxy = proxy.clone();
|
|
||||||
async move {
|
|
||||||
loop {
|
|
||||||
if proxy.is_session_active() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
smol::Timer::after(Duration::from_secs(1)).await;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn({
|
|
||||||
let proxy = proxy.clone();
|
|
||||||
async move |this, cx| {
|
|
||||||
while let Ok(url) = rx.recv_async().await {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
let save = cx.write_credentials(USER_KEYRING, "proxy", b"proxy");
|
|
||||||
cx.background_spawn(async move { save.await.ok() }).detach();
|
|
||||||
cx.open_url(&url);
|
|
||||||
this.set_signer(proxy.clone(), cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Monitor the session, if the browser disconnects, notify user to reconnect
|
|
||||||
self.tasks.push(cx.spawn({
|
|
||||||
let proxy = proxy.clone();
|
|
||||||
let executor = cx.background_executor().clone();
|
|
||||||
async move |this, cx| {
|
|
||||||
// Wait for the signer to be confirmed (timeout is 30s)
|
|
||||||
executor.timer(Duration::from_secs(30)).await;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
executor.timer(Duration::from_secs(5)).await;
|
|
||||||
if !proxy.is_session_active() {
|
|
||||||
_ = this.update(cx, |this, cx| {
|
|
||||||
// Only notify if this proxy is still the active signer
|
|
||||||
if this.current_user.is_some() {
|
|
||||||
this.signer.swap_inner(Keys::generate());
|
|
||||||
this.current_user = None;
|
|
||||||
cx.emit(StateEvent::NoSigner);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the public key of a NIP-05 address
|
/// Get the public key of a NIP-05 address
|
||||||
pub fn query_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
|
pub fn query_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
|
||||||
let client = self.client();
|
let client = self.client();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement, Interactivity,
|
AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement, Interactivity,
|
||||||
IntoElement, ObjectFit, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage,
|
IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage, Window, div, img,
|
||||||
Window, div, img, px,
|
px,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
@@ -26,7 +26,9 @@ pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
|
|||||||
/// ```
|
/// ```
|
||||||
/// use ui::{Avatar};
|
/// use ui::{Avatar};
|
||||||
///
|
///
|
||||||
/// Avatar::new("path/to/image.png").grayscale(true).border_color(gpui::red());
|
/// Avatar::new("path/to/image.png")
|
||||||
|
/// .grayscale(true)
|
||||||
|
/// .border_color(gpui::red());
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Avatar {
|
pub struct Avatar {
|
||||||
@@ -128,7 +130,7 @@ impl RenderOnce for Avatar {
|
|||||||
self.image
|
self.image
|
||||||
.size(image_size)
|
.size(image_size)
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.object_fit(ObjectFit::Cover)
|
.object_fit(gpui::ObjectFit::Fill)
|
||||||
.bg(cx.theme().ghost_element_background)
|
.bg(cx.theme().ghost_element_background)
|
||||||
.with_fallback(move || {
|
.with_fallback(move || {
|
||||||
img("brand/avatar.png")
|
img("brand/avatar.png")
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ impl Render for DragPanel {
|
|||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.whitespace_nowrap()
|
.whitespace_nowrap()
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.text_sm()
|
.text_xs()
|
||||||
.text_color(cx.theme().text)
|
.text_color(cx.theme().text)
|
||||||
.text_ellipsis()
|
.text_ellipsis()
|
||||||
.when(cx.theme().shadow, |this| this.shadow_xs())
|
.when(cx.theme().shadow, |this| this.shadow_xs())
|
||||||
@@ -312,7 +312,6 @@ impl TabPanel {
|
|||||||
|
|
||||||
cx.emit(PanelEvent::ZoomOut);
|
cx.emit(PanelEvent::ZoomOut);
|
||||||
cx.emit(PanelEvent::LayoutChanged);
|
cx.emit(PanelEvent::LayoutChanged);
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detach_panel(
|
fn detach_panel(
|
||||||
@@ -322,22 +321,10 @@ impl TabPanel {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let panel_view = panel.view();
|
let panel_view = panel.view();
|
||||||
let removed_ix = self.panels.iter().position(|p| p.view() == panel_view);
|
|
||||||
self.panels.retain(|p| p.view() != panel_view);
|
self.panels.retain(|p| p.view() != panel_view);
|
||||||
|
|
||||||
if self.active_ix >= self.panels.len() {
|
if self.active_ix >= self.panels.len() {
|
||||||
self.set_active_ix(self.panels.len().saturating_sub(1), window, cx)
|
self.set_active_ix(self.panels.len().saturating_sub(1), window, cx)
|
||||||
} else if let Some(removed_ix) = removed_ix {
|
|
||||||
if removed_ix < self.active_ix {
|
|
||||||
self.active_ix = self.active_ix.saturating_sub(1);
|
|
||||||
} else if removed_ix == self.active_ix {
|
|
||||||
// The active panel was removed and another panel shifted into
|
|
||||||
// its position. Activate the new panel at the same index.
|
|
||||||
if let Some(new_active) = self.panels.get(self.active_ix) {
|
|
||||||
new_active.set_active(true, cx);
|
|
||||||
}
|
|
||||||
self.focus_active_panel(window, cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,7 +613,7 @@ impl TabPanel {
|
|||||||
div()
|
div()
|
||||||
.w_full()
|
.w_full()
|
||||||
.text_ellipsis()
|
.text_ellipsis()
|
||||||
.text_sm()
|
.text_xs()
|
||||||
.child(panel.title(cx)),
|
.child(panel.title(cx)),
|
||||||
)
|
)
|
||||||
.when(state.draggable, |this| {
|
.when(state.draggable, |this| {
|
||||||
@@ -700,8 +687,8 @@ impl TabPanel {
|
|||||||
.on_click(cx.listener({
|
.on_click(cx.listener({
|
||||||
let panel = panel.clone();
|
let panel = panel.clone();
|
||||||
move |view, _ev, window, cx| {
|
move |view, _ev, window, cx| {
|
||||||
cx.stop_propagation();
|
|
||||||
view.remove_panel(&panel, window, cx);
|
view.remove_panel(&panel, window, cx);
|
||||||
|
view.set_active_ix(ix, window, cx);
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use instant::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -6,7 +7,6 @@ use gpui::{
|
|||||||
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
|
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
|
||||||
RenderOnce, SharedString, StyleRefinement, Styled, Window, anchored, div, hsla, point, px,
|
RenderOnce, SharedString, StyleRefinement, Styled, Window, anchored, div, hsla, point, px,
|
||||||
};
|
};
|
||||||
use instant::Duration;
|
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::actions::{Cancel, Confirm};
|
use crate::actions::{Cancel, Confirm};
|
||||||
@@ -359,8 +359,8 @@ impl RenderOnce for Modal {
|
|||||||
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
||||||
let x = bounds.center().x - self.width / 2.;
|
let x = bounds.center().x - self.width / 2.;
|
||||||
|
|
||||||
let mut padding_right = px(16.);
|
let mut padding_right = px(8.);
|
||||||
let mut padding_left = px(16.);
|
let mut padding_left = px(8.);
|
||||||
|
|
||||||
if let Some(pl) = self.style.padding.left {
|
if let Some(pl) = self.style.padding.left {
|
||||||
padding_left = pl.to_pixels(self.width.into(), window.rem_size());
|
padding_left = pl.to_pixels(self.width.into(), window.rem_size());
|
||||||
@@ -452,8 +452,8 @@ impl RenderOnce for Modal {
|
|||||||
.when_some(self.max_width, |this, w| this.max_w(w))
|
.when_some(self.max_width, |this, w| this.max_w(w))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.px_4()
|
.px_2()
|
||||||
.h_8()
|
.h_4()
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ pub fn v_flex() -> Div {
|
|||||||
|
|
||||||
/// Returns a `Div` as divider.
|
/// Returns a `Div` as divider.
|
||||||
pub fn divider(cx: &App) -> Div {
|
pub fn divider(cx: &App) -> Div {
|
||||||
div().my_1().w_full().h_px().bg(cx.theme().border_variant)
|
div().my_2().w_full().h_px().bg(cx.theme().border_variant)
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! font_weight {
|
macro_rules! font_weight {
|
||||||
|
|||||||
@@ -183,10 +183,12 @@ impl RenderOnce for Tab {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.h(TABBAR_HEIGHT)
|
.h(TABBAR_HEIGHT)
|
||||||
.relative()
|
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.text_color(fg)
|
.text_color(fg)
|
||||||
.text_sm()
|
.text_sm()
|
||||||
|
.when(!self.selected && !self.disabled, |this| {
|
||||||
|
this.hover(|this| this.text_color(cx.theme().secondary_foreground))
|
||||||
|
})
|
||||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -220,21 +222,5 @@ impl RenderOnce for Tab {
|
|||||||
this.on_click(move |event, window, cx| on_click(event, window, cx))
|
this.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.absolute()
|
|
||||||
.bottom_0()
|
|
||||||
.left_0()
|
|
||||||
.right_0()
|
|
||||||
.h_0p5()
|
|
||||||
.when(self.selected && !self.disabled, |this| {
|
|
||||||
this.bg(cx.theme().element_active)
|
|
||||||
})
|
|
||||||
.when(!self.selected && !self.disabled, |this| {
|
|
||||||
this.invisible().group_hover("", |this| {
|
|
||||||
this.visible().bg(cx.theme().secondary_background)
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ chat = { path = "../chat" }
|
|||||||
chat_ui = { path = "../chat_ui" }
|
chat_ui = { path = "../chat_ui" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
person = { path = "../person" }
|
person = { path = "../person" }
|
||||||
auto_update = { path = "../auto_update" }
|
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
instant.workspace = true
|
instant.workspace = true
|
||||||
nostr-connect.workspace = true
|
nostr-connect.workspace = true
|
||||||
browser-signer-proxy = { path = "../browser-signer-proxy" }
|
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use state::{CoopAuthUrlHandler, NostrRegistry, USER_KEYRING};
|
|||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{Input, InputEvent, InputState};
|
use ui::input::{Input, InputEvent, InputState};
|
||||||
use ui::{Disableable, StyledExt, WindowExtension, divider, v_flex};
|
use ui::{Disableable, StyledExt, WindowExtension, v_flex};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ImportIdentity {
|
pub struct ImportIdentity {
|
||||||
@@ -164,14 +164,6 @@ impl ImportIdentity {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
fn proxy(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
nostr.update(cx, |this, cx| {
|
|
||||||
this.connect_proxy(cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
self.loading = status;
|
self.loading = status;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -207,13 +199,10 @@ impl ImportIdentity {
|
|||||||
|
|
||||||
impl Render for ImportIdentity {
|
impl Render for ImportIdentity {
|
||||||
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 {
|
||||||
const BUNKER_WARN: &str = "Nostr Connect will usually take more time to get all your messages. Please keep your session open until you see all your messages.";
|
const MSG: &str = "Coop won't store your identity key on the local device. You need to re-login again in the next session. You can use Nostr Connect for persistent login.";
|
||||||
const KEY_WARN: &str = "Coop won't store your identity key on the local device. You need to re-login again in the next session. You can use Nostr Connect for persistent login.";
|
|
||||||
|
|
||||||
let is_wasm = cfg!(target_arch = "wasm32");
|
|
||||||
let require_password = self.key_input.read(cx).value().starts_with("ncryptsec1");
|
let require_password = self.key_input.read(cx).value().starts_with("ncryptsec1");
|
||||||
let key_warning = self.key_input.read(cx).value().starts_with("nsec1") || require_password;
|
let key_warning = self.key_input.read(cx).value().starts_with("nsec1") || require_password;
|
||||||
let bunker_warning = self.key_input.read(cx).value().starts_with("bunker://");
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
@@ -238,20 +227,13 @@ impl Render for ImportIdentity {
|
|||||||
.child(Input::new(&self.pass_input)),
|
.child(Input::new(&self.pass_input)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(bunker_warning, |this| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_warning)
|
|
||||||
.child(div().child(BUNKER_WARN)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(key_warning, |this| {
|
.when(key_warning, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_warning)
|
.text_color(cx.theme().text_warning)
|
||||||
.child(div().child(KEY_WARN)),
|
.child(div().font_semibold().child("Warning"))
|
||||||
|
.child(div().child(MSG)),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -266,19 +248,6 @@ impl Render for ImportIdentity {
|
|||||||
this.login(window, cx);
|
this.login(window, cx);
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(divider(cx))
|
|
||||||
.when(!is_wasm, |this| {
|
|
||||||
this.child(
|
|
||||||
Button::new("proxy")
|
|
||||||
.label("Connect via Web Extension (Experimental)")
|
|
||||||
.ghost_alt()
|
|
||||||
.loading(self.loading)
|
|
||||||
.disabled(self.loading)
|
|
||||||
.on_click(cx.listener(move |this, _ev, _window, cx| {
|
|
||||||
this.proxy(cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use theme::ActiveTheme;
|
|||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
use ui::{Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<Screening> {
|
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<Screening> {
|
||||||
cx.new(|cx| Screening::new(public_key, window, cx))
|
cx.new(|cx| Screening::new(public_key, window, cx))
|
||||||
@@ -263,7 +263,7 @@ impl Screening {
|
|||||||
let contacts = contacts.clone();
|
let contacts = contacts.clone();
|
||||||
let total = contacts.len();
|
let total = contacts.len();
|
||||||
|
|
||||||
this.title("Mutual contacts").child(
|
this.title(SharedString::from("Mutual contacts")).child(
|
||||||
v_flex().gap_1().pb_2().child(
|
v_flex().gap_1().pb_2().child(
|
||||||
uniform_list("contacts", total, move |range, _window, cx| {
|
uniform_list("contacts", total, move |range, _window, cx| {
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
@@ -342,7 +342,7 @@ impl Render for Screening {
|
|||||||
.h_7()
|
.h_7()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.bg(cx.theme().elevated_surface_background)
|
.bg(cx.theme().surface_background)
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.truncate()
|
.truncate()
|
||||||
.text_ellipsis()
|
.text_ellipsis()
|
||||||
@@ -355,8 +355,7 @@ impl Render for Screening {
|
|||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
Button::new("njump")
|
Button::new("njump")
|
||||||
.icon(IconName::Link)
|
.label("View on njump.me")
|
||||||
.label("njump.me")
|
|
||||||
.secondary()
|
.secondary()
|
||||||
.small()
|
.small()
|
||||||
.rounded()
|
.rounded()
|
||||||
@@ -387,18 +386,21 @@ impl Render for Screening {
|
|||||||
.text_sm()
|
.text_sm()
|
||||||
.child(status_badge(Some(self.followed), cx))
|
.child(status_badge(Some(self.followed), cx))
|
||||||
.child(
|
.child(
|
||||||
v_flex().text_sm().child("Contact").child(
|
v_flex()
|
||||||
div()
|
.text_sm()
|
||||||
.line_clamp(1)
|
.child(SharedString::from("Contact"))
|
||||||
.text_color(cx.theme().text_muted)
|
.child(
|
||||||
.child({
|
div()
|
||||||
if self.followed {
|
.line_clamp(1)
|
||||||
SharedString::from(CONTACT)
|
.text_color(cx.theme().text_muted)
|
||||||
} else {
|
.child({
|
||||||
SharedString::from(NOT_CONTACT)
|
if self.followed {
|
||||||
}
|
SharedString::from(CONTACT)
|
||||||
}),
|
} else {
|
||||||
),
|
SharedString::from(NOT_CONTACT)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -413,7 +415,7 @@ impl Render for Screening {
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_0p5()
|
.gap_0p5()
|
||||||
.child("Activity on Public Relays")
|
.child(SharedString::from("Activity on Public Relays"))
|
||||||
.child(
|
.child(
|
||||||
Button::new("active")
|
Button::new("active")
|
||||||
.icon(IconName::Info)
|
.icon(IconName::Info)
|
||||||
@@ -482,8 +484,25 @@ impl Render for Screening {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.child(status_badge(Some(mutuals > 0), cx))
|
.child(status_badge(Some(mutuals > 0), cx))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
v_flex()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(SharedString::from("Mutual contacts"))
|
||||||
|
.child(
|
||||||
|
Button::new("mutuals")
|
||||||
|
.icon(IconName::Info)
|
||||||
|
.xsmall()
|
||||||
|
.ghost()
|
||||||
|
.rounded()
|
||||||
|
.on_click(cx.listener(
|
||||||
|
move |this, _, window, cx| {
|
||||||
|
this.mutual_contacts(window, cx);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.line_clamp(1)
|
.line_clamp(1)
|
||||||
@@ -495,17 +514,6 @@ impl Render for Screening {
|
|||||||
SharedString::from(NO_MUTUAL)
|
SharedString::from(NO_MUTUAL)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("mutuals")
|
|
||||||
.icon(IconName::Info)
|
|
||||||
.xsmall()
|
|
||||||
.ghost()
|
|
||||||
.rounded()
|
|
||||||
.disabled(mutuals == 0)
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.mutual_contacts(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use ::settings::AppSettings;
|
use ::settings::AppSettings;
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use auto_update::AutoUpdater;
|
|
||||||
use chat::{ChatEvent, ChatRegistry};
|
use chat::{ChatEvent, ChatRegistry};
|
||||||
use common::{CoopImageCache, download_dir};
|
use common::{CoopImageCache, download_dir};
|
||||||
use device::{DeviceEvent, DeviceRegistry};
|
use device::{DeviceEvent, DeviceRegistry};
|
||||||
@@ -45,12 +44,13 @@ struct MsgRelayNotification;
|
|||||||
#[action(namespace = workspace, no_json)]
|
#[action(namespace = workspace, no_json)]
|
||||||
enum Command {
|
enum Command {
|
||||||
ToggleTheme,
|
ToggleTheme,
|
||||||
Update,
|
|
||||||
RefreshMessagingRelays,
|
RefreshMessagingRelays,
|
||||||
BackupEncryption,
|
BackupEncryption,
|
||||||
ImportEncryption,
|
ImportEncryption,
|
||||||
RefreshEncryption,
|
RefreshEncryption,
|
||||||
ResetEncryption,
|
ResetEncryption,
|
||||||
|
|
||||||
ShowRelayList,
|
ShowRelayList,
|
||||||
ShowMessaging,
|
ShowMessaging,
|
||||||
ShowProfile,
|
ShowProfile,
|
||||||
@@ -379,14 +379,6 @@ impl Workspace {
|
|||||||
Command::ImportEncryption => {
|
Command::ImportEncryption => {
|
||||||
self.import_encryption(window, cx);
|
self.import_encryption(window, cx);
|
||||||
}
|
}
|
||||||
Command::Update => {
|
|
||||||
let auto_updater = AutoUpdater::global(cx);
|
|
||||||
auto_updater.update(cx, |this, cx| {
|
|
||||||
this.updater.update(cx, |updater, cx| {
|
|
||||||
updater.check(cx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,11 +586,6 @@ impl Workspace {
|
|||||||
Box::new(Command::ToggleTheme),
|
Box::new(Command::ToggleTheme),
|
||||||
)
|
)
|
||||||
.separator()
|
.separator()
|
||||||
.menu_with_icon(
|
|
||||||
"Check for Updates",
|
|
||||||
IconName::Device,
|
|
||||||
Box::new(Command::Update),
|
|
||||||
)
|
|
||||||
.menu_with_icon(
|
.menu_with_icon(
|
||||||
"Settings",
|
"Settings",
|
||||||
IconName::Settings,
|
IconName::Settings,
|
||||||
@@ -610,7 +597,6 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let updater = AutoUpdater::global(cx);
|
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let nip4e_enabled = AppSettings::get_nip4e(cx);
|
let nip4e_enabled = AppSettings::get_nip4e(cx);
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
@@ -622,15 +608,10 @@ impl Workspace {
|
|||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let profile = persons.read(cx).get(&public_key, cx);
|
let profile = persons.read(cx).get(&public_key, cx);
|
||||||
let announcement = profile.announcement();
|
let announcement = profile.announcement();
|
||||||
let updater_idle = updater.read(cx).idle(cx);
|
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.when(!cx.theme().platform.is_mac(), |this| this.pr_2())
|
.when(!cx.theme().platform.is_mac(), |this| this.pr_2())
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.when(!updater_idle, |this| {
|
|
||||||
let status = updater.read(cx).status(cx);
|
|
||||||
this.child(div().text_xs().italic().child(status))
|
|
||||||
})
|
|
||||||
.when(nip4e_enabled, |this| {
|
.when(nip4e_enabled, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Button::new("key")
|
Button::new("key")
|
||||||
|
|||||||
@@ -164,7 +164,9 @@ impl RenderOnce for RoomEntry {
|
|||||||
)
|
)
|
||||||
.on_cancel(move |_event, window, cx| {
|
.on_cancel(move |_event, window, cx| {
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
window.dispatch_action(Box::new(ClosePanel), cx);
|
||||||
true
|
// Prevent closing the modal on click
|
||||||
|
// modal will be automatically closed after closing panel
|
||||||
|
false
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -354,14 +354,6 @@ impl Sidebar {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
self.new_requests = false;
|
self.new_requests = false;
|
||||||
|
|
||||||
// Reset search state when switching to inbox/requests
|
|
||||||
self.reset(window, cx);
|
|
||||||
|
|
||||||
// Clear the find input value
|
|
||||||
self.find_input.update(cx, |this, cx| {
|
|
||||||
this.set_value("", window, cx);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_list_items(
|
fn render_list_items(
|
||||||
|
|||||||
@@ -48,4 +48,3 @@ reqwest_client.workspace = true
|
|||||||
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
nostr-sdk.workspace = true
|
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "coop"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
publish.workspace = true
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "coop"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[package.metadata.packager]
|
|
||||||
name = "Coop"
|
|
||||||
product-name = "Coop"
|
|
||||||
description = "Chat Freely, Stay Private on Nostr"
|
|
||||||
identifier = "su.reya.coop"
|
|
||||||
category = "SocialNetworking"
|
|
||||||
version = "1.0.0-beta5"
|
|
||||||
out-dir = "../dist"
|
|
||||||
before-packaging-command = "cargo build --release"
|
|
||||||
resources = ["Cargo.toml", "src"]
|
|
||||||
icons = [
|
|
||||||
"resources/32x32.png",
|
|
||||||
"resources/128x128.png",
|
|
||||||
"resources/128x128@2x.png",
|
|
||||||
"resources/icon.icns",
|
|
||||||
"resources/icon.ico",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
assets = { path = "../crates/assets" }
|
|
||||||
workspace = { path = "../crates/workspace" }
|
|
||||||
ui = { path = "../crates/ui" }
|
|
||||||
theme = { path = "../crates/theme" }
|
|
||||||
common = { path = "../crates/common" }
|
|
||||||
state = { path = "../crates/state" }
|
|
||||||
device = { path = "../crates/device" }
|
|
||||||
chat = { path = "../crates/chat" }
|
|
||||||
settings = { path = "../crates/settings" }
|
|
||||||
auto_update = { path = "../crates/auto_update" }
|
|
||||||
person = { path = "../crates/person" }
|
|
||||||
|
|
||||||
gpui.workspace = true
|
|
||||||
gpui_platform.workspace = true
|
|
||||||
gpui_linux.workspace = true
|
|
||||||
gpui_windows.workspace = true
|
|
||||||
gpui_macos.workspace = true
|
|
||||||
reqwest_client.workspace = true
|
|
||||||
|
|
||||||
log.workspace = true
|
|
||||||
tracing-subscriber.workspace = true
|
|
||||||
nostr-sdk.workspace = true
|
|
||||||
@@ -7,7 +7,6 @@ use gpui::{
|
|||||||
actions, point, px, size,
|
actions, point, px, size,
|
||||||
};
|
};
|
||||||
use gpui_platform::application;
|
use gpui_platform::application;
|
||||||
use nostr_sdk::prelude::SecretKey;
|
|
||||||
use state::{APP_ID, CLIENT_NAME};
|
use state::{APP_ID, CLIENT_NAME};
|
||||||
use ui::Root;
|
use ui::Root;
|
||||||
|
|
||||||
@@ -17,14 +16,6 @@ fn main() {
|
|||||||
// Initialize logging
|
// Initialize logging
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
// Parse CLI arguments for --sec <nsec1>
|
|
||||||
let cli_key = parse_cli_key();
|
|
||||||
if let Err(ref e) = cli_key {
|
|
||||||
eprintln!("Failed to parse --sec argument: {e}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
let cli_key = cli_key.unwrap();
|
|
||||||
|
|
||||||
// Run application
|
// Run application
|
||||||
application()
|
application()
|
||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
@@ -33,9 +24,6 @@ fn main() {
|
|||||||
// Load embedded fonts in assets/fonts
|
// Load embedded fonts in assets/fonts
|
||||||
load_embedded_fonts(cx);
|
load_embedded_fonts(cx);
|
||||||
|
|
||||||
// Set app identity
|
|
||||||
cx.set_app_identity(APP_ID, CLIENT_NAME);
|
|
||||||
|
|
||||||
// Register the `quit` function
|
// Register the `quit` function
|
||||||
cx.on_action(quit);
|
cx.on_action(quit);
|
||||||
|
|
||||||
@@ -84,7 +72,7 @@ fn main() {
|
|||||||
settings::init(window, cx);
|
settings::init(window, cx);
|
||||||
|
|
||||||
// Initialize the nostr client
|
// Initialize the nostr client
|
||||||
state::init(window, cx, cli_key);
|
state::init(window, cx);
|
||||||
|
|
||||||
// Initialize person registry
|
// Initialize person registry
|
||||||
person::init(window, cx);
|
person::init(window, cx);
|
||||||
@@ -134,25 +122,6 @@ fn load_embedded_fonts(cx: &App) {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_cli_key() -> Result<Option<SecretKey>, String> {
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
|
||||||
let mut i = 0;
|
|
||||||
while i < args.len() {
|
|
||||||
if args[i] == "--sec" {
|
|
||||||
if i + 1 < args.len() {
|
|
||||||
let nsec = &args[i + 1];
|
|
||||||
return SecretKey::parse(nsec)
|
|
||||||
.map(Some)
|
|
||||||
.map_err(|e| format!("Invalid nsec key '{nsec}': {e}"));
|
|
||||||
} else {
|
|
||||||
return Err("--sec requires a value (nsec1...)".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn quit(_ev: &Quit, cx: &mut App) {
|
fn quit(_ev: &Quit, cx: &mut App) {
|
||||||
log::info!("Gracefully quitting the application . . .");
|
log::info!("Gracefully quitting the application . . .");
|
||||||
cx.quit();
|
cx.quit();
|
||||||
|
|||||||
|
After Width: | Height: | Size: 769 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 374 KiB |
|
After Width: | Height: | Size: 535 KiB |
|
After Width: | Height: | Size: 386 KiB |
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 862 KiB |
|
After Width: | Height: | Size: 809 KiB |
|
After Width: | Height: | Size: 634 KiB |
|
After Width: | Height: | Size: 803 KiB |
|
After Width: | Height: | Size: 651 KiB |
|
After Width: | Height: | Size: 663 KiB |
|
After Width: | Height: | Size: 696 KiB |
|
After Width: | Height: | Size: 506 KiB |
@@ -59,7 +59,7 @@ pub fn run() -> Result<(), JsValue> {
|
|||||||
settings::init(window, cx);
|
settings::init(window, cx);
|
||||||
|
|
||||||
// Initialize the nostr client
|
// Initialize the nostr client
|
||||||
state::init(window, cx, None);
|
state::init(window, cx);
|
||||||
|
|
||||||
// Initialize person registry
|
// Initialize person registry
|
||||||
person::init(window, cx);
|
person::init(window, cx);
|
||||||
|
|||||||