feat: multi-account switcher #14
77
Cargo.lock
generated
77
Cargo.lock
generated
@@ -1198,7 +1198,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1644,7 +1644,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2599,7 +2599,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-channel 2.5.0",
|
"async-channel 2.5.0",
|
||||||
@@ -2678,7 +2678,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_linux"
|
name = "gpui_linux"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2726,7 +2726,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-task",
|
"async-task",
|
||||||
@@ -2768,7 +2768,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2779,7 +2779,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -2792,7 +2792,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -2803,7 +2803,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"log",
|
"log",
|
||||||
@@ -2812,7 +2812,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
@@ -2835,7 +2835,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@@ -2863,7 +2863,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collections",
|
"collections",
|
||||||
@@ -3107,7 +3107,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
@@ -3132,7 +3132,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -3668,12 +3668,13 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.12"
|
version = "0.1.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"libc",
|
"libc",
|
||||||
|
"plain",
|
||||||
"redox_syscall 0.7.3",
|
"redox_syscall 0.7.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3893,7 +3894,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen",
|
"bindgen",
|
||||||
@@ -4120,7 +4121,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.44.1"
|
version = "0.44.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
|
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -4144,7 +4145,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-blossom"
|
name = "nostr-blossom"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
|
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4155,7 +4156,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
|
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -4168,7 +4169,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
|
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"btreecap",
|
"btreecap",
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
@@ -4178,7 +4179,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip"
|
name = "nostr-gossip"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
|
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nostr",
|
"nostr",
|
||||||
]
|
]
|
||||||
@@ -4186,7 +4187,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
|
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"flume",
|
"flume",
|
||||||
@@ -4200,7 +4201,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.44.1"
|
version = "0.44.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#860e239ff894b8471b37c84dcac12b9572b8f064"
|
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -4637,7 +4638,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4770,6 +4771,12 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plain"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "png"
|
name = "png"
|
||||||
version = "0.17.16"
|
version = "0.17.16"
|
||||||
@@ -5318,7 +5325,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
]
|
]
|
||||||
@@ -5417,7 +5424,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5472,7 +5479,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rope"
|
name = "rope"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -5734,7 +5741,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "scheduler"
|
name = "scheduler"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-task",
|
"async-task",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
@@ -6328,7 +6335,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -7271,7 +7278,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "util"
|
name = "util"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
@@ -7310,7 +7317,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "util_macros"
|
name = "util_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"perf",
|
"perf",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -9113,7 +9120,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "zlog"
|
name = "zlog"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -9130,7 +9137,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ztracing"
|
name = "ztracing"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -9141,7 +9148,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#b06522e978b5ed24bcc2cf07a6de794179d69176"
|
source = "git+https://github.com/zed-industries/zed#4668aeb7284780cca830ba1173c4d3eb8bd11e2b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zune-core"
|
name = "zune-core"
|
||||||
|
|||||||
3
assets/icons/device.svg
Normal file
3
assets/icons/device.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M14.25 10.75C14.25 9.64543 15.1454 8.75 16.25 8.75H20.25C21.3546 8.75 22.25 9.64543 22.25 10.75V19.25C22.25 20.3546 21.3546 21.25 20.25 21.25H16.25C15.1454 21.25 14.25 20.3546 14.25 19.25V10.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M17.25 18.25H19.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M20.25 8.75V5.75C20.25 4.64543 19.3546 3.75 18.25 3.75H5.75C4.64543 3.75 3.75 4.64543 3.75 5.75V14.75C3.75 15.8546 2.85457 16.75 1.75 16.75V18.25C1.75 19.3546 2.64543 20.25 3.75 20.25H14.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.75 16.75H14.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 898 B |
3
assets/icons/group.svg
Normal file
3
assets/icons/group.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="8.75" r="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="4" cy="9.80005" r="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="20" cy="9.80005" r="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.25 16.625V16.5C7.25 13.8766 9.37665 11.75 12 11.75C14.6234 11.75 16.75 13.8766 16.75 16.5V16.625C16.75 17.5225 16.0225 18.25 15.125 18.25H8.875C7.97754 18.25 7.25 17.5225 7.25 16.625Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M4.25 17.2602H2.75C1.64543 17.2602 0.706551 16.3538 0.919944 15.2701C1.25877 13.5493 2.15049 12.3257 4 11.8301" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M19.75 17.2601H21.25C22.3546 17.2601 23.2935 16.3538 23.08 15.27C22.7412 13.5493 21.8495 12.3257 20 11.8301" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
3
assets/icons/scan.svg
Normal file
3
assets/icons/scan.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M7.25 4.75H4.75C3.64543 4.75 2.75 5.64543 2.75 6.75V9.25M16.75 4.75H19.25C20.3546 4.75 21.25 5.64543 21.25 6.75V9.25M21.25 14.75V17.25C21.25 18.3546 20.3546 19.25 19.25 19.25H16.75M7.25 19.25H4.75C3.64543 19.25 2.75 18.3546 2.75 17.25V14.75M7.75 9.75V14.25M16.25 9.75V14.25M12 9.75V12.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 468 B |
@@ -113,7 +113,7 @@ impl ChatRegistry {
|
|||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe the nip65 state and load chat rooms on every state change
|
// Observe the nip65 state and load chat rooms on every state change
|
||||||
cx.observe(&nostr, |this, state, cx| {
|
cx.observe(&nostr, |this, state, cx| {
|
||||||
match state.read(cx).relay_list_state() {
|
match state.read(cx).relay_list_state {
|
||||||
RelayState::Idle => {
|
RelayState::Idle => {
|
||||||
this.reset(cx);
|
this.reset(cx);
|
||||||
}
|
}
|
||||||
@@ -262,9 +262,12 @@ impl ChatRegistry {
|
|||||||
pub fn get_contact_list(&mut self, cx: &mut Context<Self>) {
|
pub fn get_contact_list(&mut self, cx: &mut Context<Self>) {
|
||||||
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();
|
let signer = nostr.read(cx).signer();
|
||||||
let public_key = signer.public_key().unwrap();
|
|
||||||
|
let Some(public_key) = signer.public_key() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
@@ -318,9 +321,12 @@ impl ChatRegistry {
|
|||||||
fn verify_relays(&mut self, cx: &mut Context<Self>) -> Task<Result<InboxState, Error>> {
|
fn verify_relays(&mut self, cx: &mut Context<Self>) -> Task<Result<InboxState, 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();
|
let signer = nostr.read(cx).signer();
|
||||||
let public_key = signer.public_key().unwrap();
|
|
||||||
|
let Some(public_key) = signer.public_key() else {
|
||||||
|
return Task::ready(Err(anyhow!("User not found")));
|
||||||
|
};
|
||||||
|
|
||||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
@@ -685,8 +691,7 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Trigger a refresh of the opened chat rooms by their IDs
|
/// Trigger a refresh of the opened chat rooms by their IDs
|
||||||
pub fn refresh_rooms(&mut self, ids: Option<Vec<u64>>, cx: &mut Context<Self>) {
|
pub fn refresh_rooms(&mut self, ids: &[u64], cx: &mut Context<Self>) {
|
||||||
if let Some(ids) = ids {
|
|
||||||
for room in self.rooms.iter() {
|
for room in self.rooms.iter() {
|
||||||
if ids.contains(&room.read(cx).id) {
|
if ids.contains(&room.read(cx).id) {
|
||||||
room.update(cx, |this, cx| {
|
room.update(cx, |this, cx| {
|
||||||
@@ -695,7 +700,6 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unwraps a gift-wrapped event and processes its contents.
|
/// Unwraps a gift-wrapped event and processes its contents.
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ impl Room {
|
|||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
let sender = signer.public_key().unwrap();
|
let sender = signer.public_key();
|
||||||
|
|
||||||
// Get room's id
|
// Get room's id
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
@@ -340,7 +340,7 @@ impl Room {
|
|||||||
let members: Vec<PublicKey> = self
|
let members: Vec<PublicKey> = self
|
||||||
.members
|
.members
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|public_key| public_key != &&sender)
|
.filter(|public_key| Some(**public_key) != sender)
|
||||||
.copied()
|
.copied()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
@@ -876,7 +876,7 @@ impl ChatPanel {
|
|||||||
window.open_modal(cx, move |this, _window, cx| {
|
window.open_modal(cx, move |this, _window, cx| {
|
||||||
this.show_close(true)
|
this.show_close(true)
|
||||||
.title(SharedString::from("Sent Reports"))
|
.title(SharedString::from("Sent Reports"))
|
||||||
.child(v_flex().pb_4().gap_4().children({
|
.child(v_flex().pb_2().gap_4().children({
|
||||||
let mut items = Vec::with_capacity(reports.len());
|
let mut items = Vec::with_capacity(reports.len());
|
||||||
|
|
||||||
for report in reports.iter() {
|
for report in reports.iter() {
|
||||||
|
|||||||
256
crates/coop/src/dialogs/accounts.rs
Normal file
256
crates/coop/src/dialogs/accounts.rs
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
use anyhow::Error;
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, px, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||||
|
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window,
|
||||||
|
};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use person::PersonRegistry;
|
||||||
|
use state::{NostrRegistry, SignerEvent};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::avatar::Avatar;
|
||||||
|
use ui::button::{Button, ButtonVariants};
|
||||||
|
use ui::indicator::Indicator;
|
||||||
|
use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, WindowExtension};
|
||||||
|
|
||||||
|
use crate::dialogs::connect::ConnectSigner;
|
||||||
|
use crate::dialogs::import::ImportKey;
|
||||||
|
|
||||||
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<AccountSelector> {
|
||||||
|
cx.new(|cx| AccountSelector::new(window, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Account selector
|
||||||
|
pub struct AccountSelector {
|
||||||
|
/// Public key currently being chosen for login
|
||||||
|
logging_in: Entity<Option<PublicKey>>,
|
||||||
|
|
||||||
|
/// The error message displayed when an error occurs.
|
||||||
|
error: Entity<Option<SharedString>>,
|
||||||
|
|
||||||
|
/// Async tasks
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
|
/// Subscription to the signer events
|
||||||
|
_subscription: Option<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountSelector {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let logging_in = cx.new(|_| None);
|
||||||
|
let error = cx.new(|_| None);
|
||||||
|
|
||||||
|
// Subscribe to the signer events
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| {
|
||||||
|
match event {
|
||||||
|
SignerEvent::Set => {
|
||||||
|
window.close_all_modals(cx);
|
||||||
|
window.refresh();
|
||||||
|
}
|
||||||
|
SignerEvent::Error(e) => {
|
||||||
|
this.set_error(e.to_string(), cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
logging_in,
|
||||||
|
error,
|
||||||
|
tasks: vec![],
|
||||||
|
_subscription: Some(subscription),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logging_in(&self, public_key: &PublicKey, cx: &App) -> bool {
|
||||||
|
self.logging_in.read(cx) == &Some(*public_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_logging_in(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
|
||||||
|
self.logging_in.update(cx, |this, cx| {
|
||||||
|
*this = Some(public_key);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<T>(&mut self, error: T, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
T: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.error.update(cx, |this, cx| {
|
||||||
|
*this = Some(error.into());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.logging_in.update(cx, |this, cx| {
|
||||||
|
*this = None;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let task = nostr.read(cx).get_signer(&public_key, cx);
|
||||||
|
|
||||||
|
// Mark the public key as being logged in
|
||||||
|
self.set_logging_in(public_key, cx);
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(signer) => {
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.set_signer(signer, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_error(e.to_string(), cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.remove_signer(&public_key, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_import(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let import = cx.new(|cx| ImportKey::new(window, cx));
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
|
this.width(px(460.))
|
||||||
|
.title("Import a Secret Key or Bunker Connection")
|
||||||
|
.show_close(true)
|
||||||
|
.pb_2()
|
||||||
|
.child(import.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let connect = cx.new(|cx| ConnectSigner::new(window, cx));
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
|
this.width(px(460.))
|
||||||
|
.title("Scan QR Code to Connect")
|
||||||
|
.show_close(true)
|
||||||
|
.pb_2()
|
||||||
|
.child(connect.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AccountSelector {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let persons = PersonRegistry::global(cx);
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let npubs = nostr.read(cx).npubs();
|
||||||
|
let loading = self.logging_in.read(cx).is_some();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.gap_2()
|
||||||
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.italic()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().danger_active)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.children({
|
||||||
|
let mut items = vec![];
|
||||||
|
|
||||||
|
for (ix, public_key) in npubs.read(cx).iter().enumerate() {
|
||||||
|
let profile = persons.read(cx).get(public_key, cx);
|
||||||
|
let logging_in = self.logging_in(public_key, cx);
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
h_flex()
|
||||||
|
.id(ix)
|
||||||
|
.group("")
|
||||||
|
.px_2()
|
||||||
|
.h_10()
|
||||||
|
.justify_between()
|
||||||
|
.w_full()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().ghost_element_background)
|
||||||
|
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(Avatar::new(profile.avatar()).small())
|
||||||
|
.child(div().text_sm().child(profile.name())),
|
||||||
|
)
|
||||||
|
.when(logging_in, |this| this.child(Indicator::new().small()))
|
||||||
|
.when(!logging_in, |this| {
|
||||||
|
this.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.invisible()
|
||||||
|
.group_hover("", |this| this.visible())
|
||||||
|
.child(
|
||||||
|
Button::new(format!("del-{ix}"))
|
||||||
|
.icon(IconName::Close)
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.disabled(logging_in)
|
||||||
|
.on_click(cx.listener({
|
||||||
|
let public_key = *public_key;
|
||||||
|
move |this, _ev, _window, cx| {
|
||||||
|
cx.stop_propagation();
|
||||||
|
this.remove(public_key, cx);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(!logging_in, |this| {
|
||||||
|
let public_key = *public_key;
|
||||||
|
this.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.login(public_key, window, cx);
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
})
|
||||||
|
.child(div().w_full().h_px().bg(cx.theme().border_variant))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.justify_end()
|
||||||
|
.w_full()
|
||||||
|
.child(
|
||||||
|
Button::new("input")
|
||||||
|
.icon(Icon::new(IconName::Usb))
|
||||||
|
.label("Import")
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.disabled(loading)
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.open_import(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("qr")
|
||||||
|
.icon(Icon::new(IconName::Scan))
|
||||||
|
.label("Scan QR to connect")
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.disabled(loading)
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.open_connect(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
115
crates/coop/src/dialogs/connect.rs
Normal file
115
crates/coop/src/dialogs/connect.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use common::TextUtils;
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, img, px, AppContext, Context, Entity, Image, IntoElement, ParentElement, Render,
|
||||||
|
SharedString, Styled, Subscription, Window,
|
||||||
|
};
|
||||||
|
use nostr_connect::prelude::*;
|
||||||
|
use state::{
|
||||||
|
CoopAuthUrlHandler, NostrRegistry, SignerEvent, CLIENT_NAME, NOSTR_CONNECT_RELAY,
|
||||||
|
NOSTR_CONNECT_TIMEOUT,
|
||||||
|
};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::v_flex;
|
||||||
|
|
||||||
|
pub struct ConnectSigner {
|
||||||
|
/// QR Code
|
||||||
|
qr_code: Option<Arc<Image>>,
|
||||||
|
|
||||||
|
/// Error message
|
||||||
|
error: Entity<Option<SharedString>>,
|
||||||
|
|
||||||
|
/// Subscription to the signer event
|
||||||
|
_subscription: Option<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectSigner {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let error = cx.new(|_| None);
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let app_keys = nostr.read(cx).app_keys.clone();
|
||||||
|
|
||||||
|
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||||
|
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||||
|
|
||||||
|
// Generate the nostr connect uri
|
||||||
|
let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME);
|
||||||
|
|
||||||
|
// Generate the nostr connect
|
||||||
|
let mut signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None).unwrap();
|
||||||
|
|
||||||
|
// Handle the auth request
|
||||||
|
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||||
|
|
||||||
|
// Generate a QR code for quick connection
|
||||||
|
let qr_code = uri.to_string().to_qr();
|
||||||
|
|
||||||
|
// Set signer in the background
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.add_nip46_signer(&signer, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to the signer event
|
||||||
|
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
||||||
|
if let SignerEvent::Error(e) = event {
|
||||||
|
this.set_error(e, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
qr_code,
|
||||||
|
error,
|
||||||
|
_subscription: Some(subscription),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
S: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.error.update(cx, |this, cx| {
|
||||||
|
*this = Some(message.into());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ConnectSigner {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
const MSG: &str = "Scan with any Nostr Connect-compatible app to connect";
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.p_4()
|
||||||
|
.when_some(self.qr_code.as_ref(), |this, qr| {
|
||||||
|
this.child(
|
||||||
|
img(qr.clone())
|
||||||
|
.size(px(256.))
|
||||||
|
.rounded(cx.theme().radius_lg)
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().border),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().danger_active)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from(MSG)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
301
crates/coop/src/dialogs/import.rs
Normal file
301
crates/coop/src/dialogs/import.rs
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Error};
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
|
Subscription, Task, Window,
|
||||||
|
};
|
||||||
|
use nostr_connect::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use state::{CoopAuthUrlHandler, NostrRegistry, SignerEvent};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::button::{Button, ButtonVariants};
|
||||||
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
|
use ui::{v_flex, Disableable};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ImportKey {
|
||||||
|
/// Secret key input
|
||||||
|
key_input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Password input (if required)
|
||||||
|
pass_input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Error message
|
||||||
|
error: Entity<Option<SharedString>>,
|
||||||
|
|
||||||
|
/// Countdown timer for nostr connect
|
||||||
|
countdown: Entity<Option<u64>>,
|
||||||
|
|
||||||
|
/// Whether the user is currently loading
|
||||||
|
loading: bool,
|
||||||
|
|
||||||
|
/// Async tasks
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
|
/// Event subscriptions
|
||||||
|
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportKey {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let key_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
|
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
|
let error = cx.new(|_| None);
|
||||||
|
let countdown = cx.new(|_| None);
|
||||||
|
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Subscribe to key input events and process login when the user presses enter
|
||||||
|
cx.subscribe_in(&key_input, window, |this, _input, event, window, cx| {
|
||||||
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
|
this.login(window, cx);
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Subscribe to the nostr signer event
|
||||||
|
cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
||||||
|
if let SignerEvent::Error(e) = event {
|
||||||
|
this.set_error(e, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
key_input,
|
||||||
|
pass_input,
|
||||||
|
error,
|
||||||
|
countdown,
|
||||||
|
loading: false,
|
||||||
|
tasks: vec![],
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.loading {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Prevent duplicate login requests
|
||||||
|
self.set_loading(true, cx);
|
||||||
|
|
||||||
|
let value = self.key_input.read(cx).value();
|
||||||
|
let password = self.pass_input.read(cx).value();
|
||||||
|
|
||||||
|
if value.starts_with("bunker://") {
|
||||||
|
self.bunker(&value, window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.starts_with("ncryptsec1") {
|
||||||
|
self.ncryptsec(value, password, window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(secret) = SecretKey::parse(&value) {
|
||||||
|
let keys = Keys::new(secret);
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
|
// Update the signer
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.add_key_signer(&keys, cx);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.set_error("Invalid key", cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Ok(uri) = NostrConnectUri::parse(content) else {
|
||||||
|
self.set_error("Bunker is not valid", cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let app_keys = nostr.read(cx).app_keys.clone();
|
||||||
|
let timeout = Duration::from_secs(30);
|
||||||
|
|
||||||
|
// Construct the nostr connect signer
|
||||||
|
let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap();
|
||||||
|
|
||||||
|
// Handle auth url with the default browser
|
||||||
|
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||||
|
|
||||||
|
// Set signer in the background
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.add_nip46_signer(&signer, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start countdown
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
for i in (0..=30).rev() {
|
||||||
|
if i == 0 {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_countdown(None, cx);
|
||||||
|
})?;
|
||||||
|
} else {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_countdown(Some(i), cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
cx.background_executor().timer(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ncryptsec<S>(&mut self, content: S, pwd: S, window: &mut Window, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let content: String = content.into();
|
||||||
|
let password: String = pwd.into();
|
||||||
|
|
||||||
|
if password.is_empty() {
|
||||||
|
self.set_error("Password is required", cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(enc) = EncryptedSecretKey::from_bech32(&content) else {
|
||||||
|
self.set_error("Secret Key is invalid", cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decrypt in the background to ensure it doesn't block the UI
|
||||||
|
let task = cx.background_spawn(async move {
|
||||||
|
if let Ok(content) = enc.decrypt(&password) {
|
||||||
|
Ok(Keys::new(content))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Invalid password"))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(keys) => {
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.add_key_signer(&keys, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_error(e.to_string(), cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
S: Into<SharedString>,
|
||||||
|
{
|
||||||
|
// Reset the log in state
|
||||||
|
self.set_loading(false, cx);
|
||||||
|
|
||||||
|
// Reset the countdown
|
||||||
|
self.set_countdown(None, cx);
|
||||||
|
|
||||||
|
// Update error message
|
||||||
|
self.error.update(cx, |this, cx| {
|
||||||
|
*this = Some(message.into());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the error message after 3 secs
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.error.update(cx, |this, cx| {
|
||||||
|
*this = None;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
|
self.loading = status;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_countdown(&mut self, i: Option<u64>, cx: &mut Context<Self>) {
|
||||||
|
self.countdown.update(cx, |this, cx| {
|
||||||
|
*this = i;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ImportKey {
|
||||||
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.p_4()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child("nsec or bunker://")
|
||||||
|
.child(TextInput::new(&self.key_input)),
|
||||||
|
)
|
||||||
|
.when(
|
||||||
|
self.key_input.read(cx).value().starts_with("ncryptsec1"),
|
||||||
|
|this| {
|
||||||
|
this.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child("Password:")
|
||||||
|
.child(TextInput::new(&self.pass_input)),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("login")
|
||||||
|
.label("Continue")
|
||||||
|
.primary()
|
||||||
|
.loading(self.loading)
|
||||||
|
.disabled(self.loading)
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.login(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"Approve connection request from your signer in {} seconds",
|
||||||
|
i
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().danger_active)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
|
pub mod accounts;
|
||||||
pub mod screening;
|
pub mod screening;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|
||||||
|
mod connect;
|
||||||
|
mod import;
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ impl Screening {
|
|||||||
let total = contacts.len();
|
let total = contacts.len();
|
||||||
|
|
||||||
this.title(SharedString::from("Mutual contacts")).child(
|
this.title(SharedString::from("Mutual contacts")).child(
|
||||||
v_flex().gap_1().pb_4().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);
|
||||||
let mut items = Vec::with_capacity(total);
|
let mut items = Vec::with_capacity(total);
|
||||||
@@ -356,9 +356,9 @@ impl Render for Screening {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("report")
|
Button::new("report")
|
||||||
.tooltip("Report as a scam or impostor")
|
.tooltip("Report as a scam or impostor")
|
||||||
.icon(IconName::Boom)
|
.icon(IconName::Warning)
|
||||||
.small()
|
.small()
|
||||||
.danger()
|
.warning()
|
||||||
.rounded()
|
.rounded()
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||||
this.report(window, cx);
|
this.report(window, cx);
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use common::TextUtils;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
div, img, px, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
|
||||||
FocusHandle, Focusable, Image, IntoElement, ParentElement, Render, SharedString, Styled, Task,
|
|
||||||
Window,
|
|
||||||
};
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
|
||||||
use state::NostrRegistry;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
|
||||||
use ui::dock_area::ClosePanel;
|
|
||||||
use ui::notification::Notification;
|
|
||||||
use ui::{v_flex, StyledExt, WindowExtension};
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ConnectPanel> {
|
|
||||||
cx.new(|cx| ConnectPanel::new(window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ConnectPanel {
|
|
||||||
name: SharedString,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
|
|
||||||
/// QR Code
|
|
||||||
qr_code: Option<Arc<Image>>,
|
|
||||||
|
|
||||||
/// Background tasks
|
|
||||||
_tasks: SmallVec<[Task<()>; 1]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnectPanel {
|
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let weak_state = nostr.downgrade();
|
|
||||||
let (signer, uri) = nostr.read(cx).client_connect(None);
|
|
||||||
|
|
||||||
// Generate a QR code for quick connection
|
|
||||||
let qr_code = uri.to_string().to_qr();
|
|
||||||
|
|
||||||
let mut tasks = smallvec![];
|
|
||||||
|
|
||||||
tasks.push(
|
|
||||||
// Wait for nostr connect
|
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
|
||||||
let result = signer.bunker_uri().await;
|
|
||||||
|
|
||||||
weak_state
|
|
||||||
.update_in(cx, |this, window, cx| {
|
|
||||||
match result {
|
|
||||||
Ok(uri) => {
|
|
||||||
this.persist_bunker(uri, cx);
|
|
||||||
this.set_signer(signer, true, cx);
|
|
||||||
// Close the current panel after setting the signer
|
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
name: "Nostr Connect".into(),
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
qr_code,
|
|
||||||
_tasks: tasks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Panel for ConnectPanel {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
self.name.clone().into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for ConnectPanel {}
|
|
||||||
|
|
||||||
impl Focusable for ConnectPanel {
|
|
||||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ConnectPanel {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.p_2()
|
|
||||||
.gap_10()
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.justify_center()
|
|
||||||
.items_center()
|
|
||||||
.text_center()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.font_semibold()
|
|
||||||
.line_height(relative(1.25))
|
|
||||||
.child(SharedString::from("Continue with Nostr Connect")),
|
|
||||||
)
|
|
||||||
.child(div().text_sm().text_color(cx.theme().text_muted).child(
|
|
||||||
SharedString::from("Use Nostr Connect apps to scan the code"),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.when_some(self.qr_code.as_ref(), |this, qr| {
|
|
||||||
this.child(
|
|
||||||
img(qr.clone())
|
|
||||||
.size(px(256.))
|
|
||||||
.rounded(cx.theme().radius_lg)
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().border),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ use ui::dock_area::dock::DockPlacement;
|
|||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt};
|
use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt};
|
||||||
|
|
||||||
use crate::panels::{connect, import, messaging_relays, profile, relay_list};
|
use crate::panels::{messaging_relays, profile, relay_list};
|
||||||
use crate::workspace::Workspace;
|
use crate::workspace::Workspace;
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
||||||
@@ -86,10 +86,7 @@ impl Render for GreeterPanel {
|
|||||||
let nip17 = chat.read(cx).state(cx);
|
let nip17 = chat.read(cx).state(cx);
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let nip65 = nostr.read(cx).relay_list_state();
|
let nip65 = nostr.read(cx).relay_list_state.clone();
|
||||||
|
|
||||||
let signer = nostr.read(cx).signer();
|
|
||||||
let owned = signer.owned();
|
|
||||||
|
|
||||||
let required_actions =
|
let required_actions =
|
||||||
nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable;
|
nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable;
|
||||||
@@ -191,60 +188,6 @@ impl Render for GreeterPanel {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(!owned, |this| {
|
|
||||||
this.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.w_full()
|
|
||||||
.text_xs()
|
|
||||||
.font_semibold()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from("Use your own identity"))
|
|
||||||
.child(div().flex_1().h_px().bg(cx.theme().border)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
Button::new("connect")
|
|
||||||
.icon(Icon::new(IconName::Door))
|
|
||||||
.label("Connect account via Nostr Connect")
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.justify_start()
|
|
||||||
.on_click(move |_ev, window, cx| {
|
|
||||||
Workspace::add_panel(
|
|
||||||
connect::init(window, cx),
|
|
||||||
DockPlacement::Center,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("import")
|
|
||||||
.icon(Icon::new(IconName::Usb))
|
|
||||||
.label("Import a secret key or bunker")
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.justify_start()
|
|
||||||
.on_click(move |_ev, window, cx| {
|
|
||||||
Workspace::add_panel(
|
|
||||||
import::init(window, cx),
|
|
||||||
DockPlacement::Center,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
|
|||||||
@@ -1,371 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
|
||||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
|
||||||
};
|
|
||||||
use nostr_connect::prelude::*;
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
|
||||||
use state::{CoopAuthUrlHandler, NostrRegistry};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
|
||||||
use ui::dock_area::ClosePanel;
|
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
|
||||||
use ui::notification::Notification;
|
|
||||||
use ui::{v_flex, Disableable, StyledExt, WindowExtension};
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ImportPanel> {
|
|
||||||
cx.new(|cx| ImportPanel::new(window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ImportPanel {
|
|
||||||
name: SharedString,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
|
|
||||||
/// Secret key input
|
|
||||||
key_input: Entity<InputState>,
|
|
||||||
|
|
||||||
/// Password input (if required)
|
|
||||||
pass_input: Entity<InputState>,
|
|
||||||
|
|
||||||
/// Error message
|
|
||||||
error: Entity<Option<SharedString>>,
|
|
||||||
|
|
||||||
/// Countdown timer for nostr connect
|
|
||||||
countdown: Entity<Option<u64>>,
|
|
||||||
|
|
||||||
/// Whether the user is currently logging in
|
|
||||||
logging_in: bool,
|
|
||||||
|
|
||||||
/// Event subscriptions
|
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ImportPanel {
|
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let key_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
|
||||||
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
|
||||||
|
|
||||||
let error = cx.new(|_| None);
|
|
||||||
let countdown = cx.new(|_| None);
|
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Subscribe to key input events and process login when the user presses enter
|
|
||||||
cx.subscribe_in(&key_input, window, |this, _input, event, window, cx| {
|
|
||||||
if let InputEvent::PressEnter { .. } = event {
|
|
||||||
this.login(window, cx);
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
key_input,
|
|
||||||
pass_input,
|
|
||||||
error,
|
|
||||||
countdown,
|
|
||||||
name: "Import".into(),
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
logging_in: false,
|
|
||||||
_subscriptions: subscriptions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if self.logging_in {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
// Prevent duplicate login requests
|
|
||||||
self.set_logging_in(true, cx);
|
|
||||||
|
|
||||||
let value = self.key_input.read(cx).value();
|
|
||||||
let password = self.pass_input.read(cx).value();
|
|
||||||
|
|
||||||
if value.starts_with("bunker://") {
|
|
||||||
self.login_with_bunker(&value, window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.starts_with("ncryptsec1") {
|
|
||||||
self.login_with_password(&value, &password, window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(secret) = SecretKey::parse(&value) {
|
|
||||||
let keys = Keys::new(secret);
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
// Update the signer
|
|
||||||
nostr.update(cx, |this, cx| {
|
|
||||||
this.set_signer(keys, true, cx);
|
|
||||||
});
|
|
||||||
// Close the current panel after setting the signer
|
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
||||||
} else {
|
|
||||||
self.set_error("Invalid", cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn login_with_bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Ok(uri) = NostrConnectUri::parse(content) else {
|
|
||||||
self.set_error("Bunker is not valid", cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let weak_state = nostr.downgrade();
|
|
||||||
|
|
||||||
let app_keys = nostr.read(cx).app_keys();
|
|
||||||
let timeout = Duration::from_secs(30);
|
|
||||||
let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap();
|
|
||||||
|
|
||||||
// Handle auth url with the default browser
|
|
||||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
|
||||||
|
|
||||||
// Start countdown
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
for i in (0..=30).rev() {
|
|
||||||
if i == 0 {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_countdown(None, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
} else {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_countdown(Some(i), cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
cx.background_executor().timer(Duration::from_secs(1)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
// Handle connection
|
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
|
||||||
let result = signer.bunker_uri().await;
|
|
||||||
|
|
||||||
weak_state
|
|
||||||
.update_in(cx, |this, window, cx| {
|
|
||||||
match result {
|
|
||||||
Ok(uri) => {
|
|
||||||
this.persist_bunker(uri, cx);
|
|
||||||
this.set_signer(signer, true, cx);
|
|
||||||
// Close the current panel after setting the signer
|
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn login_with_password(
|
|
||||||
&mut self,
|
|
||||||
content: &str,
|
|
||||||
pwd: &str,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if pwd.is_empty() {
|
|
||||||
self.set_error("Password is required", cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Ok(enc) = EncryptedSecretKey::from_bech32(content) else {
|
|
||||||
self.set_error("Secret Key is invalid", cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let password = pwd.to_owned();
|
|
||||||
|
|
||||||
// Decrypt in the background to ensure it doesn't block the UI
|
|
||||||
let task = cx.background_spawn(async move {
|
|
||||||
if let Ok(content) = enc.decrypt(&password) {
|
|
||||||
Ok(Keys::new(content))
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Invalid password"))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let result = task.await;
|
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
match result {
|
|
||||||
Ok(keys) => {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
// Update the signer
|
|
||||||
nostr.update(cx, |this, cx| {
|
|
||||||
this.set_signer(keys, true, cx);
|
|
||||||
});
|
|
||||||
// Close the current panel after setting the signer
|
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
this.set_error(e.to_string(), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
|
||||||
where
|
|
||||||
S: Into<SharedString>,
|
|
||||||
{
|
|
||||||
// Reset the log in state
|
|
||||||
self.set_logging_in(false, cx);
|
|
||||||
|
|
||||||
// Reset the countdown
|
|
||||||
self.set_countdown(None, cx);
|
|
||||||
|
|
||||||
// Update error message
|
|
||||||
self.error.update(cx, |this, cx| {
|
|
||||||
*this = Some(message.into());
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear the error message after 3 secs
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
cx.background_executor().timer(Duration::from_secs(3)).await;
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.error.update(cx, |this, cx| {
|
|
||||||
*this = None;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.logging_in = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_countdown(&mut self, i: Option<u64>, cx: &mut Context<Self>) {
|
|
||||||
self.countdown.update(cx, |this, cx| {
|
|
||||||
*this = i;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Panel for ImportPanel {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
self.name.clone().into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for ImportPanel {}
|
|
||||||
|
|
||||||
impl Focusable for ImportPanel {
|
|
||||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ImportPanel {
|
|
||||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
const SECRET_WARN: &str = "* Coop doesn't store your secret key. \
|
|
||||||
It will be cleared when you close the app. \
|
|
||||||
To persist your identity, please connect via Nostr Connect.";
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.p_2()
|
|
||||||
.gap_10()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_center()
|
|
||||||
.font_semibold()
|
|
||||||
.line_height(relative(1.25))
|
|
||||||
.child(SharedString::from("Import a Secret Key or Bunker")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.w_112()
|
|
||||||
.gap_2()
|
|
||||||
.text_sm()
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child("nsec or bunker://")
|
|
||||||
.child(TextInput::new(&self.key_input)),
|
|
||||||
)
|
|
||||||
.when(
|
|
||||||
self.key_input.read(cx).value().starts_with("ncryptsec1"),
|
|
||||||
|this| {
|
|
||||||
this.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child("Password:")
|
|
||||||
.child(TextInput::new(&self.pass_input)),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("login")
|
|
||||||
.label("Continue")
|
|
||||||
.primary()
|
|
||||||
.loading(self.logging_in)
|
|
||||||
.disabled(self.logging_in)
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.login(window, cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_center()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from(format!(
|
|
||||||
"Approve connection request from your signer in {} seconds",
|
|
||||||
i
|
|
||||||
))),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_center()
|
|
||||||
.text_color(cx.theme().danger_foreground)
|
|
||||||
.child(error.clone()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.mt_2()
|
|
||||||
.italic()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from(SECRET_WARN)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
pub mod backup;
|
pub mod backup;
|
||||||
pub mod connect;
|
|
||||||
pub mod contact_list;
|
pub mod contact_list;
|
||||||
pub mod greeter;
|
pub mod greeter;
|
||||||
pub mod import;
|
|
||||||
pub mod messaging_relays;
|
pub mod messaging_relays;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod relay_list;
|
pub mod relay_list;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use nostr_sdk::prelude::*;
|
|||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use state::{NostrRegistry, FIND_DELAY};
|
use state::{NostrRegistry, FIND_DELAY};
|
||||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
use theme::{ActiveTheme, SIDEBAR_WIDTH, TABBAR_HEIGHT};
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
@@ -585,10 +585,11 @@ impl Render for Sidebar {
|
|||||||
)
|
)
|
||||||
.when(!show_find_panel && !loading && total_rooms == 0, |this| {
|
.when(!show_find_panel && !loading && total_rooms == 0, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
div().px_2().child(
|
div().px_2().w(SIDEBAR_WIDTH).child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.p_3()
|
.p_3()
|
||||||
.h_24()
|
.h_24()
|
||||||
|
.w_full()
|
||||||
.border_2()
|
.border_2()
|
||||||
.border_dashed()
|
.border_dashed()
|
||||||
.border_color(cx.theme().border_variant)
|
.border_color(cx.theme().border_variant)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use gpui::{
|
|||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use state::{NostrRegistry, RelayState};
|
use state::{NostrRegistry, RelayState, SignerEvent};
|
||||||
use theme::{ActiveTheme, Theme, ThemeRegistry, SIDEBAR_WIDTH};
|
use theme::{ActiveTheme, Theme, ThemeRegistry, SIDEBAR_WIDTH};
|
||||||
use title_bar::TitleBar;
|
use title_bar::TitleBar;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
@@ -23,7 +23,7 @@ use ui::menu::{DropdownMenu, PopupMenuItem};
|
|||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension};
|
use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension};
|
||||||
|
|
||||||
use crate::dialogs::settings;
|
use crate::dialogs::{accounts, settings};
|
||||||
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list};
|
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list};
|
||||||
use crate::sidebar;
|
use crate::sidebar;
|
||||||
|
|
||||||
@@ -42,6 +42,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
|||||||
#[action(namespace = workspace, no_json)]
|
#[action(namespace = workspace, no_json)]
|
||||||
enum Command {
|
enum Command {
|
||||||
ToggleTheme,
|
ToggleTheme,
|
||||||
|
ToggleAccount,
|
||||||
|
|
||||||
RefreshEncryption,
|
RefreshEncryption,
|
||||||
RefreshRelayList,
|
RefreshRelayList,
|
||||||
@@ -64,11 +65,13 @@ pub struct Workspace {
|
|||||||
dock: Entity<DockArea>,
|
dock: Entity<DockArea>,
|
||||||
|
|
||||||
/// Event subscriptions
|
/// Event subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 3]>,
|
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Workspace {
|
impl Workspace {
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let npubs = nostr.read(cx).npubs();
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let titlebar = cx.new(|_| TitleBar::new());
|
let titlebar = cx.new(|_| TitleBar::new());
|
||||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
let dock = cx.new(|cx| DockArea::new(window, cx));
|
||||||
@@ -82,6 +85,24 @@ impl Workspace {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Observe the npubs entity
|
||||||
|
cx.observe_in(&npubs, window, move |this, npubs, window, cx| {
|
||||||
|
if !npubs.read(cx).is_empty() {
|
||||||
|
this.account_selector(window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Subscribe to the signer events
|
||||||
|
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
|
||||||
|
if let SignerEvent::Set = event {
|
||||||
|
this.set_center_layout(window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe all events emitted by the chat registry
|
// Observe all events emitted by the chat registry
|
||||||
cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| {
|
cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| {
|
||||||
@@ -121,12 +142,12 @@ impl Workspace {
|
|||||||
let ids = this.panel_ids(cx);
|
let ids = this.panel_ids(cx);
|
||||||
|
|
||||||
chat.update(cx, |this, cx| {
|
chat.update(cx, |this, cx| {
|
||||||
this.refresh_rooms(ids, cx);
|
this.refresh_rooms(&ids, cx);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set the default layout for app's dock
|
// Set the layout at the end of cycle
|
||||||
cx.defer_in(window, |this, window, cx| {
|
cx.defer_in(window, |this, window, cx| {
|
||||||
this.set_layout(window, cx);
|
this.set_layout(window, cx);
|
||||||
});
|
});
|
||||||
@@ -155,49 +176,40 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get all panel ids
|
/// Get all panel ids
|
||||||
fn panel_ids(&self, cx: &App) -> Option<Vec<u64>> {
|
fn panel_ids(&self, cx: &App) -> Vec<u64> {
|
||||||
let ids: Vec<u64> = self
|
self.dock
|
||||||
.dock
|
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.items
|
.items
|
||||||
.panel_ids(cx)
|
.panel_ids(cx)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|panel| panel.parse::<u64>().ok())
|
.filter_map(|panel| panel.parse::<u64>().ok())
|
||||||
.collect();
|
.collect()
|
||||||
|
|
||||||
Some(ids)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the dock layout
|
/// Set the dock layout
|
||||||
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let weak_dock = self.dock.downgrade();
|
|
||||||
|
|
||||||
// Sidebar
|
|
||||||
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
|
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
|
||||||
|
|
||||||
// Main workspace
|
// Update the dock layout with sidebar on the left
|
||||||
let center = DockItem::split_with_sizes(
|
|
||||||
Axis::Vertical,
|
|
||||||
vec![DockItem::tabs(
|
|
||||||
vec![Arc::new(greeter::init(window, cx))],
|
|
||||||
None,
|
|
||||||
&weak_dock,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)],
|
|
||||||
vec![None],
|
|
||||||
&weak_dock,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the dock layout
|
|
||||||
self.dock.update(cx, |this, cx| {
|
self.dock.update(cx, |this, cx| {
|
||||||
this.set_left_dock(left, Some(SIDEBAR_WIDTH), true, window, cx);
|
this.set_left_dock(left, Some(SIDEBAR_WIDTH), true, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the center dock layout
|
||||||
|
fn set_center_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let dock = self.dock.downgrade();
|
||||||
|
let greeeter = Arc::new(greeter::init(window, cx));
|
||||||
|
let tabs = DockItem::tabs(vec![greeeter], None, &dock, window, cx);
|
||||||
|
let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx);
|
||||||
|
|
||||||
|
// Update the layout with center dock
|
||||||
|
self.dock.update(cx, |this, cx| {
|
||||||
this.set_center(center, window, cx);
|
this.set_center(center, window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle command events
|
||||||
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
|
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
match command {
|
match command {
|
||||||
Command::ShowSettings => {
|
Command::ShowSettings => {
|
||||||
@@ -206,7 +218,7 @@ impl Workspace {
|
|||||||
window.open_modal(cx, move |this, _window, _cx| {
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
this.width(px(520.))
|
this.width(px(520.))
|
||||||
.show_close(true)
|
.show_close(true)
|
||||||
.pb_4()
|
.pb_2()
|
||||||
.title("Preferences")
|
.title("Preferences")
|
||||||
.child(view.clone())
|
.child(view.clone())
|
||||||
});
|
});
|
||||||
@@ -290,6 +302,9 @@ impl Workspace {
|
|||||||
Command::ToggleTheme => {
|
Command::ToggleTheme => {
|
||||||
self.theme_selector(window, cx);
|
self.theme_selector(window, cx);
|
||||||
}
|
}
|
||||||
|
Command::ToggleAccount => {
|
||||||
|
self.account_selector(window, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,6 +356,20 @@ impl Workspace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn account_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let accounts = accounts::init(window, cx);
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
|
this.width(px(520.))
|
||||||
|
.title("Continue with")
|
||||||
|
.show_close(false)
|
||||||
|
.keyboard(false)
|
||||||
|
.overlay_closable(false)
|
||||||
|
.pb_2()
|
||||||
|
.child(accounts.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn theme_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn theme_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
window.open_modal(cx, move |this, _window, cx| {
|
window.open_modal(cx, move |this, _window, cx| {
|
||||||
let registry = ThemeRegistry::global(cx);
|
let registry = ThemeRegistry::global(cx);
|
||||||
@@ -349,20 +378,22 @@ impl Workspace {
|
|||||||
this.width(px(520.))
|
this.width(px(520.))
|
||||||
.show_close(true)
|
.show_close(true)
|
||||||
.title("Select theme")
|
.title("Select theme")
|
||||||
.pb_4()
|
.pb_2()
|
||||||
.child(v_flex().gap_2().w_full().children({
|
.child(v_flex().gap_2().w_full().children({
|
||||||
let mut items = vec![];
|
let mut items = vec![];
|
||||||
|
|
||||||
for (ix, (path, theme)) in themes.iter().enumerate() {
|
for (ix, (path, theme)) in themes.iter().enumerate() {
|
||||||
items.push(
|
items.push(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.id(ix)
|
||||||
.group("")
|
.group("")
|
||||||
.px_2()
|
.px_2()
|
||||||
.h_8()
|
.h_8()
|
||||||
.w_full()
|
.w_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
.bg(cx.theme().ghost_element_background)
|
||||||
|
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
@@ -427,8 +458,15 @@ impl Workspace {
|
|||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.justify_between()
|
|
||||||
.gap_2()
|
.gap_2()
|
||||||
|
.when_none(¤t_user, |this| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from("Choose an account to continue...")),
|
||||||
|
)
|
||||||
|
})
|
||||||
.when_some(current_user.as_ref(), |this, public_key| {
|
.when_some(current_user.as_ref(), |this, public_key| {
|
||||||
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);
|
||||||
@@ -477,6 +515,11 @@ impl Workspace {
|
|||||||
Box::new(Command::ToggleTheme),
|
Box::new(Command::ToggleTheme),
|
||||||
)
|
)
|
||||||
.separator()
|
.separator()
|
||||||
|
.menu_with_icon(
|
||||||
|
"Accounts",
|
||||||
|
IconName::Group,
|
||||||
|
Box::new(Command::ToggleAccount),
|
||||||
|
)
|
||||||
.menu_with_icon(
|
.menu_with_icon(
|
||||||
"Settings",
|
"Settings",
|
||||||
IconName::Settings,
|
IconName::Settings,
|
||||||
@@ -485,25 +528,11 @@ impl Workspace {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(nostr.read(cx).creating(), |this| {
|
|
||||||
this.child(div().text_xs().text_color(cx.theme().text_muted).child(
|
|
||||||
SharedString::from("Coop is creating a new identity for you..."),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.when(!nostr.read(cx).connected(), |this| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from("Connecting...")),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn titlebar_right(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
fn titlebar_right(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
let relay_list = nostr.read(cx).relay_list_state();
|
|
||||||
|
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let inbox_state = chat.read(cx).state(cx);
|
let inbox_state = chat.read(cx).state(cx);
|
||||||
@@ -633,7 +662,7 @@ impl Workspace {
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.map(|this| match relay_list {
|
.map(|this| match nostr.read(cx).relay_list_state {
|
||||||
RelayState::Checking => this
|
RelayState::Checking => this
|
||||||
.child(div().child(SharedString::from(
|
.child(div().child(SharedString::from(
|
||||||
"Fetching user's relay list...",
|
"Fetching user's relay list...",
|
||||||
@@ -652,7 +681,9 @@ impl Workspace {
|
|||||||
.tooltip("User's relay list")
|
.tooltip("User's relay list")
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.when(relay_list.configured(), |this| this.indicator())
|
.when(nostr.read(cx).relay_list_state.configured(), |this| {
|
||||||
|
this.indicator()
|
||||||
|
})
|
||||||
.dropdown_menu(move |this, _window, cx| {
|
.dropdown_menu(move |this, _window, cx| {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let urls = nostr.read(cx).read_only_relays(&pkey, cx);
|
let urls = nostr.read(cx).read_only_relays(&pkey, cx);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ impl DeviceRegistry {
|
|||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe the NIP-65 state
|
// Observe the NIP-65 state
|
||||||
cx.observe(&nostr, |this, state, cx| {
|
cx.observe(&nostr, |this, state, cx| {
|
||||||
if state.read(cx).relay_list_state() == RelayState::Configured {
|
if state.read(cx).relay_list_state == RelayState::Configured {
|
||||||
this.get_announcement(cx);
|
this.get_announcement(cx);
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -204,9 +204,11 @@ impl DeviceRegistry {
|
|||||||
fn subscribe_to_giftwrap_events(&mut self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
|
fn subscribe_to_giftwrap_events(&mut self, cx: &mut Context<Self>) -> 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 signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
let public_key = signer.public_key().unwrap();
|
|
||||||
|
let Some(public_key) = signer.public_key() else {
|
||||||
|
return Task::ready(Err(anyhow!("User not found")));
|
||||||
|
};
|
||||||
|
|
||||||
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);
|
||||||
@@ -237,9 +239,11 @@ impl DeviceRegistry {
|
|||||||
pub fn get_announcement(&mut self, cx: &mut Context<Self>) {
|
pub fn get_announcement(&mut self, cx: &mut Context<Self>) {
|
||||||
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();
|
let signer = nostr.read(cx).signer();
|
||||||
let public_key = signer.public_key().unwrap();
|
|
||||||
|
let Some(public_key) = signer.public_key() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
// Reset state before fetching announcement
|
// Reset state before fetching announcement
|
||||||
self.reset(cx);
|
self.reset(cx);
|
||||||
@@ -303,10 +307,11 @@ impl DeviceRegistry {
|
|||||||
pub fn create_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> {
|
pub fn create_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
// Get current user
|
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
let public_key = signer.public_key().unwrap();
|
|
||||||
|
let Some(public_key) = signer.public_key() else {
|
||||||
|
return Task::ready(Err(anyhow!("User not found")));
|
||||||
|
};
|
||||||
|
|
||||||
// Get user's write relays
|
// Get user's write relays
|
||||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||||
@@ -398,9 +403,11 @@ impl DeviceRegistry {
|
|||||||
pub fn listen_request(&mut self, cx: &mut Context<Self>) {
|
pub fn listen_request(&mut self, cx: &mut Context<Self>) {
|
||||||
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();
|
let signer = nostr.read(cx).signer();
|
||||||
let public_key = signer.public_key().unwrap();
|
|
||||||
|
let Some(public_key) = signer.public_key() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||||
|
|
||||||
@@ -430,9 +437,11 @@ impl DeviceRegistry {
|
|||||||
fn listen_approval(&mut self, cx: &mut Context<Self>) {
|
fn listen_approval(&mut self, cx: &mut Context<Self>) {
|
||||||
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();
|
let signer = nostr.read(cx).signer();
|
||||||
let public_key = signer.public_key().unwrap();
|
|
||||||
|
let Some(public_key) = signer.public_key() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||||
|
|
||||||
@@ -460,13 +469,15 @@ impl DeviceRegistry {
|
|||||||
fn request(&mut self, cx: &mut Context<Self>) {
|
fn request(&mut self, cx: &mut Context<Self>) {
|
||||||
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();
|
let signer = nostr.read(cx).signer();
|
||||||
let public_key = signer.public_key().unwrap();
|
|
||||||
|
let Some(public_key) = signer.public_key() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||||
|
|
||||||
let app_keys = nostr.read(cx).app_keys().clone();
|
let app_keys = nostr.read(cx).app_keys.clone();
|
||||||
let app_pubkey = app_keys.public_key();
|
let app_pubkey = app_keys.public_key();
|
||||||
|
|
||||||
let task: Task<Result<Option<Keys>, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Option<Keys>, Error>> = cx.background_spawn(async move {
|
||||||
@@ -538,7 +549,7 @@ impl DeviceRegistry {
|
|||||||
/// Parse the response event for device keys from other devices
|
/// Parse the response event for device keys from other devices
|
||||||
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
|
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let app_keys = nostr.read(cx).app_keys().clone();
|
let app_keys = nostr.read(cx).app_keys.clone();
|
||||||
|
|
||||||
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
|
||||||
let root_device = event
|
let root_device = event
|
||||||
@@ -573,10 +584,11 @@ impl DeviceRegistry {
|
|||||||
fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context<Self>) {
|
fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
// Get current user
|
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
let public_key = signer.public_key().unwrap();
|
|
||||||
|
let Some(public_key) = signer.public_key() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
// Get user's write relays
|
// Get user's write relays
|
||||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||||
|
|||||||
@@ -21,18 +21,18 @@ pub const FIND_DELAY: u64 = 600;
|
|||||||
/// Default limit for searching
|
/// Default limit for searching
|
||||||
pub const FIND_LIMIT: usize = 20;
|
pub const FIND_LIMIT: usize = 20;
|
||||||
|
|
||||||
/// Default timeout for Nostr Connect
|
|
||||||
pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
|
|
||||||
|
|
||||||
/// Default Nostr Connect relay
|
|
||||||
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
|
||||||
|
|
||||||
/// Default subscription id for device gift wrap events
|
/// Default subscription id for device gift wrap events
|
||||||
pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps";
|
pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps";
|
||||||
|
|
||||||
/// Default subscription id for user gift wrap events
|
/// Default subscription id for user gift wrap events
|
||||||
pub const USER_GIFTWRAP: &str = "user-gift-wraps";
|
pub const USER_GIFTWRAP: &str = "user-gift-wraps";
|
||||||
|
|
||||||
|
/// Default timeout for Nostr Connect
|
||||||
|
pub const NOSTR_CONNECT_TIMEOUT: u64 = 60;
|
||||||
|
|
||||||
|
/// Default Nostr Connect relay
|
||||||
|
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nip46.com";
|
||||||
|
|
||||||
/// Default vertex relays
|
/// Default vertex relays
|
||||||
pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
|
pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
|
||||||
|
|
||||||
@@ -40,10 +40,9 @@ pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
|
|||||||
pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
|
pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
|
||||||
|
|
||||||
/// Default bootstrap relays
|
/// Default bootstrap relays
|
||||||
pub const BOOTSTRAP_RELAYS: [&str; 4] = [
|
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
||||||
"wss://nos.lol",
|
|
||||||
"wss://relay.damus.io",
|
|
||||||
"wss://relay.primal.net",
|
"wss://relay.primal.net",
|
||||||
|
"wss://indexer.coracle.social",
|
||||||
"wss://user.kindpag.es",
|
"wss://user.kindpag.es",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||||
use common::config_dir;
|
use common::config_dir;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, SharedString, Task, Window};
|
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use nostr_lmdb::prelude::*;
|
use nostr_lmdb::prelude::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -51,22 +51,19 @@ pub struct NostrRegistry {
|
|||||||
/// Nostr signer
|
/// Nostr signer
|
||||||
signer: Arc<CoopSigner>,
|
signer: Arc<CoopSigner>,
|
||||||
|
|
||||||
/// App keys
|
/// Local public keys
|
||||||
///
|
npubs: Entity<Vec<PublicKey>>,
|
||||||
/// Used for Nostr Connect and NIP-4e operations
|
|
||||||
app_keys: Keys,
|
|
||||||
|
|
||||||
/// Custom gossip implementation
|
/// Custom gossip implementation
|
||||||
gossip: Entity<Gossip>,
|
gossip: Entity<Gossip>,
|
||||||
|
|
||||||
|
/// App keys
|
||||||
|
///
|
||||||
|
/// Used for Nostr Connect and NIP-4e operations
|
||||||
|
pub app_keys: Keys,
|
||||||
|
|
||||||
/// Relay list state
|
/// Relay list state
|
||||||
relay_list_state: RelayState,
|
pub relay_list_state: RelayState,
|
||||||
|
|
||||||
/// Whether Coop is connected to all bootstrap relays
|
|
||||||
connected: bool,
|
|
||||||
|
|
||||||
/// Whether Coop is creating a new signer
|
|
||||||
creating: bool,
|
|
||||||
|
|
||||||
/// Tasks for asynchronous operations
|
/// Tasks for asynchronous operations
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
@@ -89,6 +86,9 @@ impl NostrRegistry {
|
|||||||
let app_keys = get_or_init_app_keys().unwrap_or(Keys::generate());
|
let app_keys = get_or_init_app_keys().unwrap_or(Keys::generate());
|
||||||
let signer = Arc::new(CoopSigner::new(app_keys.clone()));
|
let signer = Arc::new(CoopSigner::new(app_keys.clone()));
|
||||||
|
|
||||||
|
// Construct the nostr npubs entity
|
||||||
|
let npubs = cx.new(|_| vec![]);
|
||||||
|
|
||||||
// Construct the gossip entity
|
// Construct the gossip entity
|
||||||
let gossip = cx.new(|_| Gossip::default());
|
let gossip = cx.new(|_| Gossip::default());
|
||||||
|
|
||||||
@@ -120,15 +120,30 @@ impl NostrRegistry {
|
|||||||
Self {
|
Self {
|
||||||
client,
|
client,
|
||||||
signer,
|
signer,
|
||||||
|
npubs,
|
||||||
app_keys,
|
app_keys,
|
||||||
gossip,
|
gossip,
|
||||||
relay_list_state: RelayState::Idle,
|
relay_list_state: RelayState::Idle,
|
||||||
connected: false,
|
|
||||||
creating: false,
|
|
||||||
tasks: vec![],
|
tasks: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the nostr client
|
||||||
|
pub fn client(&self) -> Client {
|
||||||
|
self.client.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the nostr signer
|
||||||
|
pub fn signer(&self) -> Arc<CoopSigner> {
|
||||||
|
self.signer.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the npubs entity
|
||||||
|
pub fn npubs(&self) -> Entity<Vec<PublicKey>> {
|
||||||
|
self.npubs.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to the bootstrapping relays
|
||||||
fn connect(&mut self, cx: &mut Context<Self>) {
|
fn connect(&mut self, cx: &mut Context<Self>) {
|
||||||
let client = self.client();
|
let client = self.client();
|
||||||
|
|
||||||
@@ -146,14 +161,13 @@ impl NostrRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connect to all added relays
|
// Connect to all added relays
|
||||||
client.connect().and_wait(Duration::from_secs(5)).await;
|
client.connect().and_wait(Duration::from_secs(2)).await;
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Update the state
|
// Update the state
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_connected(cx);
|
this.get_npubs(cx);
|
||||||
this.get_signer(cx);
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -214,39 +228,406 @@ impl NostrRegistry {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the nostr client
|
/// Get all used npubs
|
||||||
pub fn client(&self) -> Client {
|
fn get_npubs(&mut self, cx: &mut Context<Self>) {
|
||||||
self.client.clone()
|
let npubs = self.npubs.downgrade();
|
||||||
|
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
|
||||||
|
let dir = config_dir().join("keys");
|
||||||
|
// Ensure keys directory exists
|
||||||
|
smol::fs::create_dir_all(&dir).await?;
|
||||||
|
|
||||||
|
let mut files = smol::fs::read_dir(&dir).await?;
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
|
while let Some(Ok(entry)) = files.next().await {
|
||||||
|
let metadata = entry.metadata().await?;
|
||||||
|
let modified_time = metadata.modified()?;
|
||||||
|
let name = entry
|
||||||
|
.file_name()
|
||||||
|
.into_string()
|
||||||
|
.unwrap()
|
||||||
|
.replace(".npub", "");
|
||||||
|
|
||||||
|
entries.push((modified_time, name));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the nostr signer
|
// Sort by modification time (most recent first)
|
||||||
pub fn signer(&self) -> Arc<CoopSigner> {
|
entries.sort_by(|a, b| b.0.cmp(&a.0));
|
||||||
self.signer.clone()
|
|
||||||
|
let mut npubs = Vec::new();
|
||||||
|
|
||||||
|
for (_, name) in entries {
|
||||||
|
let public_key = PublicKey::parse(&name)?;
|
||||||
|
npubs.push(public_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the app keys
|
Ok(npubs)
|
||||||
pub fn app_keys(&self) -> &Keys {
|
});
|
||||||
&self.app_keys
|
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(public_keys) => match public_keys.is_empty() {
|
||||||
|
true => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.create_identity(cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
// TODO: auto login
|
||||||
|
npubs.update(cx, |this, cx| {
|
||||||
|
this.extend(public_keys);
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to get npubs: {e}");
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.create_identity(cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the connected status of the client
|
Ok(())
|
||||||
pub fn connected(&self) -> bool {
|
}));
|
||||||
self.connected
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the creating status
|
/// Create a new identity
|
||||||
pub fn creating(&self) -> bool {
|
fn create_identity(&mut self, cx: &mut Context<Self>) {
|
||||||
self.creating
|
let client = self.client();
|
||||||
|
let keys = Keys::generate();
|
||||||
|
let async_keys = keys.clone();
|
||||||
|
|
||||||
|
let username = keys.public_key().to_bech32().unwrap();
|
||||||
|
let secret = keys.secret_key().to_secret_bytes();
|
||||||
|
|
||||||
|
// Create a write credential task
|
||||||
|
let write_credential = cx.write_credentials(&username, &username, &secret);
|
||||||
|
|
||||||
|
// Run async tasks in background
|
||||||
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
|
let signer = async_keys.into_nostr_signer();
|
||||||
|
|
||||||
|
// Get default relay list
|
||||||
|
let relay_list = default_relay_list();
|
||||||
|
|
||||||
|
// Publish relay list event
|
||||||
|
let event = EventBuilder::relay_list(relay_list).sign(&signer).await?;
|
||||||
|
client
|
||||||
|
.send_event(&event)
|
||||||
|
.ok_timeout(Duration::from_secs(TIMEOUT))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Construct the default metadata
|
||||||
|
let name = petname::petname(2, "-").unwrap_or("Cooper".to_string());
|
||||||
|
let avatar = Url::parse(&format!("https://avatar.vercel.sh/{name}")).unwrap();
|
||||||
|
let metadata = Metadata::new().display_name(&name).picture(avatar);
|
||||||
|
|
||||||
|
// Publish metadata event
|
||||||
|
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
|
||||||
|
client
|
||||||
|
.send_event(&event)
|
||||||
|
.ack_policy(AckPolicy::none())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Construct the default contact list
|
||||||
|
let contacts = vec![Contact::new(PublicKey::parse(COOP_PUBKEY).unwrap())];
|
||||||
|
|
||||||
|
// Publish contact list event
|
||||||
|
let event = EventBuilder::contact_list(contacts).sign(&signer).await?;
|
||||||
|
client
|
||||||
|
.send_event(&event)
|
||||||
|
.ack_policy(AckPolicy::none())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Construct the default messaging relay list
|
||||||
|
let relays = default_messaging_relays();
|
||||||
|
|
||||||
|
// Publish messaging relay list event
|
||||||
|
let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?;
|
||||||
|
client
|
||||||
|
.send_event(&event)
|
||||||
|
.ack_policy(AckPolicy::none())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Write user's credentials to the system keyring
|
||||||
|
write_credential.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
// Wait for the task to complete
|
||||||
|
task.await?;
|
||||||
|
|
||||||
|
// Set signer
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_signer(keys, cx);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the relay list state
|
/// Get the signer in keyring by username
|
||||||
pub fn relay_list_state(&self) -> RelayState {
|
pub fn get_signer(
|
||||||
self.relay_list_state.clone()
|
&self,
|
||||||
|
public_key: &PublicKey,
|
||||||
|
cx: &App,
|
||||||
|
) -> Task<Result<Arc<dyn NostrSigner>, Error>> {
|
||||||
|
let username = public_key.to_bech32().unwrap();
|
||||||
|
let app_keys = self.app_keys.clone();
|
||||||
|
let read_credential = cx.read_credentials(&username);
|
||||||
|
|
||||||
|
cx.spawn(async move |_cx| {
|
||||||
|
let (_, secret) = read_credential
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))?
|
||||||
|
.ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?;
|
||||||
|
|
||||||
|
// Try to parse as a direct secret key first
|
||||||
|
if let Ok(secret_key) = SecretKey::from_slice(&secret) {
|
||||||
|
return Ok(Keys::new(secret_key).into_nostr_signer());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all relays for a given public key without ensuring connections
|
// Convert the secret into string
|
||||||
pub fn read_only_relays(&self, public_key: &PublicKey, cx: &App) -> Vec<SharedString> {
|
let sec = String::from_utf8(secret)
|
||||||
self.gossip.read(cx).read_only_relays(public_key)
|
.map_err(|_| anyhow!("Failed to parse secret as UTF-8"))?;
|
||||||
|
|
||||||
|
// Try to parse as a NIP-46 URI
|
||||||
|
let uri =
|
||||||
|
NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?;
|
||||||
|
|
||||||
|
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||||
|
let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?;
|
||||||
|
|
||||||
|
// Set the auth URL handler
|
||||||
|
nip46.auth_url_handler(CoopAuthUrlHandler);
|
||||||
|
|
||||||
|
Ok(nip46.into_nostr_signer())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the signer for the nostr client and verify the public key
|
||||||
|
pub fn set_signer<T>(&mut self, new: T, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
T: NostrSigner + 'static,
|
||||||
|
{
|
||||||
|
let client = self.client();
|
||||||
|
let signer = self.signer();
|
||||||
|
|
||||||
|
// Create a task to update the signer and verify the public key
|
||||||
|
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
||||||
|
// Update signer and unsubscribe
|
||||||
|
signer.switch(new).await;
|
||||||
|
client.unsubscribe_all().await?;
|
||||||
|
|
||||||
|
// Verify and get public key
|
||||||
|
let signer = client.signer().context("Signer not found")?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
let npub = public_key.to_bech32().unwrap();
|
||||||
|
let keys_dir = config_dir().join("keys");
|
||||||
|
|
||||||
|
// Ensure keys directory exists
|
||||||
|
smol::fs::create_dir_all(&keys_dir).await?;
|
||||||
|
|
||||||
|
let key_path = keys_dir.join(format!("{}.npub", npub));
|
||||||
|
smol::fs::write(key_path, "").await?;
|
||||||
|
|
||||||
|
log::info!("Signer's public key: {}", public_key);
|
||||||
|
Ok(public_key)
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(public_key) => {
|
||||||
|
// Update states
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
// Add public key to npubs if not already present
|
||||||
|
this.npubs.update(cx, |this, cx| {
|
||||||
|
if !this.contains(&public_key) {
|
||||||
|
this.push(public_key);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure relay list for the user
|
||||||
|
this.ensure_relay_list(cx);
|
||||||
|
|
||||||
|
// Emit signer changed event
|
||||||
|
cx.emit(SignerEvent::Set);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(SignerEvent::Error(e.to_string()));
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a signer from the keyring
|
||||||
|
pub fn remove_signer(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
||||||
|
let public_key = public_key.to_owned();
|
||||||
|
let npub = public_key.to_bech32().unwrap();
|
||||||
|
let keys_dir = config_dir().join("keys");
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
let key_path = keys_dir.join(format!("{}.npub", npub));
|
||||||
|
smol::fs::remove_file(key_path).await?;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.npubs().update(cx, |this, cx| {
|
||||||
|
this.retain(|k| k != &public_key);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a key signer to keyring
|
||||||
|
pub fn add_key_signer(&mut self, keys: &Keys, cx: &mut Context<Self>) {
|
||||||
|
let keys = keys.clone();
|
||||||
|
let username = keys.public_key().to_bech32().unwrap();
|
||||||
|
let secret = keys.secret_key().to_secret_bytes();
|
||||||
|
|
||||||
|
// Write the credential to the keyring
|
||||||
|
let write_credential = cx.write_credentials(&username, "keys", &secret);
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
match write_credential.await {
|
||||||
|
Ok(_) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_signer(keys, cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(SignerEvent::Error(e.to_string()));
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a nostr connect signer to keyring
|
||||||
|
pub fn add_nip46_signer(&mut self, nip46: &NostrConnect, cx: &mut Context<Self>) {
|
||||||
|
let nip46 = nip46.clone();
|
||||||
|
let async_nip46 = nip46.clone();
|
||||||
|
|
||||||
|
// Connect and verify the remote signer
|
||||||
|
let task: Task<Result<(PublicKey, NostrConnectUri), Error>> =
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let uri = async_nip46.bunker_uri().await?;
|
||||||
|
let public_key = async_nip46.get_public_key().await?;
|
||||||
|
|
||||||
|
Ok((public_key, uri))
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok((public_key, uri)) => {
|
||||||
|
let username = public_key.to_bech32().unwrap();
|
||||||
|
let write_credential = this.read_with(cx, |_this, cx| {
|
||||||
|
cx.write_credentials(&username, "nostrconnect", uri.to_string().as_bytes())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match write_credential.await {
|
||||||
|
Ok(_) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_signer(nip46, cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(SignerEvent::Error(e.to_string()));
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(SignerEvent::Error(e.to_string()));
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the state of the relay list
|
||||||
|
fn set_relay_state(&mut self, state: RelayState, cx: &mut Context<Self>) {
|
||||||
|
self.relay_list_state = state;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_relay_list(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let task = self.verify_relay_list(cx);
|
||||||
|
|
||||||
|
// Set the state to idle before starting the task
|
||||||
|
self.set_relay_state(RelayState::default(), cx);
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
let result = task.await?;
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.relay_list_state = result;
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify relay list for current user
|
||||||
|
fn verify_relay_list(&mut self, cx: &mut Context<Self>) -> Task<Result<RelayState, Error>> {
|
||||||
|
let client = self.client();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let signer = client.signer().context("Signer not found")?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::RelayList)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// Construct target for subscription
|
||||||
|
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
|
||||||
|
.into_iter()
|
||||||
|
.map(|relay| (relay, vec![filter.clone()]))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Stream events from the bootstrap relays
|
||||||
|
let mut stream = client
|
||||||
|
.stream_events(target)
|
||||||
|
.timeout(Duration::from_secs(TIMEOUT))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
while let Some((_url, res)) = stream.next().await {
|
||||||
|
match res {
|
||||||
|
Ok(event) => {
|
||||||
|
log::info!("Received relay list event: {event:?}");
|
||||||
|
return Ok(RelayState::Configured);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to receive relay list event: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RelayState::NotConfigured)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure write relays for a given public key
|
/// Ensure write relays for a given public key
|
||||||
@@ -336,316 +717,9 @@ impl NostrRegistry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the connected status of the client
|
/// Get all relays for a given public key without ensuring connections
|
||||||
fn set_connected(&mut self, cx: &mut Context<Self>) {
|
pub fn read_only_relays(&self, public_key: &PublicKey, cx: &App) -> Vec<SharedString> {
|
||||||
self.connected = true;
|
self.gossip.read(cx).read_only_relays(public_key)
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get local stored signer
|
|
||||||
fn get_signer(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let read_credential = cx.read_credentials(KEYRING);
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
match read_credential.await {
|
|
||||||
Ok(Some((_user, secret))) => {
|
|
||||||
let secret = SecretKey::from_slice(&secret)?;
|
|
||||||
let keys = Keys::new(secret);
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_signer(keys, false, cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.get_bunker(cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get local stored bunker connection
|
|
||||||
fn get_bunker(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let client = self.client();
|
|
||||||
let app_keys = self.app_keys().clone();
|
|
||||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
|
||||||
|
|
||||||
let task: Task<Result<NostrConnect, Error>> = cx.background_spawn(async move {
|
|
||||||
log::info!("Getting bunker connection");
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::ApplicationSpecificData)
|
|
||||||
.identifier("coop:account")
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
|
||||||
let uri = NostrConnectUri::parse(event.content)?;
|
|
||||||
let signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None)?;
|
|
||||||
|
|
||||||
Ok(signer)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("No account found"))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
match task.await {
|
|
||||||
Ok(signer) => {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_signer(signer, true, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("Failed to get bunker: {e}");
|
|
||||||
// Create a new identity if no stored bunker exists
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_default_signer(cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the signer for the nostr client and verify the public key
|
|
||||||
pub fn set_signer<T>(&mut self, new: T, owned: bool, cx: &mut Context<Self>)
|
|
||||||
where
|
|
||||||
T: NostrSigner + 'static,
|
|
||||||
{
|
|
||||||
let client = self.client();
|
|
||||||
let signer = self.signer();
|
|
||||||
|
|
||||||
// Create a task to update the signer and verify the public key
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
|
||||||
// Update signer
|
|
||||||
signer.switch(new, owned).await;
|
|
||||||
|
|
||||||
// Unsubscribe from all subscriptions
|
|
||||||
client.unsubscribe_all().await?;
|
|
||||||
|
|
||||||
// Verify signer
|
|
||||||
let signer = client.signer().context("Signer not found")?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
log::info!("Signer's public key: {}", public_key);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
// set signer
|
|
||||||
task.await?;
|
|
||||||
|
|
||||||
// Update states
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.ensure_relay_list(cx);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new identity
|
|
||||||
fn set_default_signer(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let client = self.client();
|
|
||||||
let keys = Keys::generate();
|
|
||||||
let async_keys = keys.clone();
|
|
||||||
|
|
||||||
// Create a write credential task
|
|
||||||
let write_credential = cx.write_credentials(
|
|
||||||
KEYRING,
|
|
||||||
&keys.public_key().to_hex(),
|
|
||||||
&keys.secret_key().to_secret_bytes(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set the creating signer status
|
|
||||||
self.set_creating_signer(true, cx);
|
|
||||||
|
|
||||||
// Run async tasks in background
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
|
||||||
let signer = async_keys.into_nostr_signer();
|
|
||||||
|
|
||||||
// Get default relay list
|
|
||||||
let relay_list = default_relay_list();
|
|
||||||
|
|
||||||
// Publish relay list event
|
|
||||||
let event = EventBuilder::relay_list(relay_list).sign(&signer).await?;
|
|
||||||
client
|
|
||||||
.send_event(&event)
|
|
||||||
.ok_timeout(Duration::from_secs(TIMEOUT))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Construct the default metadata
|
|
||||||
let name = petname::petname(2, "-").unwrap_or("Cooper".to_string());
|
|
||||||
let avatar = Url::parse(&format!("https://avatar.vercel.sh/{name}")).unwrap();
|
|
||||||
let metadata = Metadata::new().display_name(&name).picture(avatar);
|
|
||||||
|
|
||||||
// Publish metadata event
|
|
||||||
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
|
|
||||||
client
|
|
||||||
.send_event(&event)
|
|
||||||
.ok_timeout(Duration::from_secs(TIMEOUT))
|
|
||||||
.ack_policy(AckPolicy::none())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Construct the default contact list
|
|
||||||
let contacts = vec![Contact::new(PublicKey::parse(COOP_PUBKEY).unwrap())];
|
|
||||||
|
|
||||||
// Publish contact list event
|
|
||||||
let event = EventBuilder::contact_list(contacts).sign(&signer).await?;
|
|
||||||
client
|
|
||||||
.send_event(&event)
|
|
||||||
.ok_timeout(Duration::from_secs(TIMEOUT))
|
|
||||||
.ack_policy(AckPolicy::none())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Construct the default messaging relay list
|
|
||||||
let relays = default_messaging_relays();
|
|
||||||
|
|
||||||
// Publish messaging relay list event
|
|
||||||
let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?;
|
|
||||||
client
|
|
||||||
.send_event(&event)
|
|
||||||
.ok_timeout(Duration::from_secs(TIMEOUT))
|
|
||||||
.ack_policy(AckPolicy::none())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Write user's credentials to the system keyring
|
|
||||||
write_credential.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
// Wait for the task to complete
|
|
||||||
task.await?;
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_creating_signer(false, cx);
|
|
||||||
this.set_signer(keys, false, cx);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set whether Coop is creating a new signer
|
|
||||||
fn set_creating_signer(&mut self, creating: bool, cx: &mut Context<Self>) {
|
|
||||||
self.creating = creating;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the state of the relay list
|
|
||||||
fn set_relay_state(&mut self, state: RelayState, cx: &mut Context<Self>) {
|
|
||||||
self.relay_list_state = state;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ensure_relay_list(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let task = self.verify_relay_list(cx);
|
|
||||||
|
|
||||||
// Set the state to idle before starting the task
|
|
||||||
self.set_relay_state(RelayState::default(), cx);
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
let result = task.await?;
|
|
||||||
|
|
||||||
// Update state
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.relay_list_state = result;
|
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify relay list for current user
|
|
||||||
fn verify_relay_list(&mut self, cx: &mut Context<Self>) -> Task<Result<RelayState, Error>> {
|
|
||||||
let client = self.client();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let signer = client.signer().context("Signer not found")?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::RelayList)
|
|
||||||
.author(public_key)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Construct target for subscription
|
|
||||||
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
|
|
||||||
.into_iter()
|
|
||||||
.map(|relay| (relay, vec![filter.clone()]))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Stream events from the bootstrap relays
|
|
||||||
let mut stream = client
|
|
||||||
.stream_events(target)
|
|
||||||
.timeout(Duration::from_secs(TIMEOUT))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
while let Some((_url, res)) = stream.next().await {
|
|
||||||
match res {
|
|
||||||
Ok(event) => {
|
|
||||||
log::info!("Received relay list event: {event:?}");
|
|
||||||
return Ok(RelayState::Configured);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to receive relay list event: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(RelayState::NotConfigured)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate a direct nostr connection initiated by the client
|
|
||||||
pub fn client_connect(&self, relay: Option<RelayUrl>) -> (NostrConnect, NostrConnectUri) {
|
|
||||||
let app_keys = self.app_keys();
|
|
||||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
|
||||||
|
|
||||||
// Determine the relay will be used for Nostr Connect
|
|
||||||
let relay = match relay {
|
|
||||||
Some(relay) => relay,
|
|
||||||
None => RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate the nostr connect uri
|
|
||||||
let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME);
|
|
||||||
|
|
||||||
// Generate the nostr connect
|
|
||||||
let mut signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None).unwrap();
|
|
||||||
|
|
||||||
// Handle the auth request
|
|
||||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
|
||||||
|
|
||||||
(signer, uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store the bunker connection for the next login
|
|
||||||
pub fn persist_bunker(&mut self, uri: NostrConnectUri, cx: &mut App) {
|
|
||||||
let client = self.client();
|
|
||||||
let rng_keys = Keys::generate();
|
|
||||||
|
|
||||||
self.tasks.push(cx.background_spawn(async move {
|
|
||||||
// Construct the event for application-specific data
|
|
||||||
let event = EventBuilder::new(Kind::ApplicationSpecificData, uri.to_string())
|
|
||||||
.tag(Tag::identifier("coop:account"))
|
|
||||||
.sign(&rng_keys)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Store the event in the database
|
|
||||||
client.database().save_event(&event).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the public key of a NIP-05 address
|
/// Get the public key of a NIP-05 address
|
||||||
@@ -803,6 +877,8 @@ impl NostrRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<SignerEvent> for NostrRegistry {}
|
||||||
|
|
||||||
/// Get or create a new app keys
|
/// Get or create a new app keys
|
||||||
fn get_or_init_app_keys() -> Result<Keys, Error> {
|
fn get_or_init_app_keys() -> Result<Keys, Error> {
|
||||||
let dir = config_dir().join(".app_keys");
|
let dir = config_dir().join(".app_keys");
|
||||||
@@ -912,6 +988,16 @@ fn default_messaging_relays() -> Vec<RelayUrl> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Signer event.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum SignerEvent {
|
||||||
|
/// A new signer has been set
|
||||||
|
Set,
|
||||||
|
|
||||||
|
/// An error occurred
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
pub enum RelayState {
|
pub enum RelayState {
|
||||||
#[default]
|
#[default]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::result::Result;
|
use std::result::Result;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -16,11 +15,6 @@ pub struct CoopSigner {
|
|||||||
|
|
||||||
/// Specific signer for encryption purposes
|
/// Specific signer for encryption purposes
|
||||||
encryption_signer: RwLock<Option<Arc<dyn NostrSigner>>>,
|
encryption_signer: RwLock<Option<Arc<dyn NostrSigner>>>,
|
||||||
|
|
||||||
/// By default, Coop generates a new signer for new users.
|
|
||||||
///
|
|
||||||
/// This flag indicates whether the signer is user-owned or Coop-generated.
|
|
||||||
owned: AtomicBool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CoopSigner {
|
impl CoopSigner {
|
||||||
@@ -32,7 +26,6 @@ impl CoopSigner {
|
|||||||
signer: RwLock::new(signer.into_nostr_signer()),
|
signer: RwLock::new(signer.into_nostr_signer()),
|
||||||
signer_pkey: RwLock::new(None),
|
signer_pkey: RwLock::new(None),
|
||||||
encryption_signer: RwLock::new(None),
|
encryption_signer: RwLock::new(None),
|
||||||
owned: AtomicBool::new(false),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,17 +40,15 @@ impl CoopSigner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get public key
|
/// Get public key
|
||||||
|
///
|
||||||
|
/// Ensure to call this method after the signer has been initialized.
|
||||||
|
/// Otherwise, this method will panic.
|
||||||
pub fn public_key(&self) -> Option<PublicKey> {
|
pub fn public_key(&self) -> Option<PublicKey> {
|
||||||
self.signer_pkey.read_blocking().to_owned()
|
*self.signer_pkey.read_blocking()
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the flag indicating whether the signer is user-owned.
|
|
||||||
pub fn owned(&self) -> bool {
|
|
||||||
self.owned.load(Ordering::SeqCst)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switch the current signer to a new signer.
|
/// Switch the current signer to a new signer.
|
||||||
pub async fn switch<T>(&self, new: T, owned: bool)
|
pub async fn switch<T>(&self, new: T)
|
||||||
where
|
where
|
||||||
T: IntoNostrSigner,
|
T: IntoNostrSigner,
|
||||||
{
|
{
|
||||||
@@ -75,9 +66,6 @@ impl CoopSigner {
|
|||||||
|
|
||||||
// Reset the encryption signer
|
// Reset the encryption signer
|
||||||
*encryption_signer = None;
|
*encryption_signer = None;
|
||||||
|
|
||||||
// Update the owned flag
|
|
||||||
self.owned.store(owned, Ordering::SeqCst);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the encryption signer.
|
/// Set the encryption signer.
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges, Entity,
|
actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations,
|
||||||
EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
|
Edges, Entity, EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement,
|
||||||
Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
|
ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
|
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
|
||||||
|
|
||||||
use crate::dock_area::dock::{Dock, DockPlacement};
|
use crate::dock_area::dock::{Dock, DockPlacement};
|
||||||
use crate::dock_area::panel::{Panel, PanelEvent, PanelStyle, PanelView};
|
use crate::dock_area::panel::{Panel, PanelEvent, PanelStyle, PanelView};
|
||||||
@@ -202,19 +203,16 @@ impl DockItem {
|
|||||||
/// Returns all panel ids
|
/// Returns all panel ids
|
||||||
pub fn panel_ids(&self, cx: &App) -> Vec<SharedString> {
|
pub fn panel_ids(&self, cx: &App) -> Vec<SharedString> {
|
||||||
match self {
|
match self {
|
||||||
Self::Tabs { view, .. } => view.read(cx).panel_ids(cx),
|
|
||||||
Self::Split { items, .. } => {
|
|
||||||
let mut total = vec![];
|
|
||||||
|
|
||||||
for item in items.iter() {
|
|
||||||
if let DockItem::Tabs { view, .. } = item {
|
|
||||||
total.extend(view.read(cx).panel_ids(cx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
total
|
|
||||||
}
|
|
||||||
Self::Panel { .. } => vec![],
|
Self::Panel { .. } => vec![],
|
||||||
|
Self::Tabs { view, .. } => view.read(cx).panel_ids(cx),
|
||||||
|
Self::Split { items, .. } => items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| match item {
|
||||||
|
DockItem::Tabs { view, .. } => Some(view.read(cx).panel_ids(cx)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,6 +743,7 @@ impl EventEmitter<DockEvent> for DockArea {}
|
|||||||
impl Render for DockArea {
|
impl Render for DockArea {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let view = cx.entity().clone();
|
let view = cx.entity().clone();
|
||||||
|
let decorations = window.window_decorations();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id("dock-area")
|
.id("dock-area")
|
||||||
@@ -754,7 +753,17 @@ impl Render for DockArea {
|
|||||||
.on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds))
|
.on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds))
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if let Some(zoom_view) = self.zoom_view.clone() {
|
if let Some(zoom_view) = self.zoom_view.clone() {
|
||||||
this.child(zoom_view)
|
this.map(|this| match decorations {
|
||||||
|
Decorations::Server => this,
|
||||||
|
Decorations::Client { tiling } => this
|
||||||
|
.when(!(tiling.top || tiling.right), |div| {
|
||||||
|
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
})
|
||||||
|
.when(!(tiling.top || tiling.left), |div| {
|
||||||
|
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.child(zoom_view)
|
||||||
} else {
|
} else {
|
||||||
// render dock
|
// render dock
|
||||||
this.child(
|
this.child(
|
||||||
|
|||||||
@@ -1080,10 +1080,12 @@ impl TabPanel {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if self.panels.len() > 1 {
|
||||||
if let Some(panel) = self.active_panel(cx) {
|
if let Some(panel) = self.active_panel(cx) {
|
||||||
self.remove_panel(&panel, window, cx);
|
self.remove_panel(&panel, window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for TabPanel {
|
impl Focusable for TabPanel {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ pub enum IconName {
|
|||||||
CloseCircle,
|
CloseCircle,
|
||||||
CloseCircleFill,
|
CloseCircleFill,
|
||||||
Copy,
|
Copy,
|
||||||
|
Device,
|
||||||
Door,
|
Door,
|
||||||
Ellipsis,
|
Ellipsis,
|
||||||
Emoji,
|
Emoji,
|
||||||
@@ -52,12 +53,14 @@ pub enum IconName {
|
|||||||
Relay,
|
Relay,
|
||||||
Reply,
|
Reply,
|
||||||
Refresh,
|
Refresh,
|
||||||
|
Scan,
|
||||||
Search,
|
Search,
|
||||||
Settings,
|
Settings,
|
||||||
Settings2,
|
Settings2,
|
||||||
Sun,
|
Sun,
|
||||||
Ship,
|
Ship,
|
||||||
Shield,
|
Shield,
|
||||||
|
Group,
|
||||||
UserKey,
|
UserKey,
|
||||||
Upload,
|
Upload,
|
||||||
Usb,
|
Usb,
|
||||||
@@ -102,6 +105,7 @@ impl IconNamed for IconName {
|
|||||||
Self::CloseCircle => "icons/close-circle.svg",
|
Self::CloseCircle => "icons/close-circle.svg",
|
||||||
Self::CloseCircleFill => "icons/close-circle-fill.svg",
|
Self::CloseCircleFill => "icons/close-circle-fill.svg",
|
||||||
Self::Copy => "icons/copy.svg",
|
Self::Copy => "icons/copy.svg",
|
||||||
|
Self::Device => "icons/device.svg",
|
||||||
Self::Door => "icons/door.svg",
|
Self::Door => "icons/door.svg",
|
||||||
Self::Ellipsis => "icons/ellipsis.svg",
|
Self::Ellipsis => "icons/ellipsis.svg",
|
||||||
Self::Emoji => "icons/emoji.svg",
|
Self::Emoji => "icons/emoji.svg",
|
||||||
@@ -120,6 +124,7 @@ impl IconNamed for IconName {
|
|||||||
Self::Relay => "icons/relay.svg",
|
Self::Relay => "icons/relay.svg",
|
||||||
Self::Reply => "icons/reply.svg",
|
Self::Reply => "icons/reply.svg",
|
||||||
Self::Refresh => "icons/refresh.svg",
|
Self::Refresh => "icons/refresh.svg",
|
||||||
|
Self::Scan => "icons/scan.svg",
|
||||||
Self::Search => "icons/search.svg",
|
Self::Search => "icons/search.svg",
|
||||||
Self::Settings => "icons/settings.svg",
|
Self::Settings => "icons/settings.svg",
|
||||||
Self::Settings2 => "icons/settings2.svg",
|
Self::Settings2 => "icons/settings2.svg",
|
||||||
@@ -129,6 +134,7 @@ impl IconNamed for IconName {
|
|||||||
Self::UserKey => "icons/user-key.svg",
|
Self::UserKey => "icons/user-key.svg",
|
||||||
Self::Upload => "icons/upload.svg",
|
Self::Upload => "icons/upload.svg",
|
||||||
Self::Usb => "icons/usb.svg",
|
Self::Usb => "icons/usb.svg",
|
||||||
|
Self::Group => "icons/group.svg",
|
||||||
Self::PanelLeft => "icons/panel-left.svg",
|
Self::PanelLeft => "icons/panel-left.svg",
|
||||||
Self::PanelLeftOpen => "icons/panel-left-open.svg",
|
Self::PanelLeftOpen => "icons/panel-left-open.svg",
|
||||||
Self::PanelRight => "icons/panel-right.svg",
|
Self::PanelRight => "icons/panel-right.svg",
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ impl RenderOnce for Modal {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let window_paddings = crate::root::window_paddings(window, cx);
|
let window_paddings = crate::root::window_paddings(window, cx);
|
||||||
let radius = (cx.theme().radius_lg * 2.).min(px(20.));
|
let radius = cx.theme().radius_lg;
|
||||||
|
|
||||||
let view_size = window.viewport_size()
|
let view_size = window.viewport_size()
|
||||||
- gpui::size(
|
- gpui::size(
|
||||||
@@ -360,8 +360,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());
|
||||||
|
|||||||
@@ -249,7 +249,6 @@ impl Render for Root {
|
|||||||
div()
|
div()
|
||||||
.id("window")
|
.id("window")
|
||||||
.size_full()
|
.size_full()
|
||||||
.bg(gpui::transparent_black())
|
|
||||||
.map(|div| match decorations {
|
.map(|div| match decorations {
|
||||||
Decorations::Server => div,
|
Decorations::Server => div,
|
||||||
Decorations::Client { tiling } => div
|
Decorations::Client { tiling } => div
|
||||||
|
|||||||
Reference in New Issue
Block a user