chore: Refine the UI (#102)
* update deps * update window options * linux title bar * fix build * . * fix build * rounded corners on linux * . * . * fix i18n key * fix change subject modal * . * update new account * . * update relay modal * . * fix i18n keys --------- Co-authored-by: reya <reya@macbook.local>
This commit is contained in:
276
Cargo.lock
generated
276
Cargo.lock
generated
@@ -39,6 +39,12 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0453232ace82dee0dd0b4c87a59bd90f7b53b314f3e0f61fe2ee7c8a16482289"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.8.12"
|
version = "0.8.12"
|
||||||
@@ -864,9 +870,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.30"
|
version = "1.2.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
|
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jobserver",
|
"jobserver",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -1083,7 +1089,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1203,6 +1209,7 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"theme",
|
"theme",
|
||||||
|
"title_bar",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"ui",
|
"ui",
|
||||||
]
|
]
|
||||||
@@ -1436,9 +1443,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctor"
|
name = "ctor"
|
||||||
version = "0.4.2"
|
version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4735f265ba6a1188052ca32d461028a7d1125868be18e287e756019da7607b5"
|
checksum = "ec09e802f5081de6157da9a75701d6c713d8dc3ba52571fd4bd25f412644e8a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ctor-proc-macro",
|
"ctor-proc-macro",
|
||||||
"dtor",
|
"dtor",
|
||||||
@@ -1446,9 +1453,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctor-proc-macro"
|
name = "ctor-proc-macro"
|
||||||
version = "0.0.5"
|
version = "0.0.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d"
|
checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
@@ -1484,7 +1491,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#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1580,6 +1587,15 @@ dependencies = [
|
|||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dlv-list"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b"
|
||||||
|
dependencies = [
|
||||||
|
"rand 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "downcast-rs"
|
name = "downcast-rs"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -1630,9 +1646,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dyn-clone"
|
name = "dyn-clone"
|
||||||
version = "1.0.19"
|
version = "1.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
|
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
@@ -1649,7 +1665,7 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"memchr",
|
"memchr",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"toml 0.9.2",
|
"toml 0.9.4",
|
||||||
"vswhom",
|
"vswhom",
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
@@ -1824,6 +1840,15 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "file-locker"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6c3e69656680c6c3d76750b46dfa64bf07626bd2130c540d6cf2d306ba595a8"
|
||||||
|
dependencies = [
|
||||||
|
"nix 0.29.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filedescriptor"
|
name = "filedescriptor"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
@@ -1947,7 +1972,7 @@ checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"fontconfig-parser",
|
"fontconfig-parser",
|
||||||
"log",
|
"log",
|
||||||
"memmap2",
|
"memmap2 0.9.7",
|
||||||
"slotmap",
|
"slotmap",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
"ttf-parser 0.20.0",
|
"ttf-parser 0.20.0",
|
||||||
@@ -1961,7 +1986,7 @@ checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"fontconfig-parser",
|
"fontconfig-parser",
|
||||||
"log",
|
"log",
|
||||||
"memmap2",
|
"memmap2 0.9.7",
|
||||||
"slotmap",
|
"slotmap",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
"ttf-parser 0.25.1",
|
"ttf-parser 0.25.1",
|
||||||
@@ -2018,6 +2043,16 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "freedesktop_entry_parser"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "freetype-sys"
|
name = "freetype-sys"
|
||||||
version = "0.20.1"
|
version = "0.20.1"
|
||||||
@@ -2336,7 +2371,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2429,7 +2464,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#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2441,7 +2476,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#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2485,6 +2520,15 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
|
||||||
|
dependencies = [
|
||||||
|
"ahash 0.4.8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@@ -2663,7 +2707,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#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2672,6 +2716,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"log",
|
"log",
|
||||||
|
"parking_lot",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"url",
|
"url",
|
||||||
@@ -2681,7 +2726,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#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -3271,7 +3316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
|
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-targets 0.53.2",
|
"windows-targets 0.53.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3282,14 +3327,37 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.6"
|
version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0"
|
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linicon"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ee8c5653188a809616c97296180a0547a61dba205bcdcbdd261dbd022a25fd9"
|
||||||
|
dependencies = [
|
||||||
|
"file-locker",
|
||||||
|
"freedesktop_entry_parser",
|
||||||
|
"linicon-theme",
|
||||||
|
"memmap2 0.5.10",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linicon-theme"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4f8240c33bb08c5d8b8cdea87b683b05e61037aa76ff26bef40672cc6ecbb80"
|
||||||
|
dependencies = [
|
||||||
|
"freedesktop_entry_parser",
|
||||||
|
"rust-ini",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linkify"
|
name = "linkify"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@@ -3359,9 +3427,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru"
|
name = "lru"
|
||||||
version = "0.14.0"
|
version = "0.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198"
|
checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru-slab"
|
name = "lru-slab"
|
||||||
@@ -3459,7 +3527,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
@@ -3478,6 +3546,15 @@ version = "2.7.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memmap2"
|
||||||
|
version = "0.5.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memmap2"
|
name = "memmap2"
|
||||||
version = "0.9.7"
|
version = "0.9.7"
|
||||||
@@ -3681,8 +3758,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.42.1"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#dd8328ded8958c8c1133b293142da94c3e1d6f70"
|
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -3704,8 +3781,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.42.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#dd8328ded8958c8c1133b293142da94c3e1d6f70"
|
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3716,8 +3793,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.42.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#dd8328ded8958c8c1133b293142da94c3e1d6f70"
|
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -3727,10 +3804,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.42.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#dd8328ded8958c8c1133b293142da94c3e1d6f70"
|
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
|
"flume",
|
||||||
"heed",
|
"heed",
|
||||||
"nostr",
|
"nostr",
|
||||||
"nostr-database",
|
"nostr-database",
|
||||||
@@ -3740,8 +3818,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-relay-pool"
|
name = "nostr-relay-pool"
|
||||||
version = "0.42.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#dd8328ded8958c8c1133b293142da94c3e1d6f70"
|
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -3756,8 +3834,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.42.0"
|
version = "0.43.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#dd8328ded8958c8c1133b293142da94c3e1d6f70"
|
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4149,6 +4227,16 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-multimap"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485"
|
||||||
|
dependencies = [
|
||||||
|
"dlv-list",
|
||||||
|
"hashbrown 0.9.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-stream"
|
name = "ordered-stream"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -4666,9 +4754,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rangemap"
|
name = "rangemap"
|
||||||
version = "1.5.1"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684"
|
checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rav1e"
|
name = "rav1e"
|
||||||
@@ -4770,9 +4858,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.15"
|
version = "0.5.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec"
|
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
]
|
]
|
||||||
@@ -4811,7 +4899,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
@@ -4963,7 +5051,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#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5112,10 +5200,20 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rust-ini"
|
||||||
version = "0.1.25"
|
version = "0.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
|
checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"ordered-multimap",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-demangle"
|
||||||
|
version = "0.1.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
@@ -5166,9 +5264,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.29"
|
version = "0.23.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1"
|
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"log",
|
"log",
|
||||||
@@ -5489,7 +5587,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "semantic_version"
|
name = "semantic_version"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5544,9 +5642,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.141"
|
version = "1.0.142"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
|
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"itoa",
|
"itoa",
|
||||||
@@ -5884,7 +5982,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#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -6303,11 +6401,30 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "title_bar"
|
||||||
|
version = "1.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"common",
|
||||||
|
"gpui",
|
||||||
|
"i18n",
|
||||||
|
"linicon",
|
||||||
|
"log",
|
||||||
|
"nostr-sdk",
|
||||||
|
"rust-i18n",
|
||||||
|
"smallvec",
|
||||||
|
"smol",
|
||||||
|
"theme",
|
||||||
|
"ui",
|
||||||
|
"windows 0.61.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.46.1"
|
version = "1.47.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
|
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -6316,9 +6433,9 @@ dependencies = [
|
|||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"slab",
|
"slab",
|
||||||
"socket2 0.5.10",
|
"socket2 0.6.0",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6407,9 +6524,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.9.2"
|
version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac"
|
checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -6857,7 +6974,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "util"
|
name = "util"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
@@ -7125,13 +7242,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-backend"
|
name = "wayland-backend"
|
||||||
version = "0.3.10"
|
version = "0.3.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121"
|
checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"downcast-rs",
|
"downcast-rs",
|
||||||
"rustix 0.38.44",
|
"rustix 1.0.8",
|
||||||
"scoped-tls",
|
"scoped-tls",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"wayland-sys",
|
"wayland-sys",
|
||||||
@@ -7139,23 +7256,23 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-client"
|
name = "wayland-client"
|
||||||
version = "0.31.10"
|
version = "0.31.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61"
|
checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"rustix 0.38.44",
|
"rustix 1.0.8",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-scanner",
|
"wayland-scanner",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-cursor"
|
name = "wayland-cursor"
|
||||||
version = "0.31.10"
|
version = "0.31.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a65317158dec28d00416cb16705934070aef4f8393353d41126c54264ae0f182"
|
checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustix 0.38.44",
|
"rustix 1.0.8",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"xcursor",
|
"xcursor",
|
||||||
]
|
]
|
||||||
@@ -7187,9 +7304,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-scanner"
|
name = "wayland-scanner"
|
||||||
version = "0.31.6"
|
version = "0.31.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484"
|
checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quick-xml 0.37.5",
|
"quick-xml 0.37.5",
|
||||||
@@ -7198,9 +7315,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-sys"
|
name = "wayland-sys"
|
||||||
version = "0.31.6"
|
version = "0.31.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615"
|
checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dlib",
|
"dlib",
|
||||||
"log",
|
"log",
|
||||||
@@ -7489,7 +7606,7 @@ checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-result 0.3.4",
|
"windows-result 0.3.4",
|
||||||
"windows-strings 0.3.1",
|
"windows-strings 0.3.1",
|
||||||
"windows-targets 0.53.2",
|
"windows-targets 0.53.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7581,7 +7698,7 @@ version = "0.60.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets 0.53.2",
|
"windows-targets 0.53.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7632,10 +7749,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-targets"
|
name = "windows-targets"
|
||||||
version = "0.53.2"
|
version = "0.53.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
|
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
"windows_aarch64_gnullvm 0.53.0",
|
"windows_aarch64_gnullvm 0.53.0",
|
||||||
"windows_aarch64_msvc 0.53.0",
|
"windows_aarch64_msvc 0.53.0",
|
||||||
"windows_i686_gnu 0.53.0",
|
"windows_i686_gnu 0.53.0",
|
||||||
@@ -7946,7 +8064,7 @@ name = "xim"
|
|||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd"
|
source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash 0.8.12",
|
||||||
"hashbrown 0.14.5",
|
"hashbrown 0.14.5",
|
||||||
"log",
|
"log",
|
||||||
"x11rb",
|
"x11rb",
|
||||||
@@ -7978,7 +8096,7 @@ checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
"libc",
|
"libc",
|
||||||
"memmap2",
|
"memmap2 0.9.7",
|
||||||
"xkeysym",
|
"xkeysym",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8212,9 +8330,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zune-jpeg"
|
name = "zune-jpeg"
|
||||||
version = "0.4.19"
|
version = "0.4.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a"
|
checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zune-core",
|
"zune-core",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m20 9-6.586 6.586a2 2 0 0 1-2.828 0L4 9"/>
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m8 10 3.293 3.293a1 1 0 0 0 1.414 0L16 10"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 245 B After Width: | Height: | Size: 247 B |
@@ -1,3 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 3.75V12m0 0v8.25M12 12H3.75M12 12h8.25"/>
|
<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M12 4v8m0 0v8m0-8H4m8 0h8"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 248 B After Width: | Height: | Size: 205 B |
@@ -72,7 +72,11 @@ pub async fn nip96_upload(
|
|||||||
let json: Value = res.json().await?;
|
let json: Value = res.json().await?;
|
||||||
|
|
||||||
let config = nip96::ServerConfig::from_json(json.to_string())?;
|
let config = nip96::ServerConfig::from_json(json.to_string())?;
|
||||||
let signer = client.signer().await?;
|
let signer = if client.has_signer().await {
|
||||||
|
client.signer().await?
|
||||||
|
} else {
|
||||||
|
Keys::generate().into_nostr_signer()
|
||||||
|
};
|
||||||
|
|
||||||
let url = upload(&signer, &config, file, None).await?;
|
let url = upload(&signer, &config, file, None).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
assets = { path = "../assets" }
|
assets = { path = "../assets" }
|
||||||
ui = { path = "../ui" }
|
ui = { path = "../ui" }
|
||||||
|
title_bar = { path = "../title_bar" }
|
||||||
identity = { path = "../identity" }
|
identity = { path = "../identity" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
|
|||||||
@@ -1,33 +1,39 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Error;
|
|
||||||
use client_keys::ClientKeys;
|
use client_keys::ClientKeys;
|
||||||
use global::constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH};
|
use common::display::DisplayProfile;
|
||||||
use global::nostr_client;
|
use global::constants::DEFAULT_SIDEBAR_WIDTH;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, Action, App, AppContext, Axis, Context, Entity, InteractiveElement,
|
actions, div, px, rems, Action, App, AppContext, Axis, Context, Entity, InteractiveElement,
|
||||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window,
|
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
||||||
};
|
};
|
||||||
use i18n::t;
|
use i18n::t;
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
use registry::{Registry, RoomEmitter};
|
use registry::{Registry, RoomEmitter};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use theme::{ActiveTheme, Theme, ThemeMode};
|
use theme::{ActiveTheme, Theme, ThemeMode};
|
||||||
|
use title_bar::TitleBar;
|
||||||
use ui::actions::OpenProfile;
|
use ui::actions::OpenProfile;
|
||||||
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::dock::DockPlacement;
|
use ui::dock_area::dock::DockPlacement;
|
||||||
use ui::dock_area::panel::PanelView;
|
use ui::dock_area::panel::PanelView;
|
||||||
use ui::dock_area::{ClosePanel, DockArea, DockItem};
|
use ui::dock_area::{ClosePanel, DockArea, DockItem};
|
||||||
use ui::modal::ModalButtonProps;
|
use ui::modal::ModalButtonProps;
|
||||||
use ui::{ContextModal, IconName, Root, Sizable, StyledExt, TitleBar};
|
use ui::popup_menu::PopupMenuExt;
|
||||||
|
use ui::{h_flex, ContextModal, IconName, Root, Sizable, StyledExt};
|
||||||
|
|
||||||
|
use crate::views::compose::compose_button;
|
||||||
use crate::views::screening::Screening;
|
use crate::views::screening::Screening;
|
||||||
use crate::views::user_profile::UserProfile;
|
use crate::views::user_profile::UserProfile;
|
||||||
use crate::views::{
|
use crate::views::{
|
||||||
chat, login, new_account, onboarding, preferences, sidebar, startup, user_profile, welcome,
|
backup_keys, chat, login, messaging_relays, new_account, onboarding, preferences, sidebar,
|
||||||
|
startup, user_profile, welcome,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
||||||
@@ -44,6 +50,8 @@ pub fn new_account(window: &mut Window, cx: &mut App) {
|
|||||||
ChatSpace::set_center_panel(panel, window, cx);
|
ChatSpace::set_center_panel(panel, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actions!(user, [DarkMode, Settings, Logout]);
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||||
pub enum PanelKind {
|
pub enum PanelKind {
|
||||||
Room(u64),
|
Room(u64),
|
||||||
@@ -70,14 +78,15 @@ pub struct ToggleModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChatSpace {
|
pub struct ChatSpace {
|
||||||
|
title_bar: Entity<TitleBar>,
|
||||||
dock: Entity<DockArea>,
|
dock: Entity<DockArea>,
|
||||||
toolbar: bool,
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
subscriptions: SmallVec<[Subscription; 5]>,
|
subscriptions: SmallVec<[Subscription; 5]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatSpace {
|
impl ChatSpace {
|
||||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
|
let title_bar = cx.new(|_| TitleBar::new());
|
||||||
let dock = cx.new(|cx| {
|
let dock = cx.new(|cx| {
|
||||||
let panel = Arc::new(startup::init(window, cx));
|
let panel = Arc::new(startup::init(window, cx));
|
||||||
let center = DockItem::panel(panel);
|
let center = DockItem::panel(panel);
|
||||||
@@ -99,14 +108,17 @@ impl ChatSpace {
|
|||||||
window,
|
window,
|
||||||
|_this: &mut Self, state, window, cx| {
|
|_this: &mut Self, state, window, cx| {
|
||||||
if !state.read(cx).has_keys() {
|
if !state.read(cx).has_keys() {
|
||||||
window.open_modal(cx, |this, _window, cx| {
|
let title = SharedString::new(t!("startup.client_keys_warning"));
|
||||||
|
let desc = SharedString::new(t!("startup.client_keys_desc"));
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _window, cx| {
|
||||||
this.overlay_closable(false)
|
this.overlay_closable(false)
|
||||||
.show_close(false)
|
.show_close(false)
|
||||||
.keyboard(false)
|
.keyboard(false)
|
||||||
.confirm()
|
.confirm()
|
||||||
.button_props(
|
.button_props(
|
||||||
ModalButtonProps::default()
|
ModalButtonProps::default()
|
||||||
.cancel_text(t!("chatspace.create_new_keys"))
|
.cancel_text(t!("startup.create_new_keys"))
|
||||||
.ok_text(t!("common.allow")),
|
.ok_text(t!("common.allow")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -124,13 +136,9 @@ impl ChatSpace {
|
|||||||
div()
|
div()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::new(t!("chatspace.warning"))),
|
.child(title.clone()),
|
||||||
)
|
)
|
||||||
.child(div().line_height(relative(1.4)).child(
|
.child(desc.clone()),
|
||||||
SharedString::new(t!(
|
|
||||||
"chatspace.allow_keychain_access"
|
|
||||||
)),
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.on_cancel(|_, _window, cx| {
|
.on_cancel(|_, _window, cx| {
|
||||||
ClientKeys::global(cx).update(cx, |this, cx| {
|
ClientKeys::global(cx).update(cx, |this, cx| {
|
||||||
@@ -157,21 +165,21 @@ impl ChatSpace {
|
|||||||
window,
|
window,
|
||||||
|this: &mut Self, state, window, cx| {
|
|this: &mut Self, state, window, cx| {
|
||||||
if !state.read(cx).has_signer() {
|
if !state.read(cx).has_signer() {
|
||||||
this.open_onboarding(window, cx);
|
this.set_onboarding_panels(window, cx);
|
||||||
} else {
|
} else {
|
||||||
this.open_chats(window, cx);
|
this.set_chat_panels(window, cx);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
// Automatically run on_load function when UserProfile is created
|
// Automatically run load function when UserProfile is created
|
||||||
subscriptions.push(cx.observe_new::<UserProfile>(|this, window, cx| {
|
subscriptions.push(cx.observe_new::<UserProfile>(|this, window, cx| {
|
||||||
if let Some(window) = window {
|
if let Some(window) = window {
|
||||||
this.on_load(window, cx);
|
this.load(window, cx);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Automatically run on_load function when Screening is created
|
// Automatically run load function when Screening is created
|
||||||
subscriptions.push(cx.observe_new::<Screening>(|this, window, cx| {
|
subscriptions.push(cx.observe_new::<Screening>(|this, window, cx| {
|
||||||
if let Some(window) = window {
|
if let Some(window) = window {
|
||||||
this.load(window, cx);
|
this.load(window, cx);
|
||||||
@@ -196,7 +204,7 @@ impl ChatSpace {
|
|||||||
this.add_panel(panel, DockPlacement::Center, window, cx);
|
this.add_panel(panel, DockPlacement::Center, window, cx);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
window.push_notification(t!("chatspace.failed_to_open_room"), cx);
|
window.push_notification(t!("common.room_error"), cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RoomEmitter::Close(..) => {
|
RoomEmitter::Close(..) => {
|
||||||
@@ -216,16 +224,13 @@ impl ChatSpace {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
dock,
|
dock,
|
||||||
|
title_bar,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
toolbar: false,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_onboarding_panels(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
// No active user, disable user's toolbar
|
|
||||||
self.toolbar(false, cx);
|
|
||||||
|
|
||||||
let panel = Arc::new(onboarding::init(window, cx));
|
let panel = Arc::new(onboarding::init(window, cx));
|
||||||
let center = DockItem::panel(panel);
|
let center = DockItem::panel(panel);
|
||||||
|
|
||||||
@@ -235,17 +240,14 @@ impl ChatSpace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_chats(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_chat_panels(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
// Enable the toolbar for logged in users
|
let registry = Registry::global(cx);
|
||||||
self.toolbar(true, cx);
|
|
||||||
|
|
||||||
// Load all chat rooms from database
|
|
||||||
Registry::global(cx).update(cx, |this, cx| {
|
|
||||||
this.load_rooms(window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
let weak_dock = self.dock.downgrade();
|
let weak_dock = self.dock.downgrade();
|
||||||
|
|
||||||
|
// The left panel will render sidebar
|
||||||
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
|
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
|
||||||
|
|
||||||
|
// The center panel will render chat rooms (as tabs)
|
||||||
let center = DockItem::split_with_sizes(
|
let center = DockItem::split_with_sizes(
|
||||||
Axis::Vertical,
|
Axis::Vertical,
|
||||||
vec![DockItem::tabs(
|
vec![DockItem::tabs(
|
||||||
@@ -261,66 +263,31 @@ impl ChatSpace {
|
|||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update dock
|
||||||
self.dock.update(cx, |this, cx| {
|
self.dock.update(cx, |this, cx| {
|
||||||
this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx);
|
this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx);
|
||||||
this.set_center(center, window, cx);
|
this.set_center(center, window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.defer_in(window, |this, window, cx| {
|
// Load all chat rooms from the database
|
||||||
let verify_messaging_relays = this.verify_messaging_relays(cx);
|
registry.update(cx, |this, cx| {
|
||||||
|
this.load_rooms(window, cx);
|
||||||
cx.spawn_in(window, async move |_, cx| {
|
|
||||||
if let Ok(status) = verify_messaging_relays.await {
|
|
||||||
if !status {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
window.dispatch_action(
|
|
||||||
Box::new(ToggleModal {
|
|
||||||
modal: ModalKind::SetupRelay,
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_settings(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn on_settings(&mut self, _ev: &Settings, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let settings = preferences::init(window, cx);
|
let view = preferences::init(window, cx);
|
||||||
let title = SharedString::new(t!("chatspace.preferences_title"));
|
let title = SharedString::new(t!("common.preferences"));
|
||||||
|
|
||||||
window.open_modal(cx, move |modal, _, _| {
|
window.open_modal(cx, move |modal, _, _| {
|
||||||
modal
|
modal
|
||||||
.title(title.clone())
|
.title(title.clone())
|
||||||
.width(px(DEFAULT_MODAL_WIDTH))
|
.width(px(480.))
|
||||||
.child(settings.clone())
|
.child(view.clone())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toolbar(&mut self, status: bool, cx: &mut Context<Self>) {
|
fn on_dark_mode(&mut self, _ev: &DarkMode, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.toolbar = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_messaging_relays(&self, cx: &App) -> Task<Result<bool, Error>> {
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let client = nostr_client();
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::InboxRelays)
|
|
||||||
.author(public_key)
|
|
||||||
.limit(1);
|
|
||||||
let is_exist = client.database().query(filter).await?.first().is_some();
|
|
||||||
|
|
||||||
Ok(is_exist)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle_appearance(&self, window: &mut Window, cx: &mut App) {
|
|
||||||
if cx.theme().mode.is_dark() {
|
if cx.theme().mode.is_dark() {
|
||||||
Theme::change(ThemeMode::Light, Some(window), cx);
|
Theme::change(ThemeMode::Light, Some(window), cx);
|
||||||
} else {
|
} else {
|
||||||
@@ -328,23 +295,80 @@ impl ChatSpace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn logout(&self, window: &mut Window, cx: &mut App) {
|
fn on_sign_out(&mut self, _ev: &Logout, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
Identity::global(cx).update(cx, |this, cx| {
|
let identity = Identity::global(cx);
|
||||||
|
// TODO: save current session?
|
||||||
|
identity.update(cx, |this, cx| {
|
||||||
this.unload(window, cx);
|
this.unload(window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_open_profile(&mut self, a: &OpenProfile, window: &mut Window, cx: &mut Context<Self>) {
|
fn on_open_profile(&mut self, ev: &OpenProfile, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let public_key = a.0;
|
let public_key = ev.0;
|
||||||
let profile = user_profile::init(public_key, window, cx);
|
let profile = user_profile::init(public_key, window, cx);
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, _cx| {
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
// user_profile::init(public_key, window, cx)
|
this.alert()
|
||||||
this.child(profile.clone())
|
.show_close(true)
|
||||||
|
.overlay_closable(true)
|
||||||
|
.child(profile.clone())
|
||||||
|
.button_props(ModalButtonProps::default().ok_text(t!("profile.njump")))
|
||||||
|
.on_ok(move |_, _window, cx| {
|
||||||
|
let Ok(bech32) = public_key.to_bech32();
|
||||||
|
cx.open_url(&format!("https://njump.me/{bech32}"));
|
||||||
|
false
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_center_panel<P: PanelView>(panel: P, window: &mut Window, cx: &mut App) {
|
fn render_titlebar_left_side(
|
||||||
|
&mut self,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &Context<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let compose_button = compose_button().into_any_element();
|
||||||
|
|
||||||
|
h_flex().gap_1().child(compose_button)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_titlebar_right_side(
|
||||||
|
&mut self,
|
||||||
|
profile: &Profile,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &Context<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||||
|
let need_backup = Identity::read_global(cx).need_backup();
|
||||||
|
let relay_ready = Identity::read_global(cx).relay_ready();
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.when_some(relay_ready, |this, status| {
|
||||||
|
this.when(!status, |this| this.child(messaging_relays::relay_button()))
|
||||||
|
})
|
||||||
|
.when_some(need_backup, |this, keys| {
|
||||||
|
this.child(backup_keys::backup_button(keys.to_owned()))
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
Button::new("user")
|
||||||
|
.small()
|
||||||
|
.reverse()
|
||||||
|
.transparent()
|
||||||
|
.icon(IconName::CaretDown)
|
||||||
|
.child(Avatar::new(profile.avatar_url(proxy)).size(rems(1.49)))
|
||||||
|
.popup_menu(|this, _window, _cx| {
|
||||||
|
this.menu(t!("user.dark_mode"), Box::new(DarkMode))
|
||||||
|
.menu(t!("user.settings"), Box::new(Settings))
|
||||||
|
.separator()
|
||||||
|
.menu(t!("user.sign_out"), Box::new(Logout))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_center_panel<P>(panel: P, window: &mut Window, cx: &mut App)
|
||||||
|
where
|
||||||
|
P: PanelView,
|
||||||
|
{
|
||||||
if let Some(Some(root)) = window.root::<Root>() {
|
if let Some(Some(root)) = window.root::<Root>() {
|
||||||
if let Ok(chatspace) = root.read(cx).view().clone().downcast::<ChatSpace>() {
|
if let Ok(chatspace) = root.read(cx).view().clone().downcast::<ChatSpace>() {
|
||||||
let panel = Arc::new(panel);
|
let panel = Arc::new(panel);
|
||||||
@@ -365,7 +389,27 @@ impl Render for ChatSpace {
|
|||||||
let modal_layer = Root::render_modal_layer(window, cx);
|
let modal_layer = Root::render_modal_layer(window, cx);
|
||||||
let notification_layer = Root::render_notification_layer(window, cx);
|
let notification_layer = Root::render_notification_layer(window, cx);
|
||||||
|
|
||||||
|
// Only render titlebar element if user is logged in
|
||||||
|
if let Some(identity) = Identity::read_global(cx).public_key() {
|
||||||
|
let profile = Registry::read_global(cx).get_person(&identity, cx);
|
||||||
|
|
||||||
|
let left_side = self
|
||||||
|
.render_titlebar_left_side(window, cx)
|
||||||
|
.into_any_element();
|
||||||
|
|
||||||
|
let right_side = self
|
||||||
|
.render_titlebar_right_side(&profile, window, cx)
|
||||||
|
.into_any_element();
|
||||||
|
|
||||||
|
self.title_bar.update(cx, |this, _cx| {
|
||||||
|
this.set_children(vec![left_side, right_side]);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
div()
|
div()
|
||||||
|
.on_action(cx.listener(Self::on_settings))
|
||||||
|
.on_action(cx.listener(Self::on_dark_mode))
|
||||||
|
.on_action(cx.listener(Self::on_sign_out))
|
||||||
.on_action(cx.listener(Self::on_open_profile))
|
.on_action(cx.listener(Self::on_open_profile))
|
||||||
.relative()
|
.relative()
|
||||||
.size_full()
|
.size_full()
|
||||||
@@ -375,58 +419,7 @@ impl Render for ChatSpace {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.size_full()
|
.size_full()
|
||||||
// Title Bar
|
// Title Bar
|
||||||
.child(
|
.child(self.title_bar.clone())
|
||||||
TitleBar::new()
|
|
||||||
// Left side
|
|
||||||
.child(div())
|
|
||||||
// Right side
|
|
||||||
.when(self.toolbar, |this| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_end()
|
|
||||||
.gap_1p5()
|
|
||||||
.px_2()
|
|
||||||
.child(
|
|
||||||
Button::new("appearance")
|
|
||||||
.tooltip(t!("chatspace.appearance_tooltip"))
|
|
||||||
.small()
|
|
||||||
.ghost()
|
|
||||||
.map(|this| {
|
|
||||||
if cx.theme().mode.is_dark() {
|
|
||||||
this.icon(IconName::Sun)
|
|
||||||
} else {
|
|
||||||
this.icon(IconName::Moon)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
|
||||||
this.toggle_appearance(window, cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("preferences")
|
|
||||||
.tooltip(t!("chatspace.preferences_tooltip"))
|
|
||||||
.small()
|
|
||||||
.ghost()
|
|
||||||
.icon(IconName::Settings)
|
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
|
||||||
this.open_settings(window, cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("logout")
|
|
||||||
.tooltip(t!("common.logout"))
|
|
||||||
.small()
|
|
||||||
.ghost()
|
|
||||||
.icon(IconName::Logout)
|
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
|
||||||
this.logout(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
// Dock
|
// Dock
|
||||||
.child(self.dock.clone()),
|
.child(self.dock.clone()),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,21 +5,16 @@ use std::time::Duration;
|
|||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use auto_update::AutoUpdater;
|
use auto_update::AutoUpdater;
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
use global::constants::APP_NAME;
|
|
||||||
use global::constants::{
|
use global::constants::{
|
||||||
ALL_MESSAGES_SUB_ID, APP_ID, APP_PUBKEY, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT,
|
ALL_MESSAGES_SUB_ID, APP_ID, APP_NAME, APP_PUBKEY, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT,
|
||||||
METADATA_BATCH_TIMEOUT, NEW_MESSAGE_SUB_ID, SEARCH_RELAYS,
|
METADATA_BATCH_TIMEOUT, NEW_MESSAGE_SUB_ID, SEARCH_RELAYS,
|
||||||
};
|
};
|
||||||
use global::{nostr_client, NostrSignal};
|
use global::{nostr_client, NostrSignal};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
|
actions, point, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
|
||||||
WindowBounds, WindowKind, WindowOptions,
|
SharedString, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations,
|
||||||
|
WindowKind, WindowOptions,
|
||||||
};
|
};
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
use gpui::{point, SharedString, TitlebarOptions};
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use gpui::{WindowBackgroundAppearance, WindowDecorations};
|
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use registry::Registry;
|
use registry::Registry;
|
||||||
@@ -138,7 +133,7 @@ fn main() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let duration = smol::Timer::after(Duration::from_secs(75));
|
let duration = smol::Timer::after(Duration::from_secs(30));
|
||||||
|
|
||||||
let recv = || async {
|
let recv = || async {
|
||||||
// prevent inline format
|
// prevent inline format
|
||||||
@@ -202,25 +197,22 @@ fn main() {
|
|||||||
items: vec![MenuItem::action("Quit", Quit)],
|
items: vec![MenuItem::action("Quit", Quit)],
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
// Set up the window bounds
|
||||||
|
let bounds = Bounds::centered(None, size(px(920.0), px(700.0)), cx);
|
||||||
|
|
||||||
// Set up the window options
|
// Set up the window options
|
||||||
let opts = WindowOptions {
|
let opts = WindowOptions {
|
||||||
#[cfg(not(target_os = "linux"))]
|
window_background: WindowBackgroundAppearance::Opaque,
|
||||||
|
window_decorations: Some(WindowDecorations::Client),
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
|
window_min_size: Some(size(px(800.0), px(600.0))),
|
||||||
|
kind: WindowKind::Normal,
|
||||||
|
app_id: Some(APP_ID.to_owned()),
|
||||||
titlebar: Some(TitlebarOptions {
|
titlebar: Some(TitlebarOptions {
|
||||||
title: Some(SharedString::new_static(APP_NAME)),
|
title: Some(SharedString::new_static(APP_NAME)),
|
||||||
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
||||||
appears_transparent: true,
|
appears_transparent: true,
|
||||||
}),
|
}),
|
||||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
|
||||||
None,
|
|
||||||
size(px(920.0), px(700.0)),
|
|
||||||
cx,
|
|
||||||
))),
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
window_background: WindowBackgroundAppearance::Transparent,
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
window_decorations: Some(WindowDecorations::Client),
|
|
||||||
kind: WindowKind::Normal,
|
|
||||||
app_id: Some(APP_ID.to_owned()),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
258
crates/coop/src/views/backup_keys.rs
Normal file
258
crates/coop/src/views/backup_keys.rs
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use dirs::document_dir;
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, AppContext, ClipboardItem, Context, Entity, Flatten, IntoElement, ParentElement, Render,
|
||||||
|
SharedString, Styled, Window,
|
||||||
|
};
|
||||||
|
use i18n::{shared_t, t};
|
||||||
|
use identity::Identity;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||||
|
use ui::input::{InputState, TextInput};
|
||||||
|
use ui::modal::ModalButtonProps;
|
||||||
|
use ui::{divider, h_flex, v_flex, ContextModal, Disableable, IconName, Sizable};
|
||||||
|
|
||||||
|
pub fn backup_button(keys: Keys) -> impl IntoElement {
|
||||||
|
div().child(
|
||||||
|
Button::new("backup")
|
||||||
|
.icon(IconName::Info)
|
||||||
|
.label(t!("new_account.backup_label"))
|
||||||
|
.danger()
|
||||||
|
.xsmall()
|
||||||
|
.rounded(ButtonRounded::Full)
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
let title = SharedString::new(t!("new_account.backup_label"));
|
||||||
|
let keys = keys.clone();
|
||||||
|
let view = cx.new(|cx| BackupKeys::new(&keys, window, cx));
|
||||||
|
let weak_view = view.downgrade();
|
||||||
|
|
||||||
|
window.open_modal(cx, move |modal, _window, _cx| {
|
||||||
|
let weak_view = weak_view.clone();
|
||||||
|
|
||||||
|
modal
|
||||||
|
.confirm()
|
||||||
|
.title(title.clone())
|
||||||
|
.child(view.clone())
|
||||||
|
.button_props(
|
||||||
|
ModalButtonProps::default()
|
||||||
|
.cancel_text(t!("new_account.backup_skip"))
|
||||||
|
.ok_text(t!("new_account.backup_download")),
|
||||||
|
)
|
||||||
|
.on_ok(move |_, window, cx| {
|
||||||
|
weak_view
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.download(window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
// true to close the modal
|
||||||
|
false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BackupKeys {
|
||||||
|
password: Entity<InputState>,
|
||||||
|
pubkey_input: Entity<InputState>,
|
||||||
|
secret_input: Entity<InputState>,
|
||||||
|
error: Option<SharedString>,
|
||||||
|
copied: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackupKeys {
|
||||||
|
pub fn new(keys: &Keys, window: &mut Window, cx: &mut Context<'_, Self>) -> Self {
|
||||||
|
let Ok(npub) = keys.public_key.to_bech32();
|
||||||
|
let Ok(nsec) = keys.secret_key().to_bech32();
|
||||||
|
|
||||||
|
let password = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
|
|
||||||
|
let pubkey_input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx)
|
||||||
|
.disabled(true)
|
||||||
|
.default_value(npub)
|
||||||
|
});
|
||||||
|
|
||||||
|
let secret_input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx)
|
||||||
|
.disabled(true)
|
||||||
|
.default_value(nsec)
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
password,
|
||||||
|
pubkey_input,
|
||||||
|
secret_input,
|
||||||
|
error: None,
|
||||||
|
copied: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_secret(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let item = ClipboardItem::new_string(self.secret_input.read(cx).value().to_string());
|
||||||
|
cx.write_to_clipboard(item);
|
||||||
|
|
||||||
|
self.set_copied(true, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_copied(&mut self, status: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.copied = status;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
// Reset the copied state after a delay
|
||||||
|
if status {
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_copied(false, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<E>(&mut self, error: E, window: &mut Window, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
E: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.error = Some(error.into());
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
// Clear the error message after a delay
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.error = None;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let document_dir = document_dir().expect("Failed to get document directory");
|
||||||
|
let password = self.password.read(cx).value().to_string();
|
||||||
|
|
||||||
|
if password.is_empty() {
|
||||||
|
self.set_error(t!("login.password_is_required"), window, cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = cx.prompt_for_new_path(&document_dir);
|
||||||
|
let nsec = self.secret_input.read(cx).value().to_string();
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match Flatten::flatten(path.await.map_err(|e| e.into())) {
|
||||||
|
Ok(Ok(Some(path))) => {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
match fs::write(&path, nsec) {
|
||||||
|
Ok(_) => {
|
||||||
|
Identity::global(cx).update(cx, |this, cx| {
|
||||||
|
this.clear_need_backup(password, cx);
|
||||||
|
});
|
||||||
|
// Close the current modal
|
||||||
|
window.close_modal(cx);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_error(e.to_string(), window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::error!("Failed to save backup keys");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for BackupKeys {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.gap_3()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(shared_t!("new_account.backup_description")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(shared_t!("common.pubkey"))
|
||||||
|
.child(TextInput::new(&self.pubkey_input).small())
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(shared_t!("new_account.backup_pubkey_note")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(divider(cx))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(shared_t!("common.secret"))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(TextInput::new(&self.secret_input).small())
|
||||||
|
.child(
|
||||||
|
Button::new("copy")
|
||||||
|
.icon({
|
||||||
|
if self.copied {
|
||||||
|
IconName::CheckCircleFill
|
||||||
|
} else {
|
||||||
|
IconName::Copy
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ghost()
|
||||||
|
.disabled(self.copied)
|
||||||
|
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||||
|
this.copy_secret(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(shared_t!("new_account.backup_secret_note")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(divider(cx))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(shared_t!("login.set_password"))
|
||||||
|
.child(TextInput::new(&self.password).small())
|
||||||
|
.when_some(self.error.as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.italic()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().danger_foreground)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ use ui::button::{Button, ButtonVariants};
|
|||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::emoji_picker::EmojiPicker;
|
use ui::emoji_picker::EmojiPicker;
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
|
use ui::modal::ModalButtonProps;
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::popup_menu::PopupMenu;
|
use ui::popup_menu::PopupMenu;
|
||||||
use ui::text::RichText;
|
use ui::text::RichText;
|
||||||
@@ -813,7 +814,7 @@ impl Panel for Chat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn toolbar_buttons(&self, _window: &Window, cx: &App) -> Vec<Button> {
|
fn toolbar_buttons(&self, _window: &Window, cx: &App) -> Vec<Button> {
|
||||||
let id = self.room.read(cx).id;
|
let room = self.room.downgrade();
|
||||||
let subject = self
|
let subject = self
|
||||||
.room
|
.room
|
||||||
.read(cx)
|
.read(cx)
|
||||||
@@ -825,11 +826,31 @@ impl Panel for Chat {
|
|||||||
.icon(IconName::EditFill)
|
.icon(IconName::EditFill)
|
||||||
.tooltip(t!("chat.change_subject_button"))
|
.tooltip(t!("chat.change_subject_button"))
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
let subject = subject::init(id, subject.clone(), window, cx);
|
let view = subject::init(subject.clone(), window, cx);
|
||||||
|
let room = room.clone();
|
||||||
|
let weak_view = view.downgrade();
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, _cx| {
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
this.title(SharedString::new(t!("chat.change_subject_modal_title")))
|
let room = room.clone();
|
||||||
.child(subject.clone())
|
let weak_view = weak_view.clone();
|
||||||
|
|
||||||
|
this.confirm()
|
||||||
|
.title(SharedString::new(t!("chat.change_subject_modal_title")))
|
||||||
|
.child(view.clone())
|
||||||
|
.button_props(ModalButtonProps::default().ok_text(t!("common.change")))
|
||||||
|
.on_ok(move |_, _window, cx| {
|
||||||
|
if let Ok(subject) =
|
||||||
|
weak_view.read_with(cx, |this, cx| this.new_subject(cx))
|
||||||
|
{
|
||||||
|
room.update(cx, |this, cx| {
|
||||||
|
this.subject = Some(subject);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
// true to close the modal
|
||||||
|
true
|
||||||
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -891,8 +912,9 @@ impl Render for Chat {
|
|||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(
|
.child(
|
||||||
Button::new("upload")
|
Button::new("upload")
|
||||||
.icon(Icon::new(IconName::Upload))
|
.icon(IconName::Upload)
|
||||||
.ghost()
|
.ghost()
|
||||||
|
.large()
|
||||||
.disabled(self.uploading)
|
.disabled(self.uploading)
|
||||||
.loading(self.uploading)
|
.loading(self.uploading)
|
||||||
.on_click(cx.listener(
|
.on_click(cx.listener(
|
||||||
@@ -903,7 +925,8 @@ impl Render for Chat {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
EmojiPicker::new(self.input.downgrade())
|
EmojiPicker::new(self.input.downgrade())
|
||||||
.icon(IconName::EmojiFill),
|
.icon(IconName::EmojiFill)
|
||||||
|
.large(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(TextInput::new(&self.input)),
|
.child(TextInput::new(&self.input)),
|
||||||
|
|||||||
@@ -3,92 +3,53 @@ use gpui::{
|
|||||||
Styled, Window,
|
Styled, Window,
|
||||||
};
|
};
|
||||||
use i18n::t;
|
use i18n::t;
|
||||||
use registry::Registry;
|
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::input::{InputState, TextInput};
|
use ui::input::{InputState, TextInput};
|
||||||
use ui::{ContextModal, Sizable};
|
use ui::{v_flex, Sizable};
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(subject: Option<String>, window: &mut Window, cx: &mut App) -> Entity<Subject> {
|
||||||
id: u64,
|
Subject::new(subject, window, cx)
|
||||||
subject: Option<String>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Entity<Subject> {
|
|
||||||
Subject::new(id, subject, window, cx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Subject {
|
pub struct Subject {
|
||||||
id: u64,
|
|
||||||
input: Entity<InputState>,
|
input: Entity<InputState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Subject {
|
impl Subject {
|
||||||
pub fn new(
|
pub fn new(subject: Option<String>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
id: u64,
|
|
||||||
subject: Option<String>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Entity<Self> {
|
|
||||||
let input = cx.new(|cx| {
|
let input = cx.new(|cx| {
|
||||||
let mut this = InputState::new(window, cx).placeholder(t!("subject.placeholder"));
|
let mut this = InputState::new(window, cx).placeholder(t!("subject.placeholder"));
|
||||||
if let Some(text) = subject.clone() {
|
if let Some(text) = subject.as_ref() {
|
||||||
this.set_value(text, window, cx);
|
this.set_value(text, window, cx);
|
||||||
}
|
}
|
||||||
this
|
this
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.new(|_| Self { id, input })
|
cx.new(|_| Self { input })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn new_subject(&self, cx: &App) -> SharedString {
|
||||||
let registry = Registry::global(cx).read(cx);
|
self.input.read(cx).value().clone()
|
||||||
let subject = self.input.read(cx).value().clone();
|
|
||||||
|
|
||||||
if let Some(room) = registry.room(&self.id, cx) {
|
|
||||||
room.update(cx, |this, cx| {
|
|
||||||
this.subject = Some(subject);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
window.close_modal(cx);
|
|
||||||
} else {
|
|
||||||
window.push_notification(SharedString::new(t!("subject.room_not_found")), cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for Subject {
|
impl Render for Subject {
|
||||||
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 {
|
||||||
div()
|
v_flex()
|
||||||
.flex()
|
.gap_1()
|
||||||
.flex_col()
|
|
||||||
.gap_3()
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.text_sm()
|
||||||
.flex_col()
|
.text_color(cx.theme().text_muted)
|
||||||
.gap_1()
|
.child(SharedString::new(t!("subject.title"))),
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::new(t!("subject.title"))),
|
|
||||||
)
|
|
||||||
.child(TextInput::new(&self.input).small())
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.italic()
|
|
||||||
.text_color(cx.theme().text_placeholder)
|
|
||||||
.child(SharedString::new(t!("subject.help_text"))),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
.child(TextInput::new(&self.input).small())
|
||||||
.child(
|
.child(
|
||||||
Button::new("submit")
|
div()
|
||||||
.label(t!("common.change"))
|
.text_xs()
|
||||||
.primary()
|
.italic()
|
||||||
.w_full()
|
.text_color(cx.theme().text_placeholder)
|
||||||
.on_click(cx.listener(|this, _, window, cx| this.update(window, cx))),
|
.child(SharedString::new(t!("subject.help_text"))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ use global::constants::BOOTSTRAP_RELAYS;
|
|||||||
use global::nostr_client;
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, px, red, relative, uniform_list, App, AppContext, Context, Entity,
|
div, px, relative, rems, uniform_list, AppContext, Context, Entity, InteractiveElement,
|
||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled,
|
||||||
StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, Window,
|
Subscription, Task, Window,
|
||||||
};
|
};
|
||||||
use i18n::t;
|
use i18n::t;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@@ -21,13 +21,29 @@ use settings::AppSettings;
|
|||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::avatar::Avatar;
|
||||||
|
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::{v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
use ui::{h_flex, v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
|
pub fn compose_button() -> impl IntoElement {
|
||||||
cx.new(|cx| Compose::new(window, cx))
|
div().child(
|
||||||
|
Button::new("compose")
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.ghost_alt()
|
||||||
|
.cta()
|
||||||
|
.small()
|
||||||
|
.rounded(ButtonRounded::Full)
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
let compose = cx.new(|cx| Compose::new(window, cx));
|
||||||
|
let title = SharedString::new(t!("sidebar.direct_messages"));
|
||||||
|
|
||||||
|
window.open_modal(cx, move |modal, _window, _cx| {
|
||||||
|
modal.title(title.clone()).child(compose.clone())
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -147,13 +163,13 @@ impl Compose {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let public_keys: Vec<PublicKey> = self.selected(cx);
|
let public_keys: Vec<PublicKey> = self.selected(cx);
|
||||||
|
|
||||||
if public_keys.is_empty() {
|
if public_keys.is_empty() {
|
||||||
self.set_error(Some(t!("compose.receiver_required").into()), cx);
|
self.set_error(Some(t!("compose.receiver_required").into()), cx);
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
|
||||||
// Show loading spinner
|
// Show loading spinner
|
||||||
self.set_submitting(true, cx);
|
self.set_submitting(true, cx);
|
||||||
@@ -169,7 +185,7 @@ impl Compose {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let event: Task<Result<Room, anyhow::Error>> = cx.background_spawn(async move {
|
let event: Task<Result<Room, Error>> = cx.background_spawn(async move {
|
||||||
let signer = nostr_client().signer().await?;
|
let signer = nostr_client().signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
@@ -187,15 +203,17 @@ impl Compose {
|
|||||||
match event.await {
|
match event.await {
|
||||||
Ok(room) => {
|
Ok(room) => {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
|
let registry = Registry::global(cx);
|
||||||
|
// Reset local state
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_submitting(false, cx);
|
this.set_submitting(false, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
// Create and insert the new room into the registry
|
||||||
Registry::global(cx).update(cx, |this, cx| {
|
registry.update(cx, |this, cx| {
|
||||||
this.push_room(cx.new(|_| room), cx);
|
this.push_room(cx.new(|_| room), cx);
|
||||||
});
|
});
|
||||||
|
// Close the current modal
|
||||||
window.close_modal(cx);
|
window.close_modal(cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
@@ -371,22 +389,20 @@ impl Compose {
|
|||||||
let selected = entity.read(cx).select;
|
let selected = entity.read(cx).select;
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
div()
|
h_flex()
|
||||||
.id(ix)
|
.id(ix)
|
||||||
|
.px_1()
|
||||||
|
.h_9()
|
||||||
.w_full()
|
.w_full()
|
||||||
.h_11()
|
|
||||||
.py_1()
|
|
||||||
.px_3()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_between()
|
.justify_between()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_3()
|
.gap_1p5()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(img(profile.avatar_url(proxy)).size_7().flex_shrink_0())
|
.child(Avatar::new(profile.avatar_url(proxy)).size(rems(1.75)))
|
||||||
.child(profile.display_name()),
|
.child(profile.display_name()),
|
||||||
)
|
)
|
||||||
.when(selected, |this| {
|
.when(selected, |this| {
|
||||||
@@ -414,51 +430,52 @@ impl Render for Compose {
|
|||||||
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 label = if self.submitting {
|
let label = if self.submitting {
|
||||||
t!("compose.creating_dm_button")
|
t!("compose.creating_dm_button")
|
||||||
} else if self.contacts.len() > 1 {
|
} else if self.selected(cx).len() > 1 {
|
||||||
t!("compose.create_group_dm_button")
|
t!("compose.create_group_dm_button")
|
||||||
} else {
|
} else {
|
||||||
t!("compose.create_dm_button")
|
t!("compose.create_dm_button")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let error = self.error_message.read(cx).as_ref();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.mb_4()
|
||||||
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::new(t!("compose.description"))),
|
.child(SharedString::new(t!("compose.description"))),
|
||||||
)
|
)
|
||||||
.when_some(self.error_message.read(cx).as_ref(), |this, msg| {
|
.when_some(error, |this, msg| {
|
||||||
this.child(div().text_xs().text_color(red()).child(msg.clone()))
|
this.child(
|
||||||
|
div()
|
||||||
|
.italic()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().danger_foreground)
|
||||||
|
.child(msg.clone()),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
div().flex().flex_col().child(
|
h_flex()
|
||||||
div()
|
.gap_1()
|
||||||
.h_10()
|
.h_10()
|
||||||
.border_b_1()
|
.border_b_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.font_semibold()
|
|
||||||
.child(SharedString::new(t!("compose.subject_label"))),
|
|
||||||
)
|
|
||||||
.child(TextInput::new(&self.title_input).small().appearance(false)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_2()
|
|
||||||
.mt_1()
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.text_sm()
|
||||||
.flex_col()
|
.font_semibold()
|
||||||
|
.child(SharedString::new(t!("compose.subject_label"))),
|
||||||
|
)
|
||||||
|
.child(TextInput::new(&self.title_input).small().appearance(false)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.my_1()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -467,9 +484,7 @@ impl Render for Compose {
|
|||||||
.child(SharedString::new(t!("compose.to_label"))),
|
.child(SharedString::new(t!("compose.to_label"))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
h_flex()
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
TextInput::new(&self.user_input)
|
TextInput::new(&self.user_input)
|
||||||
@@ -479,8 +494,8 @@ impl Render for Compose {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("add")
|
Button::new("add")
|
||||||
.icon(IconName::PlusCircleFill)
|
.icon(IconName::PlusCircleFill)
|
||||||
.small()
|
|
||||||
.ghost()
|
.ghost()
|
||||||
|
.loading(self.adding)
|
||||||
.disabled(self.adding)
|
.disabled(self.adding)
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.add_and_select_contact(window, cx);
|
this.add_and_select_contact(window, cx);
|
||||||
@@ -491,14 +506,12 @@ impl Render for Compose {
|
|||||||
.map(|this| {
|
.map(|this| {
|
||||||
if self.contacts.is_empty() {
|
if self.contacts.is_empty() {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
v_flex()
|
||||||
.w_full()
|
|
||||||
.h_24()
|
.h_24()
|
||||||
.flex()
|
.w_full()
|
||||||
.flex_col()
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.text_align(TextAlign::Center)
|
.text_center()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
@@ -525,7 +538,7 @@ impl Render for Compose {
|
|||||||
this.list_items(range, cx)
|
this.list_items(range, cx)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.min_h(px(280.)),
|
.min_h(px(300.)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -534,11 +547,12 @@ impl Render for Compose {
|
|||||||
Button::new("create_dm_btn")
|
Button::new("create_dm_btn")
|
||||||
.label(label)
|
.label(label)
|
||||||
.primary()
|
.primary()
|
||||||
|
.small()
|
||||||
.w_full()
|
.w_full()
|
||||||
.loading(self.submitting)
|
.loading(self.submitting)
|
||||||
.disabled(self.submitting || self.adding)
|
.disabled(self.submitting || self.adding)
|
||||||
.on_click(cx.listener(move |this, _event, window, cx| {
|
.on_click(cx.listener(move |this, _event, window, cx| {
|
||||||
this.compose(window, cx);
|
this.submit(window, cx);
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use smol::fs;
|
|||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{InputState, TextInput};
|
use ui::input::{InputState, TextInput};
|
||||||
use ui::{ContextModal, Disableable, IconName, Sizable};
|
use ui::{v_flex, Disableable, IconName, Sizable};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<EditProfile> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<EditProfile> {
|
||||||
EditProfile::new(window, cx)
|
EditProfile::new(window, cx)
|
||||||
@@ -166,10 +166,7 @@ impl EditProfile {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Option<Event>, Error>> {
|
||||||
// Show loading spinner
|
|
||||||
self.set_submitting(true, cx);
|
|
||||||
|
|
||||||
let avatar = self.avatar_input.read(cx).value().to_string();
|
let avatar = self.avatar_input.read(cx).value().to_string();
|
||||||
let name = self.name_input.read(cx).value().to_string();
|
let name = self.name_input.read(cx).value().to_string();
|
||||||
let bio = self.bio_input.read(cx).value().to_string();
|
let bio = self.bio_input.read(cx).value().to_string();
|
||||||
@@ -191,52 +188,25 @@ impl EditProfile {
|
|||||||
new_metadata = new_metadata.website(url);
|
new_metadata = new_metadata.website(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
nostr_client().set_metadata(&new_metadata).await?;
|
let client = nostr_client();
|
||||||
Ok(())
|
let output = client.set_metadata(&new_metadata).await?;
|
||||||
});
|
let event = client.database().event_by_id(&output.val).await?;
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
Ok(event)
|
||||||
match task.await {
|
|
||||||
Ok(_) => {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
window.push_notification(t!("profile.updated_successfully"), cx);
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_submitting(false, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
window.push_notification(e.to_string(), cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
self.is_loading = status;
|
self.is_loading = status;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_submitting(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.is_submitting = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for EditProfile {
|
impl Render for EditProfile {
|
||||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
div()
|
v_flex()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.px_3()
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -306,17 +276,5 @@ impl Render for EditProfile {
|
|||||||
.child(SharedString::new(t!("profile.label_bio")))
|
.child(SharedString::new(t!("profile.label_bio")))
|
||||||
.child(TextInput::new(&self.bio_input).small()),
|
.child(TextInput::new(&self.bio_input).small()),
|
||||||
)
|
)
|
||||||
.child(
|
|
||||||
div().py_3().child(
|
|
||||||
Button::new("submit")
|
|
||||||
.label(SharedString::new(t!("common.update")))
|
|
||||||
.primary()
|
|
||||||
.disabled(self.is_loading || self.is_submitting)
|
|
||||||
.loading(self.is_submitting)
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.submit(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use gpui::{
|
|||||||
EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
|
EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
|
||||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
||||||
};
|
};
|
||||||
use i18n::t;
|
use i18n::{shared_t, t};
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
@@ -53,8 +53,11 @@ impl Login {
|
|||||||
let key_input =
|
let key_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("nsec... or bunker://..."));
|
cx.new(|cx| InputState::new(window, cx).placeholder("nsec... or bunker://..."));
|
||||||
|
|
||||||
let relay_input =
|
let relay_input = cx.new(|cx| {
|
||||||
cx.new(|cx| InputState::new(window, cx).default_value(NOSTR_CONNECT_RELAY));
|
InputState::new(window, cx)
|
||||||
|
.default_value(NOSTR_CONNECT_RELAY)
|
||||||
|
.placeholder(NOSTR_CONNECT_RELAY)
|
||||||
|
});
|
||||||
|
|
||||||
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||||
//
|
//
|
||||||
@@ -556,12 +559,12 @@ impl Render for Login {
|
|||||||
.text_xl()
|
.text_xl()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.line_height(relative(1.3))
|
.line_height(relative(1.3))
|
||||||
.child(SharedString::new(t!("login.title"))),
|
.child(shared_t!("login.title")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::new(t!("login.key_description"))),
|
.child(shared_t!("login.key_description")),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -581,13 +584,12 @@ impl Render for Login {
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
||||||
let msg = t!("login.approve_message", i = i);
|
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_center()
|
.text_center()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::new(msg)),
|
.child(shared_t!("login.approve_message", i = i)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when_some(self.error.read(cx).clone(), |this, error| {
|
.when_some(self.error.read(cx).clone(), |this, error| {
|
||||||
@@ -603,89 +605,90 @@ impl Render for Login {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div().flex_1().p_1().child(
|
||||||
.h_full()
|
div()
|
||||||
.flex_1()
|
.size_full()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.bg(cx.theme().surface_background)
|
.bg(cx.theme().surface_background)
|
||||||
.child(
|
.rounded(cx.theme().radius)
|
||||||
div()
|
.child(
|
||||||
.flex()
|
div()
|
||||||
.flex_col()
|
.flex()
|
||||||
.items_center()
|
.flex_col()
|
||||||
.justify_center()
|
.items_center()
|
||||||
.gap_3()
|
.justify_center()
|
||||||
.text_center()
|
.gap_3()
|
||||||
.child(
|
.text_center()
|
||||||
div()
|
.child(
|
||||||
.text_center()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.font_semibold()
|
|
||||||
.line_height(relative(1.2))
|
|
||||||
.text_color(cx.theme().text)
|
|
||||||
.child(SharedString::new(t!("login.nostr_connect"))),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::new(t!("login.scan_qr"))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.when_some(self.qr_image.read(cx).clone(), |this, qr| {
|
|
||||||
this.child(
|
|
||||||
div()
|
div()
|
||||||
.id("")
|
.text_center()
|
||||||
.mb_2()
|
.child(
|
||||||
.p_2()
|
div()
|
||||||
.size_72()
|
.font_semibold()
|
||||||
|
.line_height(relative(1.2))
|
||||||
|
.text_color(cx.theme().text)
|
||||||
|
.child(shared_t!("login.nostr_connect")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(shared_t!("login.scan_qr")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(self.qr_image.read(cx).clone(), |this, qr| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.id("")
|
||||||
|
.mb_2()
|
||||||
|
.p_2()
|
||||||
|
.size_72()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.gap_2()
|
||||||
|
.rounded_2xl()
|
||||||
|
.shadow_md()
|
||||||
|
.when(cx.theme().mode.is_dark(), |this| {
|
||||||
|
this.shadow_none()
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
})
|
||||||
|
.bg(cx.theme().background)
|
||||||
|
.child(img(qr).h_64())
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||||
|
this.connection_string.read(cx).to_string(),
|
||||||
|
));
|
||||||
|
window.push_notification(t!("common.copied"), cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.w_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.gap_2()
|
.gap_1()
|
||||||
.rounded_2xl()
|
.child(TextInput::new(&self.relay_input).xsmall())
|
||||||
.shadow_md()
|
.child(
|
||||||
.when(cx.theme().mode.is_dark(), |this| {
|
Button::new("change")
|
||||||
this.shadow_none()
|
.label(t!("common.change"))
|
||||||
.border_1()
|
.ghost()
|
||||||
.border_color(cx.theme().border)
|
.xsmall()
|
||||||
})
|
.on_click(cx.listener(
|
||||||
.bg(cx.theme().background)
|
move |this, _, window, cx| {
|
||||||
.child(img(qr).h_64())
|
this.change_relay(window, cx);
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
},
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
)),
|
||||||
this.connection_string.read(cx).to_string(),
|
),
|
||||||
));
|
),
|
||||||
window.push_notification(
|
),
|
||||||
t!("common.copied").to_string(),
|
),
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w_full()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.gap_1()
|
|
||||||
.child(TextInput::new(&self.relay_input).xsmall())
|
|
||||||
.child(
|
|
||||||
Button::new("change")
|
|
||||||
.label(t!("common.change"))
|
|
||||||
.ghost()
|
|
||||||
.xsmall()
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.change_relay(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
396
crates/coop/src/views/messaging_relays.rs
Normal file
396
crates/coop/src/views/messaging_relays.rs
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Error};
|
||||||
|
use global::constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID, NIP17_RELAYS};
|
||||||
|
use global::nostr_client;
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, px, uniform_list, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
|
||||||
|
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task,
|
||||||
|
TextAlign, UniformList, Window,
|
||||||
|
};
|
||||||
|
use i18n::{shared_t, t};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||||
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
|
use ui::modal::ModalButtonProps;
|
||||||
|
use ui::{h_flex, v_flex, ContextModal, IconName, Sizable, StyledExt};
|
||||||
|
|
||||||
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<MessagingRelays> {
|
||||||
|
cx.new(|cx| MessagingRelays::new(window, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn relay_button() -> impl IntoElement {
|
||||||
|
div().child(
|
||||||
|
Button::new("dm-relays")
|
||||||
|
.icon(IconName::Info)
|
||||||
|
.label(t!("relays.button_label"))
|
||||||
|
.warning()
|
||||||
|
.xsmall()
|
||||||
|
.rounded(ButtonRounded::Full)
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
let title = SharedString::new(t!("relays.modal_title"));
|
||||||
|
let view = cx.new(|cx| MessagingRelays::new(window, cx));
|
||||||
|
let weak_view = view.downgrade();
|
||||||
|
|
||||||
|
window.open_modal(cx, move |modal, _window, _cx| {
|
||||||
|
let weak_view = weak_view.clone();
|
||||||
|
|
||||||
|
modal
|
||||||
|
.confirm()
|
||||||
|
.title(title.clone())
|
||||||
|
.child(view.clone())
|
||||||
|
.button_props(ModalButtonProps::default().ok_text(t!("common.update")))
|
||||||
|
.on_ok(move |_, window, cx| {
|
||||||
|
weak_view
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.set_relays(window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
// true to close the modal
|
||||||
|
false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MessagingRelays {
|
||||||
|
input: Entity<InputState>,
|
||||||
|
relays: Vec<RelayUrl>,
|
||||||
|
error: Option<SharedString>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
subscriptions: SmallVec<[Subscription; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessagingRelays {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
subscriptions.push(cx.observe_new::<Self>(move |this, window, cx| {
|
||||||
|
if let Some(window) = window {
|
||||||
|
this.load(window, cx);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
subscriptions.push(cx.subscribe_in(
|
||||||
|
&input,
|
||||||
|
window,
|
||||||
|
move |this: &mut Self, _, event, window, cx| {
|
||||||
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
|
this.add(window, cx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
input,
|
||||||
|
subscriptions,
|
||||||
|
relays: vec![],
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
||||||
|
let client = nostr_client();
|
||||||
|
let signer = client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::InboxRelays)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if let Some(event) = client.database().query(filter).await?.first() {
|
||||||
|
let relays = event
|
||||||
|
.tags
|
||||||
|
.filter(TagKind::Relay)
|
||||||
|
.filter_map(|tag| RelayUrl::parse(tag.content()?).ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(relays)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Not found."))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
if let Ok(relays) = task.await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.relays = relays;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let value = self.input.read(cx).value().to_string();
|
||||||
|
|
||||||
|
if !value.starts_with("ws") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(url) = RelayUrl::parse(&value) {
|
||||||
|
if !self.relays.contains(&url) {
|
||||||
|
self.relays.push(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.input.update(cx, |this, cx| {
|
||||||
|
this.set_value("", window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, ix: usize, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.relays.remove(ix);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<E>(&mut self, error: E, window: &mut Window, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
E: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.error = Some(error.into());
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
// Clear the error message after a delay
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.error = None;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_relays(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.relays.is_empty() {
|
||||||
|
self.set_error(t!("relays.empty"), window, cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let relays = self.relays.clone();
|
||||||
|
|
||||||
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
|
let client = nostr_client();
|
||||||
|
let signer = client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(
|
||||||
|
relays
|
||||||
|
.iter()
|
||||||
|
.map(|relay| Tag::relay(relay.clone()))
|
||||||
|
.collect_vec(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set messaging relays
|
||||||
|
client.send_event_builder(builder).await?;
|
||||||
|
|
||||||
|
// Connect to messaging relays
|
||||||
|
for relay in relays.into_iter() {
|
||||||
|
_ = client.add_relay(&relay).await;
|
||||||
|
_ = client.connect_relay(&relay).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let all_msg_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
||||||
|
let new_msg_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||||
|
|
||||||
|
let all_messages = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||||
|
let new_messages = Filter::new()
|
||||||
|
.kind(Kind::GiftWrap)
|
||||||
|
.pubkey(public_key)
|
||||||
|
.limit(0);
|
||||||
|
|
||||||
|
// Close old subscriptions
|
||||||
|
client.unsubscribe(&all_msg_id).await;
|
||||||
|
client.unsubscribe(&new_msg_id).await;
|
||||||
|
|
||||||
|
// Subscribe to all messages
|
||||||
|
client
|
||||||
|
.subscribe_with_id(all_msg_id, all_messages, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Subscribe to new messages
|
||||||
|
client
|
||||||
|
.subscribe_with_id(new_msg_id, new_messages, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(_) => {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
window.close_modal(cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_error(e.to_string(), window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> UniformList {
|
||||||
|
let relays = self.relays.clone();
|
||||||
|
let total = relays.len();
|
||||||
|
|
||||||
|
uniform_list(
|
||||||
|
"relays",
|
||||||
|
total,
|
||||||
|
cx.processor(move |_, range, _window, cx| {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
for ix in range {
|
||||||
|
let item = relays.get(ix).map(|i: &RelayUrl| i.to_string()).unwrap();
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
div().group("").w_full().h_9().py_0p5().child(
|
||||||
|
div()
|
||||||
|
.px_2()
|
||||||
|
.h_full()
|
||||||
|
.w_full()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().elevated_surface_background)
|
||||||
|
.text_xs()
|
||||||
|
.child(item)
|
||||||
|
.child(
|
||||||
|
Button::new("remove_{ix}")
|
||||||
|
.icon(IconName::Close)
|
||||||
|
.xsmall()
|
||||||
|
.ghost()
|
||||||
|
.invisible()
|
||||||
|
.group_hover("", |this| this.visible())
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.remove(ix, window, cx)
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.w_full()
|
||||||
|
.min_h(px(200.))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_empty(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
h_flex()
|
||||||
|
.h_20()
|
||||||
|
.mb_2()
|
||||||
|
.justify_center()
|
||||||
|
.text_sm()
|
||||||
|
.text_align(TextAlign::Center)
|
||||||
|
.child(SharedString::new(t!("relays.add_some_relays")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for MessagingRelays {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.gap_3()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(shared_t!("relays.description")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.w_full()
|
||||||
|
.child(TextInput::new(&self.input).small())
|
||||||
|
.child(
|
||||||
|
Button::new("add")
|
||||||
|
.icon(IconName::PlusFill)
|
||||||
|
.label(t!("common.add"))
|
||||||
|
.ghost()
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.add(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(shared_t!("relays.recommended")),
|
||||||
|
)
|
||||||
|
.child(h_flex().gap_1().children({
|
||||||
|
NIP17_RELAYS.iter().map(|&relay| {
|
||||||
|
div()
|
||||||
|
.id(relay)
|
||||||
|
.group("")
|
||||||
|
.py_0p5()
|
||||||
|
.px_1p5()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.bg(cx.theme().secondary_background)
|
||||||
|
.hover(|this| this.bg(cx.theme().secondary_hover))
|
||||||
|
.active(|this| this.bg(cx.theme().secondary_active))
|
||||||
|
.rounded_full()
|
||||||
|
.child(relay)
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.input.update(cx, |this, cx| {
|
||||||
|
this.set_value(relay, window, cx);
|
||||||
|
});
|
||||||
|
this.add(window, cx);
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.when_some(self.error.as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.italic()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().danger_foreground)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.map(|this| {
|
||||||
|
if !self.relays.is_empty() {
|
||||||
|
this.child(self.render_list(window, cx))
|
||||||
|
} else {
|
||||||
|
this.child(self.render_empty(window, cx))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
|
pub mod backup_keys;
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod compose;
|
pub mod compose;
|
||||||
pub mod edit_profile;
|
pub mod edit_profile;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
pub mod messaging_relays;
|
||||||
pub mod new_account;
|
pub mod new_account;
|
||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
pub mod preferences;
|
pub mod preferences;
|
||||||
pub mod relays;
|
|
||||||
pub mod screening;
|
pub mod screening;
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
pub mod startup;
|
pub mod startup;
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
|
use anyhow::anyhow;
|
||||||
use common::nip96::nip96_upload;
|
use common::nip96::nip96_upload;
|
||||||
use global::nostr_client;
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten,
|
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
|
||||||
FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString,
|
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
|
||||||
Styled, Window,
|
Render, SharedString, Styled, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
|
use gpui_tokio::Tokio;
|
||||||
use i18n::t;
|
use i18n::t;
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::avatar::Avatar;
|
||||||
|
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::input::{InputState, TextInput};
|
use ui::input::{InputState, TextInput};
|
||||||
use ui::popup_menu::PopupMenu;
|
use ui::popup_menu::PopupMenu;
|
||||||
use ui::{ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
use ui::{divider, v_flex, ContextModal, Disableable, IconName, Sizable, StyledExt};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
||||||
NewAccount::new(window, cx)
|
NewAccount::new(window, cx)
|
||||||
@@ -25,7 +27,6 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
|||||||
pub struct NewAccount {
|
pub struct NewAccount {
|
||||||
name_input: Entity<InputState>,
|
name_input: Entity<InputState>,
|
||||||
avatar_input: Entity<InputState>,
|
avatar_input: Entity<InputState>,
|
||||||
bio_input: Entity<InputState>,
|
|
||||||
is_uploading: bool,
|
is_uploading: bool,
|
||||||
is_submitting: bool,
|
is_submitting: bool,
|
||||||
// Panel
|
// Panel
|
||||||
@@ -46,19 +47,12 @@ impl NewAccount {
|
|||||||
.placeholder(SharedString::new(t!("profile.placeholder_name")))
|
.placeholder(SharedString::new(t!("profile.placeholder_name")))
|
||||||
});
|
});
|
||||||
|
|
||||||
let bio_input = cx.new(|cx| {
|
|
||||||
InputState::new(window, cx)
|
|
||||||
.multi_line()
|
|
||||||
.placeholder(SharedString::new(t!("profile.placeholder_bio")))
|
|
||||||
});
|
|
||||||
|
|
||||||
let avatar_input =
|
let avatar_input =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.png"));
|
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.png"));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
name_input,
|
name_input,
|
||||||
avatar_input,
|
avatar_input,
|
||||||
bio_input,
|
|
||||||
is_uploading: false,
|
is_uploading: false,
|
||||||
is_submitting: false,
|
is_submitting: false,
|
||||||
name: "New Account".into(),
|
name: "New Account".into(),
|
||||||
@@ -69,140 +63,97 @@ impl NewAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.set_submitting(true, cx);
|
self.submitting(true, cx);
|
||||||
|
|
||||||
|
let identity = Identity::global(cx);
|
||||||
let avatar = self.avatar_input.read(cx).value().to_string();
|
let avatar = self.avatar_input.read(cx).value().to_string();
|
||||||
let name = self.name_input.read(cx).value().to_string();
|
let name = self.name_input.read(cx).value().to_string();
|
||||||
let bio = self.bio_input.read(cx).value().to_string();
|
|
||||||
|
|
||||||
let mut metadata = Metadata::new().display_name(name).about(bio);
|
// Build metadata
|
||||||
|
let mut metadata = Metadata::new().display_name(name.clone()).name(name);
|
||||||
|
|
||||||
if let Ok(url) = Url::parse(&avatar) {
|
if let Ok(url) = Url::parse(&avatar) {
|
||||||
metadata = metadata.picture(url);
|
metadata = metadata.picture(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
let current_view = cx.entity().downgrade();
|
identity.update(cx, |this, cx| {
|
||||||
let pwd_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
this.new_identity(metadata, window, cx);
|
||||||
let weak_input = pwd_input.downgrade();
|
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, _cx| {
|
|
||||||
let metadata = metadata.clone();
|
|
||||||
let weak_input = weak_input.clone();
|
|
||||||
let view_cancel = current_view.clone();
|
|
||||||
|
|
||||||
this.overlay_closable(false)
|
|
||||||
.show_close(false)
|
|
||||||
.keyboard(false)
|
|
||||||
.confirm()
|
|
||||||
.on_cancel(move |_, window, cx| {
|
|
||||||
view_cancel
|
|
||||||
.update(cx, |_this, cx| {
|
|
||||||
window.push_notification(t!("new_account.password_invalid"), cx)
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
true
|
|
||||||
})
|
|
||||||
.on_ok(move |_, window, cx| {
|
|
||||||
let metadata = metadata.clone();
|
|
||||||
let value = weak_input
|
|
||||||
.read_with(cx, |state, _cx| state.value().to_owned())
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
if let Some(password) = value {
|
|
||||||
Identity::global(cx).update(cx, |this, cx| {
|
|
||||||
this.new_identity(password.to_string(), metadata, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w_full()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
|
||||||
.text_sm()
|
|
||||||
.child(SharedString::new(t!("new_account.set_password_prompt")))
|
|
||||||
.child(TextInput::new(&pwd_input).small()),
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let nip96 = AppSettings::get_media_server(cx);
|
self.uploading(true, cx);
|
||||||
let avatar_input = self.avatar_input.downgrade();
|
|
||||||
|
// Get the user's configured NIP96 server
|
||||||
|
let nip96_server = AppSettings::get_media_server(cx);
|
||||||
|
|
||||||
|
// Open native file dialog
|
||||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||||
files: true,
|
files: true,
|
||||||
directories: false,
|
directories: false,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
self.set_uploading(true, cx);
|
let task = Tokio::spawn(cx, async move {
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
||||||
Ok(Some(mut paths)) => {
|
Ok(Some(mut paths)) => {
|
||||||
let Some(path) = paths.pop() else {
|
if let Some(path) = paths.pop() {
|
||||||
cx.update(|_, cx| {
|
let file = fs::read(path).await?;
|
||||||
this.update(cx, |this, cx| {
|
let url = nip96_upload(nostr_client(), &nip96_server, file).await?;
|
||||||
this.set_uploading(false, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
return;
|
Ok(url)
|
||||||
};
|
} else {
|
||||||
|
Err(anyhow!("Path not found"))
|
||||||
if let Ok(file_data) = fs::read(path).await {
|
|
||||||
let (tx, rx) = oneshot::channel::<Url>();
|
|
||||||
|
|
||||||
nostr_sdk::async_utility::task::spawn(async move {
|
|
||||||
if let Ok(url) = nip96_upload(nostr_client(), &nip96, file_data).await {
|
|
||||||
_ = tx.send(url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Ok(url) = rx.await {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
// Stop loading spinner
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_uploading(false, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Set avatar input
|
|
||||||
avatar_input
|
|
||||||
.update(cx, |this, cx| {
|
|
||||||
this.set_value(url.to_string(), window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => Err(anyhow!("User cancelled")),
|
||||||
cx.update(|_, cx| {
|
Err(e) => Err(anyhow!("File dialog error: {e}")),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match Flatten::flatten(task.await.map_err(|e| e.into())) {
|
||||||
|
Ok(Ok(url)) => {
|
||||||
|
cx.update(|window, cx| {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_uploading(false, cx);
|
this.uploading(false, cx);
|
||||||
|
this.avatar_input.update(cx, |this, cx| {
|
||||||
|
this.set_value(url.to_string(), window, cx);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
.ok();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
Err(_) => {}
|
Ok(Err(e)) => {
|
||||||
|
Self::notify_error(cx, this, e.to_string());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
Self::notify_error(cx, this, e.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_submitting(&mut self, status: bool, cx: &mut Context<Self>) {
|
fn notify_error(cx: &mut AsyncWindowContext, entity: WeakEntity<NewAccount>, e: String) {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
entity
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
window.push_notification(e, cx);
|
||||||
|
this.uploading(false, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submitting(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
self.is_submitting = status;
|
self.is_submitting = status;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_uploading(&mut self, status: bool, cx: &mut Context<Self>) {
|
fn uploading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
self.is_uploading = status;
|
self.is_uploading = status;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -243,93 +194,72 @@ impl Focusable for NewAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Render for NewAccount {
|
impl Render for NewAccount {
|
||||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
div()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.relative()
|
.relative()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.gap_10()
|
.gap_10()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_center()
|
|
||||||
.text_lg()
|
.text_lg()
|
||||||
|
.text_center()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.line_height(relative(1.3))
|
.line_height(relative(1.3))
|
||||||
.child(SharedString::new(t!("new_account.title"))),
|
.child(SharedString::new(t!("new_account.title"))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.w_72()
|
.w_96()
|
||||||
.flex()
|
.gap_4()
|
||||||
.flex_col()
|
|
||||||
.gap_3()
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.w_full()
|
|
||||||
.h_32()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.gap_2()
|
|
||||||
.map(|this| {
|
|
||||||
if self.avatar_input.read(cx).value().is_empty() {
|
|
||||||
this.child(
|
|
||||||
img("brand/avatar.png")
|
|
||||||
.rounded_full()
|
|
||||||
.size_10()
|
|
||||||
.flex_shrink_0(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.child(
|
|
||||||
img(self.avatar_input.read(cx).value().clone())
|
|
||||||
.rounded_full()
|
|
||||||
.size_10()
|
|
||||||
.flex_shrink_0(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
Button::new("upload")
|
|
||||||
.label(t!("profile.set_profile_picture"))
|
|
||||||
.icon(Icon::new(IconName::Plus))
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.disabled(self.is_submitting)
|
|
||||||
.loading(self.is_uploading)
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.upload(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(SharedString::new(t!("profile.label_name")))
|
.child(SharedString::new(t!("new_account.name")))
|
||||||
.child(TextInput::new(&self.name_input).small()),
|
.child(TextInput::new(&self.name_input).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_sm()
|
.child(
|
||||||
.child(SharedString::new(t!("profile.label_bio")))
|
div()
|
||||||
.child(TextInput::new(&self.bio_input).small()),
|
.text_sm()
|
||||||
)
|
.child(SharedString::new(t!("new_account.avatar"))),
|
||||||
.child(
|
)
|
||||||
div()
|
.child(
|
||||||
.my_2()
|
v_flex()
|
||||||
.w_full()
|
.p_1()
|
||||||
.h_px()
|
.h_32()
|
||||||
.bg(cx.theme().elevated_surface_background),
|
.w_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.gap_2()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.border_1()
|
||||||
|
.border_dashed()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.child(
|
||||||
|
Avatar::new(self.avatar_input.read(cx).value().to_string())
|
||||||
|
.size(rems(2.25)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("upload")
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.label(t!("common.upload"))
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.rounded(ButtonRounded::Full)
|
||||||
|
.disabled(self.is_submitting)
|
||||||
|
.loading(self.is_uploading)
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.upload(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
.child(divider(cx))
|
||||||
.child(
|
.child(
|
||||||
Button::new("submit")
|
Button::new("submit")
|
||||||
.label(SharedString::new(t!("common.continue")))
|
.label(SharedString::new(t!("common.continue")))
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use nostr_sdk::prelude::*;
|
|||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||||
use ui::checkbox::Checkbox;
|
use ui::checkbox::Checkbox;
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
@@ -244,12 +244,13 @@ impl Render for Onboarding {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w_24().absolute().bottom_4().right_4().child(
|
div().w_24().absolute().bottom_2().right_2().child(
|
||||||
Button::new("unload")
|
Button::new("logout")
|
||||||
.icon(IconName::Logout)
|
.icon(IconName::Logout)
|
||||||
.label(SharedString::new(t!("common.logout")))
|
.label(SharedString::new(t!("user.sign_out")))
|
||||||
.ghost()
|
.danger()
|
||||||
.small()
|
.xsmall()
|
||||||
|
.rounded(ButtonRounded::Full)
|
||||||
.disabled(self.loading)
|
.disabled(self.loading)
|
||||||
.on_click(|_, window, cx| {
|
.on_click(|_, window, cx| {
|
||||||
Identity::global(cx).update(cx, |this, cx| {
|
Identity::global(cx).update(cx, |this, cx| {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use common::display::DisplayProfile;
|
use common::display::DisplayProfile;
|
||||||
use global::constants::{DEFAULT_MODAL_WIDTH, NIP96_SERVER};
|
|
||||||
use gpui::http_client::Url;
|
use gpui::http_client::Url;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -14,10 +13,11 @@ use theme::ActiveTheme;
|
|||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{InputState, TextInput};
|
use ui::input::{InputState, TextInput};
|
||||||
|
use ui::modal::ModalButtonProps;
|
||||||
use ui::switch::Switch;
|
use ui::switch::Switch;
|
||||||
use ui::{v_flex, ContextModal, IconName, Sizable, Size, StyledExt};
|
use ui::{v_flex, ContextModal, IconName, Sizable, Size, StyledExt};
|
||||||
|
|
||||||
use crate::views::{edit_profile, relays};
|
use crate::views::{edit_profile, messaging_relays};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
||||||
Preferences::new(window, cx)
|
Preferences::new(window, cx)
|
||||||
@@ -33,8 +33,8 @@ impl Preferences {
|
|||||||
let media_server = AppSettings::get_media_server(cx).to_string();
|
let media_server = AppSettings::get_media_server(cx).to_string();
|
||||||
let media_input = cx.new(|cx| {
|
let media_input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.default_value(media_server)
|
.default_value(media_server.clone())
|
||||||
.placeholder(NIP96_SERVER)
|
.placeholder(media_server)
|
||||||
});
|
});
|
||||||
|
|
||||||
Self { media_input }
|
Self { media_input }
|
||||||
@@ -42,25 +42,73 @@ impl Preferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn open_edit_profile(&self, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_edit_profile(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let edit_profile = edit_profile::init(window, cx);
|
let view = edit_profile::init(window, cx);
|
||||||
|
let weak_view = view.downgrade();
|
||||||
let title = SharedString::new(t!("profile.title"));
|
let title = SharedString::new(t!("profile.title"));
|
||||||
|
|
||||||
window.open_modal(cx, move |modal, _window, _cx| {
|
window.open_modal(cx, move |modal, _window, _cx| {
|
||||||
|
let weak_view = weak_view.clone();
|
||||||
|
|
||||||
modal
|
modal
|
||||||
|
.confirm()
|
||||||
.title(title.clone())
|
.title(title.clone())
|
||||||
.width(px(DEFAULT_MODAL_WIDTH))
|
.child(view.clone())
|
||||||
.child(edit_profile.clone())
|
.button_props(ModalButtonProps::default().ok_text(t!("common.update")))
|
||||||
|
.on_ok(move |_, window, cx| {
|
||||||
|
weak_view
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
let set_metadata = this.set_metadata(cx);
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |_, cx| {
|
||||||
|
match set_metadata.await {
|
||||||
|
Ok(event) => {
|
||||||
|
if let Some(event) = event {
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
Registry::global(cx).update(cx, |this, cx| {
|
||||||
|
this.insert_or_update_person(event, cx);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
// true to close the modal
|
||||||
|
true
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let relays = relays::init(window, cx);
|
let title = SharedString::new(t!("relays.modal_title"));
|
||||||
let title = SharedString::new(t!("preferences.modal_relays_title"));
|
let view = messaging_relays::init(window, cx);
|
||||||
|
let weak_view = view.downgrade();
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, _cx| {
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
this.width(px(DEFAULT_MODAL_WIDTH))
|
let weak_view = weak_view.clone();
|
||||||
|
|
||||||
|
this.confirm()
|
||||||
.title(title.clone())
|
.title(title.clone())
|
||||||
.child(relays.clone())
|
.child(view.clone())
|
||||||
|
.button_props(ModalButtonProps::default().ok_text(t!("common.update")))
|
||||||
|
.on_ok(move |_, window, cx| {
|
||||||
|
weak_view
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.set_relays(window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
// true to close the modal
|
||||||
|
false
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,10 +128,8 @@ impl Render for Preferences {
|
|||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.py_2()
|
.py_2()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -135,7 +181,7 @@ impl Render for Preferences {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("relays")
|
Button::new("relays")
|
||||||
.label("DM Relays")
|
.label("Messaging Relays")
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||||
@@ -146,11 +192,8 @@ impl Render for Preferences {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.py_2()
|
.py_2()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_2()
|
|
||||||
.border_t_1()
|
.border_t_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.child(
|
.child(
|
||||||
@@ -162,6 +205,7 @@ impl Render for Preferences {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.my_1()
|
||||||
.flex()
|
.flex()
|
||||||
.items_start()
|
.items_start()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
@@ -193,10 +237,8 @@ impl Render for Preferences {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.py_2()
|
.py_2()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.border_t_1()
|
.border_t_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
@@ -208,9 +250,7 @@ impl Render for Preferences {
|
|||||||
.child(SharedString::new(t!("preferences.messages_header"))),
|
.child(SharedString::new(t!("preferences.messages_header"))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
Switch::new("screening")
|
Switch::new("screening")
|
||||||
@@ -242,10 +282,8 @@ impl Render for Preferences {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.py_2()
|
.py_2()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.border_t_1()
|
.border_t_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
@@ -257,9 +295,7 @@ impl Render for Preferences {
|
|||||||
.child(SharedString::new(t!("preferences.display_header"))),
|
.child(SharedString::new(t!("preferences.display_header"))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
Switch::new("hide_user_avatars")
|
Switch::new("hide_user_avatars")
|
||||||
|
|||||||
@@ -1,347 +0,0 @@
|
|||||||
use anyhow::Error;
|
|
||||||
use global::constants::NEW_MESSAGE_SUB_ID;
|
|
||||||
use global::nostr_client;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
div, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
|
||||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, TextAlign,
|
|
||||||
UniformList, Window,
|
|
||||||
};
|
|
||||||
use i18n::t;
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
|
||||||
use ui::{ContextModal, Disableable, IconName, Sizable};
|
|
||||||
|
|
||||||
const MIN_HEIGHT: f32 = 200.0;
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Relays> {
|
|
||||||
Relays::new(window, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Relays {
|
|
||||||
relays: Entity<Vec<RelayUrl>>,
|
|
||||||
input: Entity<InputState>,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
is_loading: bool,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
subscriptions: SmallVec<[Subscription; 1]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Relays {
|
|
||||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
|
||||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
|
|
||||||
let relays = cx.new(|cx| {
|
|
||||||
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
|
||||||
let client = nostr_client();
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::InboxRelays)
|
|
||||||
.author(public_key)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
|
||||||
let relays = event
|
|
||||||
.tags
|
|
||||||
.filter(TagKind::Relay)
|
|
||||||
.filter_map(|tag| RelayUrl::parse(tag.content()?).ok())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Ok(relays)
|
|
||||||
} else {
|
|
||||||
let relays = vec![
|
|
||||||
RelayUrl::parse("wss://auth.nostr1.com")?,
|
|
||||||
RelayUrl::parse("wss://relay.0xchat.com")?,
|
|
||||||
];
|
|
||||||
|
|
||||||
Ok(relays)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
if let Ok(relays) = task.await {
|
|
||||||
cx.update(|cx| {
|
|
||||||
this.update(cx, |this: &mut Vec<RelayUrl>, cx| {
|
|
||||||
*this = relays;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
vec![]
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.new(|cx| {
|
|
||||||
let mut subscriptions = smallvec![];
|
|
||||||
|
|
||||||
subscriptions.push(cx.subscribe_in(
|
|
||||||
&input,
|
|
||||||
window,
|
|
||||||
move |this: &mut Relays, _, event, window, cx| {
|
|
||||||
if let InputEvent::PressEnter { .. } = event {
|
|
||||||
this.add(window, cx);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
Self {
|
|
||||||
relays,
|
|
||||||
input,
|
|
||||||
subscriptions,
|
|
||||||
is_loading: false,
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.set_loading(true, cx);
|
|
||||||
|
|
||||||
let relays = self.relays.read(cx).clone();
|
|
||||||
let task: Task<Result<EventId, Error>> = cx.background_spawn(async move {
|
|
||||||
let client = nostr_client();
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
|
|
||||||
// If user didn't have any NIP-65 relays, add default ones
|
|
||||||
if client.database().relay_list(public_key).await?.is_empty() {
|
|
||||||
let builder = EventBuilder::relay_list(vec![
|
|
||||||
(RelayUrl::parse("wss://relay.damus.io/").unwrap(), None),
|
|
||||||
(RelayUrl::parse("wss://relay.primal.net/").unwrap(), None),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if let Err(e) = client.send_event_builder(builder).await {
|
|
||||||
log::error!("Failed to send relay list event: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let tags: Vec<Tag> = relays
|
|
||||||
.iter()
|
|
||||||
.map(|relay| Tag::custom(TagKind::Relay, vec![relay.to_string()]))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
|
|
||||||
let output = client.send_event_builder(builder).await?;
|
|
||||||
|
|
||||||
// Connect to messaging relays
|
|
||||||
for relay in relays.into_iter() {
|
|
||||||
_ = client.add_relay(&relay).await;
|
|
||||||
_ = client.connect_relay(&relay).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
|
||||||
|
|
||||||
// Close old subscription
|
|
||||||
client.unsubscribe(&sub_id).await;
|
|
||||||
|
|
||||||
// Subscribe to new messages
|
|
||||||
if let Err(e) = client
|
|
||||||
.subscribe_with_id(
|
|
||||||
sub_id,
|
|
||||||
Filter::new()
|
|
||||||
.kind(Kind::GiftWrap)
|
|
||||||
.pubkey(public_key)
|
|
||||||
.limit(0),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
log::error!("Failed to subscribe to new messages: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(output.val)
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
if task.await.is_ok() {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_loading(false, cx);
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
window.close_modal(cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.is_loading = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let value = self.input.read(cx).value().to_string();
|
|
||||||
|
|
||||||
if !value.starts_with("ws") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(url) = RelayUrl::parse(&value) {
|
|
||||||
self.relays.update(cx, |this, cx| {
|
|
||||||
if !this.contains(&url) {
|
|
||||||
this.push(url);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.input.update(cx, |this, cx| {
|
|
||||||
this.set_value("", window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove(&mut self, ix: usize, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.relays.update(cx, |this, cx| {
|
|
||||||
this.remove(ix);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_list(
|
|
||||||
&mut self,
|
|
||||||
relays: Vec<RelayUrl>,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> UniformList {
|
|
||||||
let total = relays.len();
|
|
||||||
|
|
||||||
uniform_list(
|
|
||||||
"relays",
|
|
||||||
total,
|
|
||||||
cx.processor(move |_, range, _window, cx| {
|
|
||||||
let mut items = Vec::new();
|
|
||||||
|
|
||||||
for ix in range {
|
|
||||||
let item = relays.get(ix).map(|i: &RelayUrl| i.to_string()).unwrap();
|
|
||||||
|
|
||||||
items.push(
|
|
||||||
div().group("").w_full().h_9().py_0p5().child(
|
|
||||||
div()
|
|
||||||
.px_2()
|
|
||||||
.h_full()
|
|
||||||
.w_full()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_between()
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.bg(cx.theme().elevated_surface_background)
|
|
||||||
.text_xs()
|
|
||||||
.child(item)
|
|
||||||
.child(
|
|
||||||
Button::new("remove_{ix}")
|
|
||||||
.icon(IconName::Close)
|
|
||||||
.xsmall()
|
|
||||||
.ghost()
|
|
||||||
.invisible()
|
|
||||||
.group_hover("", |this| this.visible())
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.remove(ix, window, cx)
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
items
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.w_full()
|
|
||||||
.min_h(px(MIN_HEIGHT))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_empty(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
div()
|
|
||||||
.h_20()
|
|
||||||
.mb_2()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.text_sm()
|
|
||||||
.text_align(TextAlign::Center)
|
|
||||||
.child(SharedString::new(t!("relays.add_some_relays")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for Relays {
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
div()
|
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.size_full()
|
|
||||||
.px_3()
|
|
||||||
.pb_3()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.justify_between()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::new(t!("relays.description"))),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w_full()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_3()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.w_full()
|
|
||||||
.gap_2()
|
|
||||||
.child(TextInput::new(&self.input).small())
|
|
||||||
.child(
|
|
||||||
Button::new("add_relay_btn")
|
|
||||||
.icon(IconName::Plus)
|
|
||||||
.label(t!("common.add"))
|
|
||||||
.small()
|
|
||||||
.ghost()
|
|
||||||
.rounded_md()
|
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
|
||||||
this.add(window, cx)
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.map(|this| {
|
|
||||||
let relays = self.relays.read(cx).clone();
|
|
||||||
|
|
||||||
if !relays.is_empty() {
|
|
||||||
this.child(self.render_list(relays, window, cx))
|
|
||||||
} else {
|
|
||||||
this.child(self.render_empty(window, cx))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("submti")
|
|
||||||
.label(t!("common.update"))
|
|
||||||
.primary()
|
|
||||||
.w_full()
|
|
||||||
.loading(self.is_loading)
|
|
||||||
.disabled(self.is_loading)
|
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
|
||||||
this.update(window, cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,15 +4,14 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use common::debounced_delay::DebouncedDelay;
|
use common::debounced_delay::DebouncedDelay;
|
||||||
use common::display::{DisplayProfile, TextUtils};
|
use common::display::TextUtils;
|
||||||
use global::constants::{BOOTSTRAP_RELAYS, DEFAULT_MODAL_WIDTH, SEARCH_RELAYS};
|
use global::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
||||||
use global::nostr_client;
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, uniform_list, AnyElement, App, AppContext, ClipboardItem, Context,
|
div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||||
Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
|
FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString,
|
||||||
Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
Styled, Subscription, Task, Window,
|
||||||
Task, Window,
|
|
||||||
};
|
};
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use i18n::t;
|
use i18n::t;
|
||||||
@@ -25,7 +24,6 @@ use registry::{Registry, RoomEmitter};
|
|||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
|
||||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
@@ -34,8 +32,6 @@ use ui::popup_menu::PopupMenu;
|
|||||||
use ui::skeleton::Skeleton;
|
use ui::skeleton::Skeleton;
|
||||||
use ui::{v_flex, ContextModal, IconName, Selectable, Sizable, StyledExt};
|
use ui::{v_flex, ContextModal, IconName, Selectable, Sizable, StyledExt};
|
||||||
|
|
||||||
use crate::views::compose;
|
|
||||||
|
|
||||||
mod list_item;
|
mod list_item;
|
||||||
|
|
||||||
const FIND_DELAY: u64 = 600;
|
const FIND_DELAY: u64 = 600;
|
||||||
@@ -551,18 +547,6 @@ impl Sidebar {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_compose(&self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let compose = compose::init(window, cx);
|
|
||||||
let title = SharedString::new(t!("sidebar.direct_messages"));
|
|
||||||
|
|
||||||
window.open_modal(cx, move |modal, _window, _cx| {
|
|
||||||
modal
|
|
||||||
.title(title.clone())
|
|
||||||
.width(px(DEFAULT_MODAL_WIDTH))
|
|
||||||
.child(compose.clone())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_loading_modal(&self, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_loading_modal(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let title = SharedString::new(t!("sidebar.loading_modal_title"));
|
let title = SharedString::new(t!("sidebar.loading_modal_title"));
|
||||||
let text_1 = SharedString::new(t!("sidebar.loading_modal_body_1"));
|
let text_1 = SharedString::new(t!("sidebar.loading_modal_body_1"));
|
||||||
@@ -596,49 +580,6 @@ impl Sidebar {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn account(&self, profile: &Profile, cx: &Context<Self>) -> impl IntoElement {
|
|
||||||
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
|
||||||
|
|
||||||
div()
|
|
||||||
.px_3()
|
|
||||||
.h_8()
|
|
||||||
.flex_none()
|
|
||||||
.flex()
|
|
||||||
.justify_between()
|
|
||||||
.items_center()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.id("current-user")
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_2()
|
|
||||||
.text_sm()
|
|
||||||
.font_semibold()
|
|
||||||
.child(Avatar::new(profile.avatar_url(proxy)).size(rems(1.75)))
|
|
||||||
.child(profile.display_name())
|
|
||||||
.on_click(cx.listener({
|
|
||||||
let Ok(public_key) = profile.public_key().to_bech32();
|
|
||||||
let item = ClipboardItem::new_string(public_key);
|
|
||||||
|
|
||||||
move |_, _, window, cx| {
|
|
||||||
cx.write_to_clipboard(item.clone());
|
|
||||||
window.push_notification(t!("common.copied"), cx);
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("compose")
|
|
||||||
.icon(IconName::PlusFill)
|
|
||||||
.tooltip(t!("sidebar.dm_tooltip"))
|
|
||||||
.small()
|
|
||||||
.primary()
|
|
||||||
.rounded(ButtonRounded::Full)
|
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
|
||||||
this.open_compose(window, cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn skeletons(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
|
fn skeletons(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
|
||||||
(0..total).map(|_| {
|
(0..total).map(|_| {
|
||||||
div()
|
div()
|
||||||
@@ -727,9 +668,6 @@ impl Focusable for Sidebar {
|
|||||||
impl Render for Sidebar {
|
impl Render for Sidebar {
|
||||||
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 registry = Registry::read_global(cx);
|
let registry = Registry::read_global(cx);
|
||||||
let profile = Identity::read_global(cx)
|
|
||||||
.public_key()
|
|
||||||
.map(|pk| registry.get_person(&pk, cx));
|
|
||||||
|
|
||||||
// Get rooms from either search results or the chat registry
|
// Get rooms from either search results or the chat registry
|
||||||
let rooms = if let Some(results) = self.local_result.read(cx).as_ref() {
|
let rooms = if let Some(results) = self.local_result.read(cx).as_ref() {
|
||||||
@@ -745,22 +683,17 @@ impl Render for Sidebar {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
div()
|
v_flex()
|
||||||
.image_cache(self.image_cache.clone())
|
.image_cache(self.image_cache.clone())
|
||||||
.size_full()
|
.size_full()
|
||||||
.relative()
|
.relative()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_3()
|
.gap_3()
|
||||||
// Account
|
|
||||||
.when_some(profile, |this, profile| {
|
|
||||||
this.child(self.account(&profile, cx))
|
|
||||||
})
|
|
||||||
// Search Input
|
// Search Input
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
.px_3()
|
.mt_3()
|
||||||
|
.px_2p5()
|
||||||
.w_full()
|
.w_full()
|
||||||
.h_7()
|
.h_7()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
@@ -781,14 +714,12 @@ impl Render for Sidebar {
|
|||||||
)
|
)
|
||||||
// Chat Rooms
|
// Chat Rooms
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.px_2()
|
|
||||||
.w_full()
|
|
||||||
.flex_1()
|
|
||||||
.overflow_y_hidden()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
.gap_1()
|
||||||
|
.flex_1()
|
||||||
|
.px_1p5()
|
||||||
|
.w_full()
|
||||||
|
.overflow_y_hidden()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
@@ -879,8 +810,11 @@ impl Render for Sidebar {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.when(registry.loading, |this| {
|
.when(registry.loading, |this| {
|
||||||
|
let title = SharedString::new(t!("sidebar.retrieving_messages"));
|
||||||
|
let desc = SharedString::new(t!("sidebar.retrieving_messages_description"));
|
||||||
|
|
||||||
this.child(
|
this.child(
|
||||||
div().absolute().bottom_4().px_4().w_full().child(
|
div().absolute().bottom_3().px_3().w_full().child(
|
||||||
div()
|
div()
|
||||||
.p_1()
|
.p_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -890,35 +824,28 @@ impl Render for Sidebar {
|
|||||||
.justify_between()
|
.justify_between()
|
||||||
.bg(cx.theme().panel_background)
|
.bg(cx.theme().panel_background)
|
||||||
.shadow_sm()
|
.shadow_sm()
|
||||||
// Empty div
|
// Loading
|
||||||
.child(div().size_6().flex_shrink_0())
|
.child(div().flex_shrink_0().pl_1().child(Indicator::new().small()))
|
||||||
// Loading indicator
|
// Title
|
||||||
.child(
|
.child(
|
||||||
div()
|
v_flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.text_xs()
|
|
||||||
.text_center()
|
.text_center()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.text_sm()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_1()
|
|
||||||
.line_height(relative(1.2))
|
.line_height(relative(1.2))
|
||||||
.child(Indicator::new().xsmall())
|
.child(title.clone()),
|
||||||
.child(SharedString::new(t!(
|
|
||||||
"sidebar.retrieving_messages"
|
|
||||||
))),
|
|
||||||
)
|
)
|
||||||
.child(div().text_color(cx.theme().text_muted).child(
|
.child(
|
||||||
SharedString::new(t!(
|
div()
|
||||||
"sidebar.retrieving_messages_description"
|
.text_xs()
|
||||||
)),
|
.text_color(cx.theme().text_muted)
|
||||||
)),
|
.child(desc.clone()),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
// Info button
|
// Info button
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ impl UserProfile {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
// Skip if user isn't logged in
|
// Skip if user isn't logged in
|
||||||
let Some(identity) = Identity::read_global(cx).public_key() else {
|
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||||
return;
|
return;
|
||||||
@@ -100,11 +100,6 @@ impl UserProfile {
|
|||||||
self.profile(cx).metadata().nip05
|
self.profile(cx).metadata().nip05
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_njump(&mut self, _window: &mut Window, cx: &mut App) {
|
|
||||||
let Ok(bech32) = self.public_key.to_bech32();
|
|
||||||
cx.open_url(&format!("https://njump.me/{bech32}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn copy_pubkey(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn copy_pubkey(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Ok(bech32) = self.public_key.to_bech32();
|
let Ok(bech32) = self.public_key.to_bech32();
|
||||||
let item = ClipboardItem::new_string(bech32);
|
let item = ClipboardItem::new_string(bech32);
|
||||||
@@ -221,7 +216,7 @@ impl Render for UserProfile {
|
|||||||
.child(shared_bech32),
|
.child(shared_bech32),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("copy-pubkey")
|
Button::new("copy")
|
||||||
.icon({
|
.icon({
|
||||||
if self.copied {
|
if self.copied {
|
||||||
IconName::CheckCircleFill
|
IconName::CheckCircleFill
|
||||||
@@ -259,14 +254,5 @@ impl Render for UserProfile {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
|
||||||
Button::new("open-njump")
|
|
||||||
.label(t!("profile.njump"))
|
|
||||||
.primary()
|
|
||||||
.small()
|
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
|
||||||
this.open_njump(window, cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub const NIP65_RELAYS: [&str; 4] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/// Messaging Relays. Used for new account
|
/// Messaging Relays. Used for new account
|
||||||
pub const NIP17_RELAYS: [&str; 2] = ["wss://auth.nostr1.com", "wss://relay.0xchat.com"];
|
pub const NIP17_RELAYS: [&str; 2] = ["wss://nip17.com", "wss://relay.0xchat.com"];
|
||||||
|
|
||||||
/// Default relay for Nostr Connect
|
/// Default relay for Nostr Connect
|
||||||
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ impl Global for GlobalIdentity {}
|
|||||||
|
|
||||||
pub struct Identity {
|
pub struct Identity {
|
||||||
public_key: Option<PublicKey>,
|
public_key: Option<PublicKey>,
|
||||||
auto_logging_in_progress: bool,
|
logging_in: bool,
|
||||||
|
relay_ready: Option<bool>,
|
||||||
|
need_backup: Option<Keys>,
|
||||||
|
need_onboarding: bool,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
subscriptions: SmallVec<[Subscription; 1]>,
|
subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
}
|
}
|
||||||
@@ -66,14 +69,17 @@ impl Identity {
|
|||||||
this.set_logging_in(true, cx);
|
this.set_logging_in(true, cx);
|
||||||
this.load(window, cx);
|
this.load(window, cx);
|
||||||
} else {
|
} else {
|
||||||
this.set_public_key(None, cx);
|
this.set_public_key(None, window, cx);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
public_key: None,
|
public_key: None,
|
||||||
auto_logging_in_progress: false,
|
relay_ready: None,
|
||||||
|
need_backup: None,
|
||||||
|
need_onboarding: false,
|
||||||
|
logging_in: false,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,8 +111,11 @@ impl Identity {
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
} else {
|
} else {
|
||||||
this.update(cx, |this, cx| {
|
cx.update(|window, cx| {
|
||||||
this.set_public_key(None, cx);
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_public_key(None, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -129,19 +138,24 @@ impl Identity {
|
|||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| match task.await {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
Ok(_) => {
|
match task.await {
|
||||||
this.update(cx, |this, cx| {
|
Ok(_) => {
|
||||||
this.set_public_key(None, cx);
|
cx.update(|window, cx| {
|
||||||
})
|
this.update(cx, |this, cx| {
|
||||||
.ok();
|
this.set_public_key(None, window, cx);
|
||||||
}
|
})
|
||||||
Err(e) => {
|
.ok();
|
||||||
cx.update(|window, cx| {
|
})
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
.ok();
|
||||||
})
|
}
|
||||||
.ok();
|
Err(e) => {
|
||||||
}
|
cx.update(|window, cx| {
|
||||||
|
window.push_notification(Notification::error(e.to_string()), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
@@ -158,13 +172,13 @@ impl Identity {
|
|||||||
self.login_with_bunker(uri, window, cx);
|
self.login_with_bunker(uri, window, cx);
|
||||||
} else {
|
} else {
|
||||||
window.push_notification(Notification::error("Bunker URI is invalid"), cx);
|
window.push_notification(Notification::error("Bunker URI is invalid"), cx);
|
||||||
self.set_public_key(None, cx);
|
self.set_public_key(None, window, cx);
|
||||||
}
|
}
|
||||||
} else if let Ok(enc) = EncryptedSecretKey::from_bech32(secret) {
|
} else if let Ok(enc) = EncryptedSecretKey::from_bech32(secret) {
|
||||||
self.login_with_keys(enc, window, cx);
|
self.login_with_keys(enc, window, cx);
|
||||||
} else {
|
} else {
|
||||||
window.push_notification(Notification::error("Secret Key is invalid"), cx);
|
window.push_notification(Notification::error("Secret Key is invalid"), cx);
|
||||||
self.set_public_key(None, cx);
|
self.set_public_key(None, window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +196,7 @@ impl Identity {
|
|||||||
Notification::error("Bunker URI is invalid").title("Nostr Connect"),
|
Notification::error("Bunker URI is invalid").title("Nostr Connect"),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
self.set_public_key(None, cx);
|
self.set_public_key(None, window, cx);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// Automatically open auth url
|
// Automatically open auth url
|
||||||
@@ -204,7 +218,7 @@ impl Identity {
|
|||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
window.push_notification(Notification::error(e.to_string()), cx);
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_public_key(None, cx);
|
this.set_public_key(None, window, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
})
|
})
|
||||||
@@ -239,10 +253,10 @@ impl Identity {
|
|||||||
.show_close(false)
|
.show_close(false)
|
||||||
.keyboard(false)
|
.keyboard(false)
|
||||||
.confirm()
|
.confirm()
|
||||||
.on_cancel(move |_, _window, cx| {
|
.on_cancel(move |_, window, cx| {
|
||||||
entity
|
entity
|
||||||
.update(cx, |this, cx| {
|
.update(cx, |this, cx| {
|
||||||
this.set_public_key(None, cx);
|
this.set_public_key(None, window, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
// Close modal
|
// Close modal
|
||||||
@@ -341,8 +355,10 @@ impl Identity {
|
|||||||
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
||||||
let client = nostr_client();
|
let client = nostr_client();
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
// Update signer
|
// Update signer
|
||||||
client.set_signer(signer).await;
|
client.set_signer(signer).await;
|
||||||
|
|
||||||
// Subscribe for user metadata
|
// Subscribe for user metadata
|
||||||
Self::subscribe(client, public_key).await?;
|
Self::subscribe(client, public_key).await?;
|
||||||
|
|
||||||
@@ -352,8 +368,11 @@ impl Identity {
|
|||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
match task.await {
|
match task.await {
|
||||||
Ok(public_key) => {
|
Ok(public_key) => {
|
||||||
this.update(cx, |this, cx| {
|
cx.update(|window, cx| {
|
||||||
this.set_public_key(Some(public_key), cx);
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_public_key(Some(public_key), window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -368,10 +387,9 @@ impl Identity {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new identity with the given keys and metadata
|
/// Creates a new identity with the given metadata
|
||||||
pub fn new_identity(
|
pub fn new_identity(
|
||||||
&mut self,
|
&mut self,
|
||||||
password: String,
|
|
||||||
metadata: Metadata,
|
metadata: Metadata,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
@@ -382,34 +400,32 @@ impl Identity {
|
|||||||
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
||||||
let client = nostr_client();
|
let client = nostr_client();
|
||||||
let public_key = async_keys.public_key();
|
let public_key = async_keys.public_key();
|
||||||
|
|
||||||
// Update signer
|
// Update signer
|
||||||
client.set_signer(async_keys).await;
|
client.set_signer(async_keys).await;
|
||||||
|
|
||||||
// Set metadata
|
// Set metadata
|
||||||
client.set_metadata(&metadata).await?;
|
client.set_metadata(&metadata).await?;
|
||||||
|
|
||||||
// Create relay list
|
// Create relay list
|
||||||
let relay_list = EventBuilder::new(Kind::RelayList, "").tags(
|
let relay_list = EventBuilder::new(Kind::RelayList, "").tags(
|
||||||
NIP65_RELAYS.into_iter().filter_map(|url| {
|
NIP65_RELAYS
|
||||||
if let Ok(url) = RelayUrl::parse(url) {
|
.into_iter()
|
||||||
Some(Tag::relay_metadata(url, None))
|
.filter_map(|url| RelayUrl::parse(url).ok())
|
||||||
} else {
|
.map(|url| Tag::relay_metadata(url, None)),
|
||||||
None
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create messaging relay list
|
// Create messaging relay list
|
||||||
let dm_relay = EventBuilder::new(Kind::InboxRelays, "").tags(
|
let dm_relay = EventBuilder::new(Kind::InboxRelays, "").tags(
|
||||||
NIP17_RELAYS.into_iter().filter_map(|url| {
|
NIP17_RELAYS
|
||||||
if let Ok(url) = RelayUrl::parse(url) {
|
.into_iter()
|
||||||
Some(Tag::relay(url))
|
.filter_map(|url| RelayUrl::parse(url).ok())
|
||||||
} else {
|
.map(Tag::relay),
|
||||||
None
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Set user's NIP65 relays
|
||||||
client.send_event_builder(relay_list).await?;
|
client.send_event_builder(relay_list).await?;
|
||||||
|
// Set user's NIP17 relays
|
||||||
client.send_event_builder(dm_relay).await?;
|
client.send_event_builder(dm_relay).await?;
|
||||||
|
|
||||||
// Subscribe for user metadata
|
// Subscribe for user metadata
|
||||||
@@ -421,9 +437,13 @@ impl Identity {
|
|||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
match task.await {
|
match task.await {
|
||||||
Ok(public_key) => {
|
Ok(public_key) => {
|
||||||
this.update(cx, |this, cx| {
|
cx.update(|window, cx| {
|
||||||
this.write_keys(&keys, password, cx);
|
this.update(cx, |this, cx| {
|
||||||
this.set_public_key(Some(public_key), cx);
|
this.set_public_key(Some(public_key), window, cx);
|
||||||
|
this.set_need_backup(Some(keys), cx);
|
||||||
|
this.set_need_onboarding(cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -438,6 +458,40 @@ impl Identity {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear the user's need backup status
|
||||||
|
pub fn clear_need_backup(&mut self, password: String, cx: &mut Context<Self>) {
|
||||||
|
if let Some(keys) = self.need_backup.as_ref() {
|
||||||
|
// Encrypt the keys then writing them to keychain
|
||||||
|
self.write_keys(keys, password, cx);
|
||||||
|
// Clear the needed backup keys
|
||||||
|
self.need_backup = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the user's need backup status
|
||||||
|
pub(crate) fn set_need_backup(&mut self, keys: Option<Keys>, cx: &mut Context<Self>) {
|
||||||
|
self.need_backup = keys;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the user's need onboarding status
|
||||||
|
pub(crate) fn set_need_onboarding(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.need_onboarding = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the user needs backup their keys
|
||||||
|
pub fn need_backup(&self) -> Option<&Keys> {
|
||||||
|
self.need_backup.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the user needs onboarding
|
||||||
|
pub fn need_onboarding(&self) -> bool {
|
||||||
|
self.need_onboarding
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the bunker uri to the database
|
||||||
pub fn write_bunker(&self, uri: &NostrConnectURI, cx: &mut Context<Self>) {
|
pub fn write_bunker(&self, uri: &NostrConnectURI, cx: &mut Context<Self>) {
|
||||||
let mut value = uri.to_string();
|
let mut value = uri.to_string();
|
||||||
|
|
||||||
@@ -469,6 +523,7 @@ impl Identity {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes the keys to the database
|
||||||
pub fn write_keys(&self, keys: &Keys, password: String, cx: &mut Context<Self>) {
|
pub fn write_keys(&self, keys: &Keys, password: String, cx: &mut Context<Self>) {
|
||||||
let keys = keys.to_owned();
|
let keys = keys.to_owned();
|
||||||
let public_key = keys.public_key();
|
let public_key = keys.public_key();
|
||||||
@@ -495,9 +550,61 @@ impl Identity {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_public_key(&mut self, public_key: Option<PublicKey>, cx: &mut Context<Self>) {
|
fn verify_dm_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(public_key) = self.public_key() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let task: Task<bool> = cx.background_spawn(async move {
|
||||||
|
let client = nostr_client();
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::InboxRelays)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let Ok(events) = client.database().query(filter).await else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(event) = events.first() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let relays: Vec<RelayUrl> = event
|
||||||
|
.tags
|
||||||
|
.filter(TagKind::Relay)
|
||||||
|
.filter_map(|tag| RelayUrl::parse(tag.content()?).ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
!relays.is_empty()
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let result = task.await;
|
||||||
|
log::info!("result: {result}");
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.relay_ready = Some(result);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the public key of the identity
|
||||||
|
pub(crate) fn set_public_key(
|
||||||
|
&mut self,
|
||||||
|
public_key: Option<PublicKey>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
self.public_key = public_key;
|
self.public_key = public_key;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
// Run verify user's dm relays task
|
||||||
|
cx.defer_in(window, |this, window, cx| {
|
||||||
|
this.verify_dm_relays(window, cx);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current identity's public key
|
/// Returns the current identity's public key
|
||||||
@@ -510,12 +617,18 @@ impl Identity {
|
|||||||
self.public_key.is_some()
|
self.public_key.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logging_in(&self) -> bool {
|
pub fn relay_ready(&self) -> Option<bool> {
|
||||||
self.auto_logging_in_progress
|
self.relay_ready
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the identity is currently logging in
|
||||||
|
pub fn logging_in(&self) -> bool {
|
||||||
|
self.logging_in
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the logging in status of the identity
|
||||||
pub(crate) fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) {
|
pub(crate) fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
self.auto_logging_in_progress = status;
|
self.logging_in = status;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,20 @@ use colors::{brand, hsl, neutral};
|
|||||||
use gpui::{px, App, Global, Hsla, Pixels, SharedString, Window, WindowAppearance};
|
use gpui::{px, App, Global, Hsla, Pixels, SharedString, Window, WindowAppearance};
|
||||||
|
|
||||||
use crate::colors::{danger, warning};
|
use crate::colors::{danger, warning};
|
||||||
|
use crate::platform_kind::PlatformKind;
|
||||||
|
use crate::scrollbar_mode::ScrollBarMode;
|
||||||
|
|
||||||
mod colors;
|
mod colors;
|
||||||
mod scale;
|
mod scale;
|
||||||
|
|
||||||
|
pub mod platform_kind;
|
||||||
|
pub mod scrollbar_mode;
|
||||||
|
|
||||||
|
/// Defines window border radius for platforms that use client side decorations.
|
||||||
|
pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0);
|
||||||
|
/// Defines window shadow size for platforms that use client side decorations.
|
||||||
|
pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0);
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
Theme::sync_system_appearance(None, cx);
|
Theme::sync_system_appearance(None, cx);
|
||||||
}
|
}
|
||||||
@@ -21,7 +31,7 @@ pub struct ThemeColor {
|
|||||||
pub panel_background: Hsla,
|
pub panel_background: Hsla,
|
||||||
pub overlay: Hsla,
|
pub overlay: Hsla,
|
||||||
pub title_bar: Hsla,
|
pub title_bar: Hsla,
|
||||||
pub title_bar_border: Hsla,
|
pub title_bar_inactive: Hsla,
|
||||||
pub window_border: Hsla,
|
pub window_border: Hsla,
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
@@ -78,6 +88,7 @@ pub struct ThemeColor {
|
|||||||
|
|
||||||
// Ghost element colors
|
// Ghost element colors
|
||||||
pub ghost_element_background: Hsla,
|
pub ghost_element_background: Hsla,
|
||||||
|
pub ghost_element_background_alt: Hsla,
|
||||||
pub ghost_element_hover: Hsla,
|
pub ghost_element_hover: Hsla,
|
||||||
pub ghost_element_active: Hsla,
|
pub ghost_element_active: Hsla,
|
||||||
pub ghost_element_selected: Hsla,
|
pub ghost_element_selected: Hsla,
|
||||||
@@ -116,7 +127,7 @@ impl ThemeColor {
|
|||||||
panel_background: gpui::white(),
|
panel_background: gpui::white(),
|
||||||
overlay: neutral().light_alpha().step_3(),
|
overlay: neutral().light_alpha().step_3(),
|
||||||
title_bar: gpui::transparent_black(),
|
title_bar: gpui::transparent_black(),
|
||||||
title_bar_border: gpui::transparent_black(),
|
title_bar_inactive: neutral().light().step_1(),
|
||||||
window_border: hsl(240.0, 5.9, 78.0),
|
window_border: hsl(240.0, 5.9, 78.0),
|
||||||
|
|
||||||
border: neutral().light().step_6(),
|
border: neutral().light().step_6(),
|
||||||
@@ -158,16 +169,17 @@ impl ThemeColor {
|
|||||||
danger_disabled: danger().light_alpha().step_3(),
|
danger_disabled: danger().light_alpha().step_3(),
|
||||||
|
|
||||||
warning_foreground: warning().light().step_12(),
|
warning_foreground: warning().light().step_12(),
|
||||||
warning_background: warning().light().step_9(),
|
warning_background: warning().light().step_3(),
|
||||||
warning_hover: warning().light_alpha().step_10(),
|
warning_hover: warning().light_alpha().step_4(),
|
||||||
warning_active: warning().light().step_10(),
|
warning_active: warning().light().step_5(),
|
||||||
warning_selected: warning().light().step_11(),
|
warning_selected: warning().light().step_5(),
|
||||||
warning_disabled: warning().light_alpha().step_3(),
|
warning_disabled: warning().light_alpha().step_3(),
|
||||||
|
|
||||||
ghost_element_background: gpui::transparent_black(),
|
ghost_element_background: gpui::transparent_black(),
|
||||||
ghost_element_hover: neutral().light_alpha().step_3(),
|
ghost_element_background_alt: neutral().light().step_3(),
|
||||||
ghost_element_active: neutral().light_alpha().step_5(),
|
ghost_element_hover: neutral().light_alpha().step_4(),
|
||||||
ghost_element_selected: neutral().light_alpha().step_5(),
|
ghost_element_active: neutral().light().step_5(),
|
||||||
|
ghost_element_selected: neutral().light().step_5(),
|
||||||
ghost_element_disabled: neutral().light_alpha().step_2(),
|
ghost_element_disabled: neutral().light_alpha().step_2(),
|
||||||
|
|
||||||
tab_inactive_background: neutral().light().step_3(),
|
tab_inactive_background: neutral().light().step_3(),
|
||||||
@@ -197,7 +209,7 @@ impl ThemeColor {
|
|||||||
panel_background: gpui::black(),
|
panel_background: gpui::black(),
|
||||||
overlay: neutral().dark_alpha().step_3(),
|
overlay: neutral().dark_alpha().step_3(),
|
||||||
title_bar: gpui::transparent_black(),
|
title_bar: gpui::transparent_black(),
|
||||||
title_bar_border: gpui::transparent_black(),
|
title_bar_inactive: neutral().dark().step_1(),
|
||||||
window_border: hsl(240.0, 3.7, 28.0),
|
window_border: hsl(240.0, 3.7, 28.0),
|
||||||
|
|
||||||
border: neutral().dark().step_6(),
|
border: neutral().dark().step_6(),
|
||||||
@@ -239,16 +251,17 @@ impl ThemeColor {
|
|||||||
danger_disabled: danger().dark_alpha().step_3(),
|
danger_disabled: danger().dark_alpha().step_3(),
|
||||||
|
|
||||||
warning_foreground: warning().dark().step_12(),
|
warning_foreground: warning().dark().step_12(),
|
||||||
warning_background: warning().dark().step_9(),
|
warning_background: warning().dark().step_3(),
|
||||||
warning_hover: warning().dark_alpha().step_10(),
|
warning_hover: warning().dark_alpha().step_4(),
|
||||||
warning_active: warning().dark().step_10(),
|
warning_active: warning().dark().step_5(),
|
||||||
warning_selected: warning().dark().step_11(),
|
warning_selected: warning().dark().step_5(),
|
||||||
warning_disabled: warning().dark_alpha().step_3(),
|
warning_disabled: warning().dark_alpha().step_3(),
|
||||||
|
|
||||||
ghost_element_background: gpui::transparent_black(),
|
ghost_element_background: gpui::transparent_black(),
|
||||||
ghost_element_hover: neutral().dark_alpha().step_3(),
|
ghost_element_background_alt: neutral().dark().step_3(),
|
||||||
ghost_element_active: neutral().dark_alpha().step_4(),
|
ghost_element_hover: neutral().dark_alpha().step_4(),
|
||||||
ghost_element_selected: neutral().dark_alpha().step_5(),
|
ghost_element_active: neutral().dark().step_5(),
|
||||||
|
ghost_element_selected: neutral().dark().step_5(),
|
||||||
ghost_element_disabled: neutral().dark_alpha().step_2(),
|
ghost_element_disabled: neutral().dark_alpha().step_2(),
|
||||||
|
|
||||||
tab_inactive_background: neutral().dark().step_3(),
|
tab_inactive_background: neutral().dark().step_3(),
|
||||||
@@ -309,24 +322,6 @@ impl From<WindowAppearance> for ThemeMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
|
||||||
pub enum ScrollBarMode {
|
|
||||||
#[default]
|
|
||||||
Scrolling,
|
|
||||||
Hover,
|
|
||||||
Always,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScrollBarMode {
|
|
||||||
pub fn is_hover(&self) -> bool {
|
|
||||||
matches!(self, Self::Hover)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_always(&self) -> bool {
|
|
||||||
matches!(self, Self::Always)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
pub colors: ThemeColor,
|
pub colors: ThemeColor,
|
||||||
@@ -335,6 +330,7 @@ pub struct Theme {
|
|||||||
pub font_size: Pixels,
|
pub font_size: Pixels,
|
||||||
pub radius: Pixels,
|
pub radius: Pixels,
|
||||||
pub scrollbar_mode: ScrollBarMode,
|
pub scrollbar_mode: ScrollBarMode,
|
||||||
|
pub platform_kind: PlatformKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for Theme {
|
impl Deref for Theme {
|
||||||
@@ -412,6 +408,7 @@ impl From<ThemeColor> for Theme {
|
|||||||
font_family: ".SystemUIFont".into(),
|
font_family: ".SystemUIFont".into(),
|
||||||
radius: px(5.),
|
radius: px(5.),
|
||||||
scrollbar_mode: ScrollBarMode::default(),
|
scrollbar_mode: ScrollBarMode::default(),
|
||||||
|
platform_kind: PlatformKind::platform(),
|
||||||
mode,
|
mode,
|
||||||
colors,
|
colors,
|
||||||
}
|
}
|
||||||
|
|||||||
30
crates/theme/src/platform_kind.rs
Normal file
30
crates/theme/src/platform_kind.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
|
pub enum PlatformKind {
|
||||||
|
Mac,
|
||||||
|
Linux,
|
||||||
|
Windows,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlatformKind {
|
||||||
|
pub const fn platform() -> Self {
|
||||||
|
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||||
|
Self::Linux
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
Self::Windows
|
||||||
|
} else {
|
||||||
|
Self::Mac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_linux(&self) -> bool {
|
||||||
|
matches!(self, Self::Linux)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_windows(&self) -> bool {
|
||||||
|
matches!(self, Self::Windows)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_mac(&self) -> bool {
|
||||||
|
matches!(self, Self::Mac)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
crates/theme/src/scrollbar_mode.rs
Normal file
21
crates/theme/src/scrollbar_mode.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub enum ScrollBarMode {
|
||||||
|
#[default]
|
||||||
|
Scrolling,
|
||||||
|
Hover,
|
||||||
|
Always,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScrollBarMode {
|
||||||
|
pub fn is_scrolling(&self) -> bool {
|
||||||
|
matches!(self, Self::Scrolling)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_hover(&self) -> bool {
|
||||||
|
matches!(self, Self::Hover)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_always(&self) -> bool {
|
||||||
|
matches!(self, Self::Always)
|
||||||
|
}
|
||||||
|
}
|
||||||
25
crates/title_bar/Cargo.toml
Normal file
25
crates/title_bar/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "title_bar"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
ui = { path = "../ui" }
|
||||||
|
|
||||||
|
rust-i18n.workspace = true
|
||||||
|
i18n.workspace = true
|
||||||
|
nostr-sdk.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
windows = { version = "0.61", features = ["Wdk_System_SystemServices"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
linicon = "2.3.0"
|
||||||
178
crates/title_bar/src/lib.rs
Normal file
178
crates/title_bar/src/lib.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use gpui::MouseButton;
|
||||||
|
use gpui::{
|
||||||
|
div, px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement,
|
||||||
|
ParentElement, Pixels, Render, StatefulInteractiveElement as _, Styled, Window,
|
||||||
|
WindowControlArea,
|
||||||
|
};
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use theme::platform_kind::PlatformKind;
|
||||||
|
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
|
||||||
|
use ui::h_flex;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use crate::platforms::linux::LinuxWindowControls;
|
||||||
|
use crate::platforms::windows::WindowsWindowControls;
|
||||||
|
|
||||||
|
mod platforms;
|
||||||
|
|
||||||
|
pub struct TitleBar {
|
||||||
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
should_move: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TitleBar {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TitleBar {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
children: smallvec![],
|
||||||
|
should_move: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub fn height(window: &mut Window) -> Pixels {
|
||||||
|
(1.75 * window.rem_size()).max(px(34.))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn height(_window: &mut Window) -> Pixels {
|
||||||
|
px(32.)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
|
||||||
|
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||||
|
if window.is_window_active() && !self.should_move {
|
||||||
|
cx.theme().title_bar
|
||||||
|
} else {
|
||||||
|
cx.theme().title_bar_inactive
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cx.theme().title_bar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_children<T>(&mut self, children: T)
|
||||||
|
where
|
||||||
|
T: IntoIterator<Item = AnyElement>,
|
||||||
|
{
|
||||||
|
self.children = children.into_iter().collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for TitleBar {
|
||||||
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||||
|
self.children.extend(elements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TitleBar {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let supported_controls = window.window_controls();
|
||||||
|
let decorations = window.window_decorations();
|
||||||
|
let height = Self::height(window);
|
||||||
|
let color = self.title_bar_color(window, cx);
|
||||||
|
let children = mem::take(&mut self.children);
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.window_control_area(WindowControlArea::Drag)
|
||||||
|
.w_full()
|
||||||
|
.h(height)
|
||||||
|
.map(|this| {
|
||||||
|
if window.is_fullscreen() {
|
||||||
|
this.pl_2()
|
||||||
|
} else if cx.theme().platform_kind.is_mac() {
|
||||||
|
this.pl(px(platforms::mac::TRAFFIC_LIGHT_PADDING))
|
||||||
|
} else {
|
||||||
|
this.pl_2()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|this| match decorations {
|
||||||
|
Decorations::Server => this,
|
||||||
|
Decorations::Client { tiling, .. } => this
|
||||||
|
.when(!(tiling.top || tiling.right), |el| {
|
||||||
|
el.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
})
|
||||||
|
.when(!(tiling.top || tiling.left), |el| {
|
||||||
|
el.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.bg(color)
|
||||||
|
.content_stretch()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id("title-bar")
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.w_full()
|
||||||
|
.when(cx.theme().platform_kind.is_mac(), |this| {
|
||||||
|
this.on_click(|event, window, _| {
|
||||||
|
if event.up.click_count == 2 {
|
||||||
|
window.titlebar_double_click();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.when(cx.theme().platform_kind.is_linux(), |this| {
|
||||||
|
this.on_click(|event, window, _| {
|
||||||
|
if event.up.click_count == 2 {
|
||||||
|
window.zoom_window();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.children(children),
|
||||||
|
)
|
||||||
|
.when(!window.is_fullscreen(), |this| {
|
||||||
|
match cx.theme().platform_kind {
|
||||||
|
PlatformKind::Linux => {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if matches!(decorations, Decorations::Client { .. }) {
|
||||||
|
this.child(LinuxWindowControls::new(None))
|
||||||
|
.when(supported_controls.window_menu, |this| {
|
||||||
|
this.on_mouse_down(MouseButton::Right, move |ev, window, _| {
|
||||||
|
window.show_window_menu(ev.position)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on_mouse_move(cx.listener(move |this, _ev, window, _| {
|
||||||
|
if this.should_move {
|
||||||
|
this.should_move = false;
|
||||||
|
window.start_window_move();
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
|
||||||
|
this.should_move = false;
|
||||||
|
}))
|
||||||
|
.on_mouse_up(
|
||||||
|
MouseButton::Left,
|
||||||
|
cx.listener(move |this, _ev, _window, _cx| {
|
||||||
|
this.should_move = false;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.on_mouse_down(
|
||||||
|
MouseButton::Left,
|
||||||
|
cx.listener(move |this, _ev, _window, _cx| {
|
||||||
|
this.should_move = true;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
this
|
||||||
|
}
|
||||||
|
PlatformKind::Windows => this.child(WindowsWindowControls::new(height)),
|
||||||
|
PlatformKind::Mac => this,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
229
crates/title_bar/src/platforms/linux.rs
Normal file
229
crates/title_bar/src/platforms/linux.rs
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
img, Action, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
|
||||||
|
StatefulInteractiveElement, Styled, Window,
|
||||||
|
};
|
||||||
|
use linicon::{lookup_icon, IconType};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::{h_flex, Icon, IconName, Sizable};
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct LinuxWindowControls {
|
||||||
|
close_window_action: Option<Box<dyn Action>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinuxWindowControls {
|
||||||
|
pub fn new(close_window_action: Option<Box<dyn Action>>) -> Self {
|
||||||
|
Self {
|
||||||
|
close_window_action,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for LinuxWindowControls {
|
||||||
|
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
|
h_flex()
|
||||||
|
.id("linux-window-controls")
|
||||||
|
.px_2()
|
||||||
|
.gap_2()
|
||||||
|
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||||
|
.child(WindowControl::new(
|
||||||
|
LinuxControl::Minimize,
|
||||||
|
IconName::WindowMinimize,
|
||||||
|
))
|
||||||
|
.child({
|
||||||
|
if window.is_maximized() {
|
||||||
|
WindowControl::new(LinuxControl::Restore, IconName::WindowRestore)
|
||||||
|
} else {
|
||||||
|
WindowControl::new(LinuxControl::Maximize, IconName::WindowMaximize)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
WindowControl::new(LinuxControl::Close, IconName::WindowClose)
|
||||||
|
.when_some(self.close_window_action, |this, close_action| {
|
||||||
|
this.close_action(close_action)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct WindowControl {
|
||||||
|
kind: LinuxControl,
|
||||||
|
fallback: IconName,
|
||||||
|
close_action: Option<Box<dyn Action>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowControl {
|
||||||
|
pub fn new(kind: LinuxControl, fallback: IconName) -> Self {
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
fallback,
|
||||||
|
close_action: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_action(mut self, action: Box<dyn Action>) -> Self {
|
||||||
|
self.close_action = Some(action);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_gnome(&self) -> bool {
|
||||||
|
matches!(detect_desktop_environment(), DesktopEnvironment::Gnome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for WindowControl {
|
||||||
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let is_gnome = self.is_gnome();
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.id(self.kind.as_icon_name())
|
||||||
|
.group("")
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.rounded_full()
|
||||||
|
.map(|this| {
|
||||||
|
if is_gnome {
|
||||||
|
this.size_6()
|
||||||
|
.bg(cx.theme().tab_inactive_background)
|
||||||
|
.hover(|this| this.bg(cx.theme().tab_hover_background))
|
||||||
|
.active(|this| this.bg(cx.theme().tab_active_background))
|
||||||
|
} else {
|
||||||
|
this.size_5()
|
||||||
|
.bg(cx.theme().ghost_element_background)
|
||||||
|
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||||
|
.active(|this| this.bg(cx.theme().ghost_element_active))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|this| {
|
||||||
|
if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() {
|
||||||
|
this.child(img(path).flex_grow().size_4())
|
||||||
|
} else {
|
||||||
|
this.child(Icon::new(self.fallback).flex_grow().small())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_mouse_move(|_, _window, cx| cx.stop_propagation())
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
cx.stop_propagation();
|
||||||
|
match self.kind {
|
||||||
|
LinuxControl::Minimize => window.minimize_window(),
|
||||||
|
LinuxControl::Restore => window.zoom_window(),
|
||||||
|
LinuxControl::Maximize => window.zoom_window(),
|
||||||
|
LinuxControl::Close => window.dispatch_action(
|
||||||
|
self.close_action
|
||||||
|
.as_ref()
|
||||||
|
.expect("Use WindowControl::new_close() for close control.")
|
||||||
|
.boxed_clone(),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static DE: OnceLock<DesktopEnvironment> = OnceLock::new();
|
||||||
|
static LINUX_CONTROLS: OnceLock<HashMap<LinuxControl, Option<PathBuf>>> = OnceLock::new();
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum DesktopEnvironment {
|
||||||
|
Gnome,
|
||||||
|
Kde,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect the current desktop environment
|
||||||
|
pub fn detect_desktop_environment() -> &'static DesktopEnvironment {
|
||||||
|
DE.get_or_init(|| {
|
||||||
|
// Try to use environment variables first
|
||||||
|
if let Ok(output) = std::env::var("XDG_CURRENT_DESKTOP") {
|
||||||
|
let desktop = output.to_lowercase();
|
||||||
|
if desktop.contains("gnome") {
|
||||||
|
return DesktopEnvironment::Gnome;
|
||||||
|
} else if desktop.contains("kde") {
|
||||||
|
return DesktopEnvironment::Kde;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback detection methods
|
||||||
|
if let Ok(output) = std::env::var("DESKTOP_SESSION") {
|
||||||
|
let session = output.to_lowercase();
|
||||||
|
if session.contains("gnome") {
|
||||||
|
return DesktopEnvironment::Gnome;
|
||||||
|
} else if session.contains("kde") || session.contains("plasma") {
|
||||||
|
return DesktopEnvironment::Kde;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopEnvironment::Unknown
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
|
pub enum LinuxControl {
|
||||||
|
Minimize,
|
||||||
|
Restore,
|
||||||
|
Maximize,
|
||||||
|
Close,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinuxControl {
|
||||||
|
pub fn as_icon_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
LinuxControl::Close => "window-close",
|
||||||
|
LinuxControl::Minimize => "window-minimize",
|
||||||
|
LinuxControl::Maximize => "window-maximize",
|
||||||
|
LinuxControl::Restore => "window-restore",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linux_controls() -> &'static HashMap<LinuxControl, Option<PathBuf>> {
|
||||||
|
LINUX_CONTROLS.get_or_init(|| {
|
||||||
|
let mut icons = HashMap::new();
|
||||||
|
icons.insert(LinuxControl::Close, None);
|
||||||
|
icons.insert(LinuxControl::Minimize, None);
|
||||||
|
icons.insert(LinuxControl::Maximize, None);
|
||||||
|
icons.insert(LinuxControl::Restore, None);
|
||||||
|
|
||||||
|
let icon_names = [
|
||||||
|
(LinuxControl::Close, vec!["window-close", "dialog-close"]),
|
||||||
|
(
|
||||||
|
LinuxControl::Minimize,
|
||||||
|
vec!["window-minimize", "window-lower"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
LinuxControl::Maximize,
|
||||||
|
vec!["window-maximize", "window-expand"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
LinuxControl::Restore,
|
||||||
|
vec!["window-restore", "window-return"],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (control, icon_names) in icon_names {
|
||||||
|
for icon_name in icon_names {
|
||||||
|
// Try GNOME-style naming first
|
||||||
|
let mut control_icon = lookup_icon(format!("{icon_name}-symbolic"))
|
||||||
|
.find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG));
|
||||||
|
|
||||||
|
// If not found, try KDE-style naming
|
||||||
|
if control_icon.is_none() {
|
||||||
|
control_icon = lookup_icon(icon_name)
|
||||||
|
.find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Ok(icon)) = control_icon {
|
||||||
|
icons.entry(control).and_modify(|v| *v = Some(icon.path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
icons
|
||||||
|
})
|
||||||
|
}
|
||||||
6
crates/title_bar/src/platforms/mac.rs
Normal file
6
crates/title_bar/src/platforms/mac.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/// Use pixels here instead of a rem-based size because the macOS traffic
|
||||||
|
/// lights are a static size, and don't scale with the rest of the UI.
|
||||||
|
///
|
||||||
|
/// Magic number: There is one extra pixel of padding on the left side due to
|
||||||
|
/// the 1px border around the window on macOS apps.
|
||||||
|
pub const TRAFFIC_LIGHT_PADDING: f32 = 71.;
|
||||||
4
crates/title_bar/src/platforms/mod.rs
Normal file
4
crates/title_bar/src/platforms/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub mod linux;
|
||||||
|
pub mod mac;
|
||||||
|
pub mod windows;
|
||||||
147
crates/title_bar/src/platforms/windows.rs
Normal file
147
crates/title_bar/src/platforms/windows.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, px, App, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels,
|
||||||
|
RenderOnce, Rgba, StatefulInteractiveElement, Styled, Window, WindowControlArea,
|
||||||
|
};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::h_flex;
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct WindowsWindowControls {
|
||||||
|
button_height: Pixels,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowsWindowControls {
|
||||||
|
pub fn new(button_height: Pixels) -> Self {
|
||||||
|
Self { button_height }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn get_font() -> &'static str {
|
||||||
|
"Segoe Fluent Icons"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn get_font() -> &'static str {
|
||||||
|
use windows::Wdk::System::SystemServices::RtlGetVersion;
|
||||||
|
|
||||||
|
let mut version = unsafe { std::mem::zeroed() };
|
||||||
|
let status = unsafe { RtlGetVersion(&mut version) };
|
||||||
|
|
||||||
|
if status.is_ok() && version.dwBuildNumber >= 22000 {
|
||||||
|
"Segoe Fluent Icons"
|
||||||
|
} else {
|
||||||
|
"Segoe MDL2 Assets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for WindowsWindowControls {
|
||||||
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let close_button_hover_color = Rgba {
|
||||||
|
r: 232.0 / 255.0,
|
||||||
|
g: 17.0 / 255.0,
|
||||||
|
b: 32.0 / 255.0,
|
||||||
|
a: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let button_hover_color = cx.theme().ghost_element_hover;
|
||||||
|
let button_active_color = cx.theme().ghost_element_active;
|
||||||
|
|
||||||
|
div()
|
||||||
|
.id("windows-window-controls")
|
||||||
|
.font_family(Self::get_font())
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.justify_center()
|
||||||
|
.content_stretch()
|
||||||
|
.max_h(self.button_height)
|
||||||
|
.min_h(self.button_height)
|
||||||
|
.child(WindowsCaptionButton::new(
|
||||||
|
"minimize",
|
||||||
|
WindowsCaptionButtonIcon::Minimize,
|
||||||
|
button_hover_color,
|
||||||
|
button_active_color,
|
||||||
|
))
|
||||||
|
.child(WindowsCaptionButton::new(
|
||||||
|
"maximize-or-restore",
|
||||||
|
if window.is_maximized() {
|
||||||
|
WindowsCaptionButtonIcon::Restore
|
||||||
|
} else {
|
||||||
|
WindowsCaptionButtonIcon::Maximize
|
||||||
|
},
|
||||||
|
button_hover_color,
|
||||||
|
button_active_color,
|
||||||
|
))
|
||||||
|
.child(WindowsCaptionButton::new(
|
||||||
|
"close",
|
||||||
|
WindowsCaptionButtonIcon::Close,
|
||||||
|
close_button_hover_color,
|
||||||
|
button_active_color,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
|
enum WindowsCaptionButtonIcon {
|
||||||
|
Minimize,
|
||||||
|
Restore,
|
||||||
|
Maximize,
|
||||||
|
Close,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
struct WindowsCaptionButton {
|
||||||
|
id: ElementId,
|
||||||
|
icon: WindowsCaptionButtonIcon,
|
||||||
|
hover_background_color: Hsla,
|
||||||
|
active_background_color: Hsla,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowsCaptionButton {
|
||||||
|
pub fn new(
|
||||||
|
id: impl Into<ElementId>,
|
||||||
|
icon: WindowsCaptionButtonIcon,
|
||||||
|
hover_background_color: impl Into<Hsla>,
|
||||||
|
active_background_color: impl Into<Hsla>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
icon,
|
||||||
|
hover_background_color: hover_background_color.into(),
|
||||||
|
active_background_color: active_background_color.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for WindowsCaptionButton {
|
||||||
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
|
h_flex()
|
||||||
|
.id(self.id)
|
||||||
|
.justify_center()
|
||||||
|
.content_center()
|
||||||
|
.occlude()
|
||||||
|
.w(px(36.))
|
||||||
|
.h_full()
|
||||||
|
.text_size(px(10.0))
|
||||||
|
.hover(|style| style.bg(self.hover_background_color))
|
||||||
|
.active(|style| style.bg(self.active_background_color))
|
||||||
|
.map(|this| match self.icon {
|
||||||
|
WindowsCaptionButtonIcon::Close => {
|
||||||
|
this.window_control_area(WindowControlArea::Close)
|
||||||
|
}
|
||||||
|
WindowsCaptionButtonIcon::Maximize | WindowsCaptionButtonIcon::Restore => {
|
||||||
|
this.window_control_area(WindowControlArea::Max)
|
||||||
|
}
|
||||||
|
WindowsCaptionButtonIcon::Minimize => {
|
||||||
|
this.window_control_area(WindowControlArea::Min)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(match self.icon {
|
||||||
|
WindowsCaptionButtonIcon::Minimize => "\u{e921}",
|
||||||
|
WindowsCaptionButtonIcon::Restore => "\u{e923}",
|
||||||
|
WindowsCaptionButtonIcon::Maximize => "\u{e922}",
|
||||||
|
WindowsCaptionButtonIcon::Close => "\u{e8bb}",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ use theme::ActiveTheme;
|
|||||||
|
|
||||||
use crate::indicator::Indicator;
|
use crate::indicator::Indicator;
|
||||||
use crate::tooltip::Tooltip;
|
use crate::tooltip::Tooltip;
|
||||||
use crate::{Disableable, Icon, Selectable, Sizable, Size, StyledExt};
|
use crate::{h_flex, Disableable, Icon, Selectable, Sizable, Size, StyledExt};
|
||||||
|
|
||||||
pub enum ButtonRounded {
|
pub enum ButtonRounded {
|
||||||
Normal,
|
Normal,
|
||||||
@@ -48,7 +48,12 @@ pub trait ButtonVariants: Sized {
|
|||||||
|
|
||||||
/// With the ghost style for the Button.
|
/// With the ghost style for the Button.
|
||||||
fn ghost(self) -> Self {
|
fn ghost(self) -> Self {
|
||||||
self.with_variant(ButtonVariant::Ghost)
|
self.with_variant(ButtonVariant::Ghost { alt: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// With the ghost style for the Button.
|
||||||
|
fn ghost_alt(self) -> Self {
|
||||||
|
self.with_variant(ButtonVariant::Ghost { alt: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// With the transparent style for the Button.
|
/// With the transparent style for the Button.
|
||||||
@@ -100,7 +105,7 @@ pub enum ButtonVariant {
|
|||||||
Secondary,
|
Secondary,
|
||||||
Danger,
|
Danger,
|
||||||
Warning,
|
Warning,
|
||||||
Ghost,
|
Ghost { alt: bool },
|
||||||
Transparent,
|
Transparent,
|
||||||
Custom(ButtonCustomVariant),
|
Custom(ButtonCustomVariant),
|
||||||
}
|
}
|
||||||
@@ -118,19 +123,26 @@ type OnClick = Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>
|
|||||||
pub struct Button {
|
pub struct Button {
|
||||||
pub base: Div,
|
pub base: Div,
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
label: Option<SharedString>,
|
label: Option<SharedString>,
|
||||||
|
tooltip: Option<SharedString>,
|
||||||
children: Vec<AnyElement>,
|
children: Vec<AnyElement>,
|
||||||
disabled: bool,
|
|
||||||
variant: ButtonVariant,
|
variant: ButtonVariant,
|
||||||
rounded: ButtonRounded,
|
rounded: ButtonRounded,
|
||||||
size: Size,
|
size: Size,
|
||||||
|
|
||||||
|
disabled: bool,
|
||||||
reverse: bool,
|
reverse: bool,
|
||||||
bold: bool,
|
bold: bool,
|
||||||
tooltip: Option<SharedString>,
|
cta: bool,
|
||||||
on_click: OnClick,
|
|
||||||
loading: bool,
|
loading: bool,
|
||||||
loading_icon: Option<Icon>,
|
loading_icon: Option<Icon>,
|
||||||
|
|
||||||
|
on_click: OnClick,
|
||||||
|
|
||||||
pub(crate) selected: bool,
|
pub(crate) selected: bool,
|
||||||
pub(crate) stop_propagation: bool,
|
pub(crate) stop_propagation: bool,
|
||||||
}
|
}
|
||||||
@@ -159,6 +171,7 @@ impl Button {
|
|||||||
loading: false,
|
loading: false,
|
||||||
reverse: false,
|
reverse: false,
|
||||||
bold: false,
|
bold: false,
|
||||||
|
cta: false,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
loading_icon: None,
|
loading_icon: None,
|
||||||
}
|
}
|
||||||
@@ -200,28 +213,38 @@ impl Button {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set bold the button (label will be use the semi-bold font).
|
||||||
pub fn bold(mut self) -> Self {
|
pub fn bold(mut self) -> Self {
|
||||||
self.bold = true;
|
self.bold = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_click(
|
/// Set the cta style of the button.
|
||||||
mut self,
|
pub fn cta(mut self) -> Self {
|
||||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
self.cta = true;
|
||||||
) -> Self {
|
|
||||||
self.on_click = Some(Box::new(handler));
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the stop propagation of the button.
|
||||||
pub fn stop_propagation(mut self, val: bool) -> Self {
|
pub fn stop_propagation(mut self, val: bool) -> Self {
|
||||||
self.stop_propagation = val;
|
self.stop_propagation = val;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the loading icon of the button.
|
||||||
pub fn loading_icon(mut self, icon: impl Into<Icon>) -> Self {
|
pub fn loading_icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||||
self.loading_icon = Some(icon.into());
|
self.loading_icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the click handler of the button.
|
||||||
|
pub fn on_click<C>(mut self, handler: C) -> Self
|
||||||
|
where
|
||||||
|
C: Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||||
|
{
|
||||||
|
self.on_click = Some(Box::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Disableable for Button {
|
impl Disableable for Button {
|
||||||
@@ -280,7 +303,7 @@ impl RenderOnce for Button {
|
|||||||
let normal_style = style.normal(window, cx);
|
let normal_style = style.normal(window, cx);
|
||||||
let icon_size = match self.size {
|
let icon_size = match self.size {
|
||||||
Size::Size(v) => Size::Size(v * 0.75),
|
Size::Size(v) => Size::Size(v * 0.75),
|
||||||
Size::Medium => Size::Small,
|
Size::Large => Size::Medium,
|
||||||
_ => self.size,
|
_ => self.size,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -300,25 +323,67 @@ impl RenderOnce for Button {
|
|||||||
// Icon Button
|
// Icon Button
|
||||||
match self.size {
|
match self.size {
|
||||||
Size::Size(px) => this.size(px),
|
Size::Size(px) => this.size(px),
|
||||||
Size::XSmall => this.size_5(),
|
Size::XSmall => {
|
||||||
Size::Small => this.size_6(),
|
if self.cta {
|
||||||
Size::Medium => this.size_7(),
|
this.w_10().h_5()
|
||||||
_ => this.size_9(),
|
} else {
|
||||||
|
this.size_5()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Size::Small => {
|
||||||
|
if self.cta {
|
||||||
|
this.w_12().h_6()
|
||||||
|
} else {
|
||||||
|
this.size_6()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Size::Medium => {
|
||||||
|
if self.cta {
|
||||||
|
this.w_12().h_7()
|
||||||
|
} else {
|
||||||
|
this.size_7()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if self.cta {
|
||||||
|
this.w_16().h_9()
|
||||||
|
} else {
|
||||||
|
this.size_9()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Normal Button
|
// Normal Button
|
||||||
match self.size {
|
match self.size {
|
||||||
Size::Size(size) => this.px(size * 0.2),
|
Size::Size(size) => this.px(size * 0.2),
|
||||||
Size::XSmall => this.h_6().px_2(),
|
Size::XSmall => {
|
||||||
Size::Small => {
|
|
||||||
if self.icon.is_some() {
|
if self.icon.is_some() {
|
||||||
this.h_7().pl_2().pr_3()
|
this.h_6().pl_2().pr_2p5()
|
||||||
} else {
|
} else {
|
||||||
this.h_7().px_3()
|
this.h_6().px_2()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Size::Small => {
|
||||||
|
if self.icon.is_some() {
|
||||||
|
this.h_7().pl_2().pr_2p5()
|
||||||
|
} else {
|
||||||
|
this.h_7().px_2()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Size::Medium => {
|
||||||
|
if self.icon.is_some() {
|
||||||
|
this.h_8().pl_3().pr_3p5()
|
||||||
|
} else {
|
||||||
|
this.h_8().px_3()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Size::Large => {
|
||||||
|
if self.icon.is_some() {
|
||||||
|
this.h_10().px_3().pr_3p5()
|
||||||
|
} else {
|
||||||
|
this.h_10().px_3()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Size::Medium => this.h_8().px_3(),
|
|
||||||
Size::Large => this.h_10().px_4(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -346,17 +411,14 @@ impl RenderOnce for Button {
|
|||||||
.shadow_none()
|
.shadow_none()
|
||||||
})
|
})
|
||||||
.child({
|
.child({
|
||||||
div()
|
h_flex()
|
||||||
.flex()
|
|
||||||
.when(self.reverse, |this| this.flex_row_reverse())
|
|
||||||
.id("label")
|
.id("label")
|
||||||
.items_center()
|
.when(self.reverse, |this| this.flex_row_reverse())
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.text_sm()
|
|
||||||
.map(|this| match self.size {
|
.map(|this| match self.size {
|
||||||
Size::XSmall => this.gap_0p5(),
|
Size::XSmall => this.text_xs().gap_1(),
|
||||||
Size::Small => this.gap_1(),
|
Size::Small => this.text_sm().gap_1p5(),
|
||||||
_ => this.gap_2().font_medium(),
|
_ => this.text_sm().gap_2(),
|
||||||
})
|
})
|
||||||
.when(!self.loading, |this| {
|
.when(!self.loading, |this| {
|
||||||
this.when_some(self.icon, |this, icon| {
|
this.when_some(self.icon, |this, icon| {
|
||||||
@@ -421,8 +483,15 @@ impl ButtonVariant {
|
|||||||
ButtonVariant::Secondary => cx.theme().elevated_surface_background,
|
ButtonVariant::Secondary => cx.theme().elevated_surface_background,
|
||||||
ButtonVariant::Danger => cx.theme().danger_background,
|
ButtonVariant::Danger => cx.theme().danger_background,
|
||||||
ButtonVariant::Warning => cx.theme().warning_background,
|
ButtonVariant::Warning => cx.theme().warning_background,
|
||||||
|
ButtonVariant::Ghost { alt } => {
|
||||||
|
if *alt {
|
||||||
|
cx.theme().ghost_element_background_alt
|
||||||
|
} else {
|
||||||
|
cx.theme().ghost_element_background
|
||||||
|
}
|
||||||
|
}
|
||||||
ButtonVariant::Custom(colors) => colors.color,
|
ButtonVariant::Custom(colors) => colors.color,
|
||||||
_ => cx.theme().ghost_element_background,
|
_ => gpui::transparent_black(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +502,13 @@ impl ButtonVariant {
|
|||||||
ButtonVariant::Danger => cx.theme().danger_foreground,
|
ButtonVariant::Danger => cx.theme().danger_foreground,
|
||||||
ButtonVariant::Warning => cx.theme().warning_foreground,
|
ButtonVariant::Warning => cx.theme().warning_foreground,
|
||||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||||
ButtonVariant::Ghost => cx.theme().text_muted,
|
ButtonVariant::Ghost { alt } => {
|
||||||
|
if *alt {
|
||||||
|
cx.theme().text
|
||||||
|
} else {
|
||||||
|
cx.theme().text_muted
|
||||||
|
}
|
||||||
|
}
|
||||||
ButtonVariant::Custom(colors) => colors.foreground,
|
ButtonVariant::Custom(colors) => colors.foreground,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -444,14 +519,14 @@ impl ButtonVariant {
|
|||||||
ButtonVariant::Secondary => cx.theme().secondary_hover,
|
ButtonVariant::Secondary => cx.theme().secondary_hover,
|
||||||
ButtonVariant::Danger => cx.theme().danger_hover,
|
ButtonVariant::Danger => cx.theme().danger_hover,
|
||||||
ButtonVariant::Warning => cx.theme().warning_hover,
|
ButtonVariant::Warning => cx.theme().warning_hover,
|
||||||
ButtonVariant::Ghost => cx.theme().ghost_element_hover,
|
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_hover,
|
||||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||||
ButtonVariant::Custom(colors) => colors.hover,
|
ButtonVariant::Custom(colors) => colors.hover,
|
||||||
};
|
};
|
||||||
|
|
||||||
let fg = match self {
|
let fg = match self {
|
||||||
ButtonVariant::Secondary => cx.theme().secondary_foreground,
|
ButtonVariant::Secondary => cx.theme().secondary_foreground,
|
||||||
ButtonVariant::Ghost => cx.theme().text,
|
ButtonVariant::Ghost { .. } => cx.theme().text,
|
||||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||||
_ => self.text_color(window, cx),
|
_ => self.text_color(window, cx),
|
||||||
};
|
};
|
||||||
@@ -465,7 +540,7 @@ impl ButtonVariant {
|
|||||||
ButtonVariant::Secondary => cx.theme().secondary_active,
|
ButtonVariant::Secondary => cx.theme().secondary_active,
|
||||||
ButtonVariant::Danger => cx.theme().danger_active,
|
ButtonVariant::Danger => cx.theme().danger_active,
|
||||||
ButtonVariant::Warning => cx.theme().warning_active,
|
ButtonVariant::Warning => cx.theme().warning_active,
|
||||||
ButtonVariant::Ghost => cx.theme().ghost_element_active,
|
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_active,
|
||||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||||
ButtonVariant::Custom(colors) => colors.active,
|
ButtonVariant::Custom(colors) => colors.active,
|
||||||
};
|
};
|
||||||
@@ -485,7 +560,7 @@ impl ButtonVariant {
|
|||||||
ButtonVariant::Secondary => cx.theme().secondary_selected,
|
ButtonVariant::Secondary => cx.theme().secondary_selected,
|
||||||
ButtonVariant::Danger => cx.theme().danger_selected,
|
ButtonVariant::Danger => cx.theme().danger_selected,
|
||||||
ButtonVariant::Warning => cx.theme().warning_selected,
|
ButtonVariant::Warning => cx.theme().warning_selected,
|
||||||
ButtonVariant::Ghost => cx.theme().ghost_element_selected,
|
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_selected,
|
||||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||||
ButtonVariant::Custom(colors) => colors.active,
|
ButtonVariant::Custom(colors) => colors.active,
|
||||||
};
|
};
|
||||||
@@ -501,7 +576,9 @@ impl ButtonVariant {
|
|||||||
|
|
||||||
fn disabled(&self, _window: &Window, cx: &App) -> ButtonVariantStyle {
|
fn disabled(&self, _window: &Window, cx: &App) -> ButtonVariantStyle {
|
||||||
let bg = match self {
|
let bg = match self {
|
||||||
ButtonVariant::Ghost => cx.theme().ghost_element_disabled,
|
ButtonVariant::Danger => cx.theme().danger_disabled,
|
||||||
|
ButtonVariant::Warning => cx.theme().warning_disabled,
|
||||||
|
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_disabled,
|
||||||
ButtonVariant::Secondary => cx.theme().secondary_disabled,
|
ButtonVariant::Secondary => cx.theme().secondary_disabled,
|
||||||
_ => cx.theme().element_disabled,
|
_ => cx.theme().element_disabled,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,34 +1,21 @@
|
|||||||
use std::rc::Rc;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, Action, App, AppContext, Corner, Element, InteractiveElement, IntoElement,
|
div, px, App, AppContext, Corner, Element, InteractiveElement, IntoElement, ParentElement,
|
||||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, WeakEntity,
|
RenderOnce, SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
|
||||||
Window,
|
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::button::{Button, ButtonVariants};
|
use crate::button::{Button, ButtonVariants};
|
||||||
use crate::input::InputState;
|
use crate::input::InputState;
|
||||||
use crate::popover::{Popover, PopoverContent};
|
use crate::popover::{Popover, PopoverContent};
|
||||||
use crate::Icon;
|
use crate::{Icon, Sizable, Size};
|
||||||
|
|
||||||
/// Emit a emoji to target input
|
static EMOJIS: OnceLock<Vec<SharedString>> = OnceLock::new();
|
||||||
#[derive(Action, PartialEq, Clone, Debug, Deserialize)]
|
|
||||||
#[action(namespace = emoji, no_json)]
|
|
||||||
pub struct EmitEmoji(pub SharedString);
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
fn get_emojis() -> &'static Vec<SharedString> {
|
||||||
pub struct EmojiPicker {
|
EMOJIS.get_or_init(|| {
|
||||||
icon: Option<Icon>,
|
|
||||||
anchor: Option<Corner>,
|
|
||||||
target_input: WeakEntity<InputState>,
|
|
||||||
emojis: Rc<Vec<SharedString>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EmojiPicker {
|
|
||||||
pub fn new(target_input: WeakEntity<InputState>) -> Self {
|
|
||||||
let mut emojis: Vec<SharedString> = vec![];
|
let mut emojis: Vec<SharedString> = vec![];
|
||||||
|
|
||||||
emojis.extend(
|
emojis.extend(
|
||||||
@@ -38,9 +25,23 @@ impl EmojiPicker {
|
|||||||
.collect::<Vec<SharedString>>(),
|
.collect::<Vec<SharedString>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
emojis
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct EmojiPicker {
|
||||||
|
icon: Option<Icon>,
|
||||||
|
size: Size,
|
||||||
|
anchor: Option<Corner>,
|
||||||
|
target_input: WeakEntity<InputState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmojiPicker {
|
||||||
|
pub fn new(target_input: WeakEntity<InputState>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
target_input,
|
target_input,
|
||||||
emojis: emojis.into(),
|
size: Size::default(),
|
||||||
anchor: None,
|
anchor: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
}
|
}
|
||||||
@@ -57,6 +58,13 @@ impl EmojiPicker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Sizable for EmojiPicker {
|
||||||
|
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||||
|
self.size = size.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RenderOnce for EmojiPicker {
|
impl RenderOnce for EmojiPicker {
|
||||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
Popover::new("emoji-picker")
|
Popover::new("emoji-picker")
|
||||||
@@ -70,10 +78,10 @@ impl RenderOnce for EmojiPicker {
|
|||||||
.trigger(
|
.trigger(
|
||||||
Button::new("emoji-trigger")
|
Button::new("emoji-trigger")
|
||||||
.when_some(self.icon, |this, icon| this.icon(icon))
|
.when_some(self.icon, |this, icon| this.icon(icon))
|
||||||
.ghost(),
|
.ghost()
|
||||||
|
.with_size(self.size),
|
||||||
)
|
)
|
||||||
.content(move |window, cx| {
|
.content(move |window, cx| {
|
||||||
let emojis = self.emojis.clone();
|
|
||||||
let input = self.target_input.clone();
|
let input = self.target_input.clone();
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
@@ -83,7 +91,7 @@ impl RenderOnce for EmojiPicker {
|
|||||||
.flex_wrap()
|
.flex_wrap()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.children(emojis.iter().map(|e| {
|
.children(get_emojis().iter().map(|e| {
|
||||||
div()
|
div()
|
||||||
.id(e.clone())
|
.id(e.clone())
|
||||||
.flex_auto()
|
.flex_auto()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ pub use focusable::FocusableCycle;
|
|||||||
pub use icon::*;
|
pub use icon::*;
|
||||||
pub use root::{ContextModal, Root};
|
pub use root::{ContextModal, Root};
|
||||||
pub use styled::*;
|
pub use styled::*;
|
||||||
pub use title_bar::*;
|
|
||||||
pub use window_border::{window_border, WindowBorder};
|
pub use window_border::{window_border, WindowBorder};
|
||||||
|
|
||||||
pub use crate::Disableable;
|
pub use crate::Disableable;
|
||||||
@@ -39,7 +38,6 @@ mod focusable;
|
|||||||
mod icon;
|
mod icon;
|
||||||
mod root;
|
mod root;
|
||||||
mod styled;
|
mod styled;
|
||||||
mod title_bar;
|
|
||||||
mod window_border;
|
mod window_border;
|
||||||
|
|
||||||
i18n::init!();
|
i18n::init!();
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ impl Default for ModalButtonProps {
|
|||||||
ok_text: None,
|
ok_text: None,
|
||||||
ok_variant: ButtonVariant::Primary,
|
ok_variant: ButtonVariant::Primary,
|
||||||
cancel_text: None,
|
cancel_text: None,
|
||||||
cancel_variant: ButtonVariant::Ghost,
|
cancel_variant: ButtonVariant::Ghost { alt: false },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,12 +450,18 @@ impl RenderOnce for Modal {
|
|||||||
.top(y)
|
.top(y)
|
||||||
.w(self.width)
|
.w(self.width)
|
||||||
.when_some(self.max_width, |this, w| this.max_w(w))
|
.when_some(self.max_width, |this, w| this.max_w(w))
|
||||||
.child(h_flex().h_4().px_3().justify_center().when_some(
|
.child(
|
||||||
self.title,
|
div()
|
||||||
|this, title| {
|
.px_2()
|
||||||
this.h_12().font_semibold().text_center().child(title)
|
.h_4()
|
||||||
},
|
.w_full()
|
||||||
))
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.when_some(self.title, |this, title| {
|
||||||
|
this.h_10().font_semibold().text_center().child(title)
|
||||||
|
}),
|
||||||
|
)
|
||||||
.when(self.show_close, |this| {
|
.when(self.show_close, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Button::new("close")
|
Button::new("close")
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, AnyView, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
|
div, AnyView, App, AppContext, Context, Decorations, Entity, FocusHandle, InteractiveElement,
|
||||||
ParentElement as _, Render, Styled, Window,
|
IntoElement, ParentElement as _, Render, Styled, Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
|
||||||
|
|
||||||
use crate::input::InputState;
|
use crate::input::InputState;
|
||||||
use crate::modal::Modal;
|
use crate::modal::Modal;
|
||||||
@@ -257,11 +258,29 @@ impl Render for Root {
|
|||||||
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 base_font_size = cx.theme().font_size;
|
let base_font_size = cx.theme().font_size;
|
||||||
let font_family = cx.theme().font_family.clone();
|
let font_family = cx.theme().font_family.clone();
|
||||||
|
let decorations = window.window_decorations();
|
||||||
|
|
||||||
window.set_rem_size(base_font_size);
|
window.set_rem_size(base_font_size);
|
||||||
|
|
||||||
window_border().child(
|
window_border().child(
|
||||||
div()
|
div()
|
||||||
.id("root")
|
.id("root")
|
||||||
|
.map(|this| match decorations {
|
||||||
|
Decorations::Server => this,
|
||||||
|
Decorations::Client { tiling, .. } => this
|
||||||
|
.when(!(tiling.top || tiling.right), |el| {
|
||||||
|
el.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
})
|
||||||
|
.when(!(tiling.top || tiling.left), |el| {
|
||||||
|
el.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
})
|
||||||
|
.when(!(tiling.bottom || tiling.right), |el| {
|
||||||
|
el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
})
|
||||||
|
.when(!(tiling.bottom || tiling.left), |el| {
|
||||||
|
el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
}),
|
||||||
|
})
|
||||||
.relative()
|
.relative()
|
||||||
.size_full()
|
.size_full()
|
||||||
.font_family(font_family)
|
.font_family(font_family)
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ use gpui::{
|
|||||||
IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
||||||
Position, ScrollHandle, ScrollWheelEvent, Size, UniformListScrollHandle, Window,
|
Position, ScrollHandle, ScrollWheelEvent, Size, UniformListScrollHandle, Window,
|
||||||
};
|
};
|
||||||
use theme::{ActiveTheme, ScrollBarMode};
|
use theme::scrollbar_mode::ScrollBarMode;
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::AxisExt;
|
use crate::AxisExt;
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ pub fn v_flex() -> Div {
|
|||||||
div().v_flex()
|
div().v_flex()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a `Div` as divider.
|
||||||
|
pub fn divider(cx: &App) -> Div {
|
||||||
|
div().my_2().w_full().h_px().bg(cx.theme().border)
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! font_weight {
|
macro_rules! font_weight {
|
||||||
($fn:ident, $const:ident) => {
|
($fn:ident, $const:ident) => {
|
||||||
/// [docs](https://tailwindcss.com/docs/font-weight)
|
/// [docs](https://tailwindcss.com/docs/font-weight)
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ impl Element for Switch {
|
|||||||
.when_some(self.description.clone(), |this, description| {
|
.when_some(self.description.clone(), |this, description| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.w_3_4()
|
.pr_2()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(description),
|
.child(description),
|
||||||
|
|||||||
@@ -1,391 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
|
||||||
use gpui::{
|
|
||||||
black, div, px, relative, white, AnyElement, App, ClickEvent, Div, Element, Hsla,
|
|
||||||
InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, Rgba,
|
|
||||||
Stateful, StatefulInteractiveElement as _, Style, Styled, Window, WindowControlArea,
|
|
||||||
};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
|
|
||||||
use crate::{h_flex, Icon, IconName, InteractiveElementExt as _, Sizable as _};
|
|
||||||
|
|
||||||
const TITLE_BAR_HEIGHT: Pixels = px(34.);
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
const TITLE_BAR_LEFT_PADDING: Pixels = px(80.);
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
const TITLE_BAR_LEFT_PADDING: Pixels = px(12.);
|
|
||||||
|
|
||||||
type OnCloseWindow = Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>;
|
|
||||||
|
|
||||||
/// TitleBar used to customize the appearance of the title bar.
|
|
||||||
///
|
|
||||||
/// We can put some elements inside the title bar.
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct TitleBar {
|
|
||||||
base: Stateful<Div>,
|
|
||||||
children: Vec<AnyElement>,
|
|
||||||
on_close_window: OnCloseWindow,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TitleBar {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
base: div().id("title-bar"),
|
|
||||||
children: Vec::new(),
|
|
||||||
on_close_window: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add custom for close window event, default is None, then click X button will call `window.remove_window()`.
|
|
||||||
/// Linux only, this will do nothing on other platforms.
|
|
||||||
pub fn on_close_window(
|
|
||||||
mut self,
|
|
||||||
f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
if cfg!(target_os = "linux") {
|
|
||||||
self.on_close_window = Some(Rc::new(Box::new(f)));
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TitleBar {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Windows control buttons have a fixed width of 35px.
|
|
||||||
//
|
|
||||||
// We don't need implementation the click event for the control buttons.
|
|
||||||
// If user clicked in the bounds, the window event will be triggered.
|
|
||||||
#[derive(IntoElement, Clone)]
|
|
||||||
enum ControlIcon {
|
|
||||||
Minimize,
|
|
||||||
Restore,
|
|
||||||
Maximize,
|
|
||||||
Close { on_close_window: OnCloseWindow },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ControlIcon {
|
|
||||||
fn minimize() -> Self {
|
|
||||||
Self::Minimize
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore() -> Self {
|
|
||||||
Self::Restore
|
|
||||||
}
|
|
||||||
|
|
||||||
fn maximize() -> Self {
|
|
||||||
Self::Maximize
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close(on_close_window: OnCloseWindow) -> Self {
|
|
||||||
Self::Close { on_close_window }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Minimize => "minimize",
|
|
||||||
Self::Restore => "restore",
|
|
||||||
Self::Maximize => "maximize",
|
|
||||||
Self::Close { .. } => "close",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn icon(&self) -> IconName {
|
|
||||||
match self {
|
|
||||||
Self::Minimize => IconName::WindowMinimize,
|
|
||||||
Self::Restore => IconName::WindowRestore,
|
|
||||||
Self::Maximize => IconName::WindowMaximize,
|
|
||||||
Self::Close { .. } => IconName::WindowClose,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn window_control_area(&self) -> WindowControlArea {
|
|
||||||
match self {
|
|
||||||
Self::Minimize => WindowControlArea::Min,
|
|
||||||
Self::Restore | Self::Maximize => WindowControlArea::Max,
|
|
||||||
Self::Close { .. } => WindowControlArea::Close,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_close(&self) -> bool {
|
|
||||||
matches!(self, Self::Close { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fg(&self, cx: &App) -> Hsla {
|
|
||||||
if cx.theme().mode.is_dark() {
|
|
||||||
white()
|
|
||||||
} else {
|
|
||||||
black()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hover_fg(&self, cx: &App) -> Hsla {
|
|
||||||
if self.is_close() || cx.theme().mode.is_dark() {
|
|
||||||
white()
|
|
||||||
} else {
|
|
||||||
black()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hover_bg(&self, cx: &App) -> Rgba {
|
|
||||||
if self.is_close() {
|
|
||||||
Rgba {
|
|
||||||
r: 232.0 / 255.0,
|
|
||||||
g: 17.0 / 255.0,
|
|
||||||
b: 32.0 / 255.0,
|
|
||||||
a: 1.0,
|
|
||||||
}
|
|
||||||
} else if cx.theme().mode.is_dark() {
|
|
||||||
Rgba {
|
|
||||||
r: 0.9,
|
|
||||||
g: 0.9,
|
|
||||||
b: 0.9,
|
|
||||||
a: 0.1,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Rgba {
|
|
||||||
r: 0.1,
|
|
||||||
g: 0.1,
|
|
||||||
b: 0.1,
|
|
||||||
a: 0.2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for ControlIcon {
|
|
||||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
let is_linux = cfg!(target_os = "linux");
|
|
||||||
let is_windows = cfg!(target_os = "windows");
|
|
||||||
let fg = self.fg(cx);
|
|
||||||
let hover_fg = self.hover_fg(cx);
|
|
||||||
let hover_bg = self.hover_bg(cx);
|
|
||||||
let icon = self.clone();
|
|
||||||
let on_close_window = match &self {
|
|
||||||
ControlIcon::Close { on_close_window } => on_close_window.clone(),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id(self.id())
|
|
||||||
.flex()
|
|
||||||
.w(TITLE_BAR_HEIGHT)
|
|
||||||
.h_full()
|
|
||||||
.justify_center()
|
|
||||||
.content_center()
|
|
||||||
.items_center()
|
|
||||||
.text_color(fg)
|
|
||||||
.when(is_windows, |this| {
|
|
||||||
this.window_control_area(self.window_control_area())
|
|
||||||
})
|
|
||||||
.when(is_linux, |this| {
|
|
||||||
this.on_mouse_down(MouseButton::Left, move |_, window, cx| {
|
|
||||||
window.prevent_default();
|
|
||||||
cx.stop_propagation();
|
|
||||||
})
|
|
||||||
.on_click(move |_, window, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
match icon {
|
|
||||||
Self::Minimize => window.minimize_window(),
|
|
||||||
Self::Restore | Self::Maximize => window.zoom_window(),
|
|
||||||
Self::Close { .. } => {
|
|
||||||
if let Some(f) = on_close_window.clone() {
|
|
||||||
f(&ClickEvent::default(), window, cx);
|
|
||||||
} else {
|
|
||||||
window.remove_window();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.hover(|style| style.bg(hover_bg).text_color(hover_fg))
|
|
||||||
.active(|style| style.bg(hover_bg))
|
|
||||||
.child(Icon::new(self.icon()).small())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
struct WindowControls {
|
|
||||||
on_close_window: OnCloseWindow,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for WindowControls {
|
|
||||||
fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
|
|
||||||
if cfg!(target_os = "macos") {
|
|
||||||
return div().id("window-controls");
|
|
||||||
}
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.id("window-controls")
|
|
||||||
.items_center()
|
|
||||||
.flex_shrink_0()
|
|
||||||
.h_full()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.justify_center()
|
|
||||||
.content_stretch()
|
|
||||||
.h_full()
|
|
||||||
.child(ControlIcon::minimize())
|
|
||||||
.child(if window.is_maximized() {
|
|
||||||
ControlIcon::restore()
|
|
||||||
} else {
|
|
||||||
ControlIcon::maximize()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(ControlIcon::close(self.on_close_window))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Styled for TitleBar {
|
|
||||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
|
||||||
self.base.style()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for TitleBar {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
||||||
self.children.extend(elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for TitleBar {
|
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
const HEIGHT: Pixels = px(34.);
|
|
||||||
|
|
||||||
let is_linux = cfg!(target_os = "linux");
|
|
||||||
let is_macos = cfg!(target_os = "macos");
|
|
||||||
|
|
||||||
div().flex_shrink_0().child(
|
|
||||||
self.base
|
|
||||||
.flex()
|
|
||||||
.flex_row()
|
|
||||||
.items_center()
|
|
||||||
.justify_between()
|
|
||||||
.h(HEIGHT)
|
|
||||||
.bg(cx.theme().title_bar)
|
|
||||||
.when(window.is_fullscreen(), |this| this.pl(px(12.)))
|
|
||||||
.when(is_linux, |this| {
|
|
||||||
this.on_double_click(|_, window, _| window.zoom_window())
|
|
||||||
})
|
|
||||||
.when(is_macos, |this| {
|
|
||||||
this.on_double_click(|_, window, _| window.titlebar_double_click())
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.id("bar")
|
|
||||||
.pl(TITLE_BAR_LEFT_PADDING)
|
|
||||||
.when(window.is_fullscreen(), |this| this.pl(px(12.)))
|
|
||||||
.window_control_area(WindowControlArea::Drag)
|
|
||||||
.justify_between()
|
|
||||||
.flex_shrink_0()
|
|
||||||
.flex_1()
|
|
||||||
.h_full()
|
|
||||||
.when(is_linux, |this| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.top_0()
|
|
||||||
.left_0()
|
|
||||||
.absolute()
|
|
||||||
.size_full()
|
|
||||||
.h_full()
|
|
||||||
.child(TitleBarElement {}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.children(self.children),
|
|
||||||
)
|
|
||||||
.child(WindowControls {
|
|
||||||
on_close_window: self.on_close_window,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A TitleBar Element that can be move the window.
|
|
||||||
pub struct TitleBarElement {}
|
|
||||||
|
|
||||||
impl IntoElement for TitleBarElement {
|
|
||||||
type Element = Self;
|
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element for TitleBarElement {
|
|
||||||
type PrepaintState = ();
|
|
||||||
type RequestLayoutState = ();
|
|
||||||
|
|
||||||
fn id(&self) -> Option<gpui::ElementId> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_layout(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
|
||||||
let style = Style {
|
|
||||||
flex_grow: 1.0,
|
|
||||||
flex_shrink: 1.0,
|
|
||||||
size: gpui::Size {
|
|
||||||
width: relative(1.).into(),
|
|
||||||
height: relative(1.).into(),
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let id = window.request_layout(style, [], cx);
|
|
||||||
|
|
||||||
(id, ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepaint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
_: gpui::Bounds<Pixels>,
|
|
||||||
_: &mut Self::RequestLayoutState,
|
|
||||||
_window: &mut Window,
|
|
||||||
_cx: &mut App,
|
|
||||||
) -> Self::PrepaintState {
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
bounds: gpui::Bounds<Pixels>,
|
|
||||||
_: &mut Self::RequestLayoutState,
|
|
||||||
_: &mut Self::PrepaintState,
|
|
||||||
window: &mut Window,
|
|
||||||
_cx: &mut App,
|
|
||||||
) {
|
|
||||||
use gpui::{MouseButton, MouseMoveEvent, MouseUpEvent};
|
|
||||||
|
|
||||||
window.on_mouse_event(
|
|
||||||
move |ev: &MouseMoveEvent, _, window: &mut Window, _cx: &mut App| {
|
|
||||||
if bounds.contains(&ev.position) && ev.pressed_button == Some(MouseButton::Left) {
|
|
||||||
window.start_window_move();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
window.on_mouse_event(
|
|
||||||
move |ev: &MouseUpEvent, _, window: &mut Window, _cx: &mut App| {
|
|
||||||
if ev.button == MouseButton::Left {
|
|
||||||
window.show_window_menu(ev.position);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,14 +4,9 @@ use gpui::{
|
|||||||
HitboxBehavior, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels,
|
HitboxBehavior, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels,
|
||||||
Point, RenderOnce, ResizeEdge, Size, Styled as _, Window,
|
Point, RenderOnce, ResizeEdge, Size, Styled as _, Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::{CLIENT_SIDE_DECORATION_ROUNDING, CLIENT_SIDE_DECORATION_SHADOW};
|
||||||
|
|
||||||
pub(crate) const BORDER_SIZE: Pixels = Pixels(1.0);
|
const WINDOW_BORDER_WIDTH: Pixels = Pixels(1.0);
|
||||||
pub(crate) const BORDER_RADIUS: Pixels = Pixels(0.0);
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
pub(crate) const SHADOW_SIZE: Pixels = Pixels(0.0);
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub(crate) const SHADOW_SIZE: Pixels = Pixels(12.0);
|
|
||||||
|
|
||||||
/// Create a new window border.
|
/// Create a new window border.
|
||||||
pub fn window_border() -> WindowBorder {
|
pub fn window_border() -> WindowBorder {
|
||||||
@@ -29,7 +24,7 @@ pub fn window_paddings(window: &Window, _cx: &App) -> Edges<Pixels> {
|
|||||||
match window.window_decorations() {
|
match window.window_decorations() {
|
||||||
Decorations::Server => Edges::all(px(0.0)),
|
Decorations::Server => Edges::all(px(0.0)),
|
||||||
Decorations::Client { tiling } => {
|
Decorations::Client { tiling } => {
|
||||||
let mut paddings = Edges::all(SHADOW_SIZE);
|
let mut paddings = Edges::all(CLIENT_SIDE_DECORATION_SHADOW);
|
||||||
if tiling.top {
|
if tiling.top {
|
||||||
paddings.top = px(0.0);
|
paddings.top = px(0.0);
|
||||||
}
|
}
|
||||||
@@ -62,9 +57,9 @@ impl ParentElement for WindowBorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for WindowBorder {
|
impl RenderOnce for WindowBorder {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
let decorations = window.window_decorations();
|
let decorations = window.window_decorations();
|
||||||
window.set_client_inset(SHADOW_SIZE);
|
window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW);
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id("window-backdrop")
|
.id("window-backdrop")
|
||||||
@@ -87,7 +82,9 @@ impl RenderOnce for WindowBorder {
|
|||||||
move |_bounds, hitbox, window, _cx| {
|
move |_bounds, hitbox, window, _cx| {
|
||||||
let mouse = window.mouse_position();
|
let mouse = window.mouse_position();
|
||||||
let size = window.window_bounds().get_bounds().size;
|
let size = window.window_bounds().get_bounds().size;
|
||||||
let Some(edge) = resize_edge(mouse, SHADOW_SIZE, size) else {
|
let Some(edge) =
|
||||||
|
resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size)
|
||||||
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
window.set_cursor_style(
|
window.set_cursor_style(
|
||||||
@@ -113,20 +110,26 @@ impl RenderOnce for WindowBorder {
|
|||||||
.absolute(),
|
.absolute(),
|
||||||
)
|
)
|
||||||
.when(!(tiling.top || tiling.right), |div| {
|
.when(!(tiling.top || tiling.right), |div| {
|
||||||
div.rounded_tr(BORDER_RADIUS)
|
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
})
|
})
|
||||||
.when(!(tiling.top || tiling.left), |div| {
|
.when(!(tiling.top || tiling.left), |div| {
|
||||||
div.rounded_tl(BORDER_RADIUS)
|
div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
})
|
})
|
||||||
.when(!tiling.top, |div| div.pt(SHADOW_SIZE))
|
.when(!(tiling.bottom || tiling.right), |div| {
|
||||||
.when(!tiling.bottom, |div| div.pb(SHADOW_SIZE))
|
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
.when(!tiling.left, |div| div.pl(SHADOW_SIZE))
|
})
|
||||||
.when(!tiling.right, |div| div.pr(SHADOW_SIZE))
|
.when(!(tiling.bottom || tiling.left), |div| {
|
||||||
|
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
})
|
||||||
|
.when(!tiling.top, |div| div.pt(CLIENT_SIDE_DECORATION_SHADOW))
|
||||||
|
.when(!tiling.bottom, |div| div.pb(CLIENT_SIDE_DECORATION_SHADOW))
|
||||||
|
.when(!tiling.left, |div| div.pl(CLIENT_SIDE_DECORATION_SHADOW))
|
||||||
|
.when(!tiling.right, |div| div.pr(CLIENT_SIDE_DECORATION_SHADOW))
|
||||||
.on_mouse_down(MouseButton::Left, move |_, window, _cx| {
|
.on_mouse_down(MouseButton::Left, move |_, window, _cx| {
|
||||||
let size = window.window_bounds().get_bounds().size;
|
let size = window.window_bounds().get_bounds().size;
|
||||||
let pos = window.mouse_position();
|
let pos = window.mouse_position();
|
||||||
|
|
||||||
if let Some(edge) = resize_edge(pos, SHADOW_SIZE, size) {
|
if let Some(edge) = resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size) {
|
||||||
window.start_window_resize(edge)
|
window.start_window_resize(edge)
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -137,17 +140,22 @@ impl RenderOnce for WindowBorder {
|
|||||||
.map(|div| match decorations {
|
.map(|div| match decorations {
|
||||||
Decorations::Server => div,
|
Decorations::Server => div,
|
||||||
Decorations::Client { tiling } => div
|
Decorations::Client { tiling } => div
|
||||||
.border_color(cx.theme().window_border)
|
|
||||||
.when(!(tiling.top || tiling.right), |div| {
|
.when(!(tiling.top || tiling.right), |div| {
|
||||||
div.rounded_tr(BORDER_RADIUS)
|
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
})
|
})
|
||||||
.when(!(tiling.top || tiling.left), |div| {
|
.when(!(tiling.top || tiling.left), |div| {
|
||||||
div.rounded_tl(BORDER_RADIUS)
|
div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
})
|
})
|
||||||
.when(!tiling.top, |div| div.border_t(BORDER_SIZE))
|
.when(!(tiling.bottom || tiling.right), |div| {
|
||||||
.when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
|
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
.when(!tiling.left, |div| div.border_l(BORDER_SIZE))
|
})
|
||||||
.when(!tiling.right, |div| div.border_r(BORDER_SIZE))
|
.when(!(tiling.bottom || tiling.left), |div| {
|
||||||
|
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
})
|
||||||
|
.when(!tiling.top, |div| div.border_t(WINDOW_BORDER_WIDTH))
|
||||||
|
.when(!tiling.bottom, |div| div.border_b(WINDOW_BORDER_WIDTH))
|
||||||
|
.when(!tiling.left, |div| div.border_l(WINDOW_BORDER_WIDTH))
|
||||||
|
.when(!tiling.right, |div| div.border_r(WINDOW_BORDER_WIDTH))
|
||||||
.when(!tiling.is_tiled(), |div| {
|
.when(!tiling.is_tiled(), |div| {
|
||||||
div.shadow(vec![gpui::BoxShadow {
|
div.shadow(vec![gpui::BoxShadow {
|
||||||
color: Hsla {
|
color: Hsla {
|
||||||
@@ -156,7 +164,7 @@ impl RenderOnce for WindowBorder {
|
|||||||
l: 0.,
|
l: 0.,
|
||||||
a: 0.3,
|
a: 0.3,
|
||||||
},
|
},
|
||||||
blur_radius: SHADOW_SIZE / 2.,
|
blur_radius: CLIENT_SIDE_DECORATION_SHADOW / 2.,
|
||||||
spread_radius: px(0.),
|
spread_radius: px(0.),
|
||||||
offset: point(px(0.0), px(0.0)),
|
offset: point(px(0.0), px(0.0)),
|
||||||
}])
|
}])
|
||||||
|
|||||||
@@ -5,25 +5,41 @@ common:
|
|||||||
en: "Add"
|
en: "Add"
|
||||||
update:
|
update:
|
||||||
en: "Update"
|
en: "Update"
|
||||||
|
upload:
|
||||||
|
en: "Upload"
|
||||||
change:
|
change:
|
||||||
en: "Change"
|
en: "Change"
|
||||||
continue:
|
continue:
|
||||||
en: "Continue"
|
en: "Continue"
|
||||||
|
pubkey:
|
||||||
|
en: "Public Key"
|
||||||
pubkey_invalid:
|
pubkey_invalid:
|
||||||
en: "Public Key is not valid"
|
en: "Public Key is not valid"
|
||||||
|
secret:
|
||||||
|
en: "Secret Key"
|
||||||
not_found:
|
not_found:
|
||||||
en: "Not Found"
|
en: "Not Found"
|
||||||
room_error:
|
room_error:
|
||||||
en: "Failed to open room. Please try again later."
|
en: "Failed to open room. Please try again later."
|
||||||
|
preferences:
|
||||||
|
en: "Preferences"
|
||||||
allow:
|
allow:
|
||||||
en: "Allow"
|
en: "Allow"
|
||||||
logout:
|
|
||||||
en: "Logout"
|
|
||||||
copied:
|
copied:
|
||||||
en: "Copied"
|
en: "Copied"
|
||||||
|
saved:
|
||||||
|
en: "Your Secret Key has been saved"
|
||||||
clear:
|
clear:
|
||||||
en: "Clear"
|
en: "Clear"
|
||||||
|
|
||||||
|
user:
|
||||||
|
dark_mode:
|
||||||
|
en: "Dark Mode"
|
||||||
|
settings:
|
||||||
|
en: "Settings"
|
||||||
|
sign_out:
|
||||||
|
en: "Sign out"
|
||||||
|
|
||||||
welcome:
|
welcome:
|
||||||
title:
|
title:
|
||||||
en: "Welcome to Coop"
|
en: "Welcome to Coop"
|
||||||
@@ -41,6 +57,12 @@ onboarding:
|
|||||||
en: "Already have an account? Log in."
|
en: "Already have an account? Log in."
|
||||||
|
|
||||||
startup:
|
startup:
|
||||||
|
client_keys_warning:
|
||||||
|
en: "Warning"
|
||||||
|
client_keys_desc:
|
||||||
|
en: "Allow Coop to read the client keys stored in Keychain to continue"
|
||||||
|
create_new_keys:
|
||||||
|
en: "Create New Keys"
|
||||||
auto_login_in_progress:
|
auto_login_in_progress:
|
||||||
en: "Auto login in progress"
|
en: "Auto login in progress"
|
||||||
stuck:
|
stuck:
|
||||||
@@ -50,11 +72,23 @@ startup:
|
|||||||
|
|
||||||
new_account:
|
new_account:
|
||||||
title:
|
title:
|
||||||
en: "Create New Account"
|
en: "Create a new identity"
|
||||||
password_invalid:
|
name:
|
||||||
en: "Password is invalid"
|
en: "What should people call you?"
|
||||||
set_password_prompt:
|
avatar:
|
||||||
en: "Set password to encrypt your key *"
|
en: "Choose an avatar to help people recognize you"
|
||||||
|
backup_label:
|
||||||
|
en: "Backup to avoid losing access to your account"
|
||||||
|
backup_description:
|
||||||
|
en: "In the Nostr Network, your account is defined by a Secret Key. This key is used to sign your messages and identify you."
|
||||||
|
backup_pubkey_note:
|
||||||
|
en: "Your Public Key is the address that others will use to find you on the Nostr Network."
|
||||||
|
backup_secret_note:
|
||||||
|
en: "Your Secret Key is required to access your account. If you lose it, you will lose access to your account."
|
||||||
|
backup_skip:
|
||||||
|
en: "Do it later"
|
||||||
|
backup_download:
|
||||||
|
en: "Download"
|
||||||
|
|
||||||
login:
|
login:
|
||||||
title:
|
title:
|
||||||
@@ -92,27 +126,21 @@ login:
|
|||||||
logging_in:
|
logging_in:
|
||||||
en: "Logging in..."
|
en: "Logging in..."
|
||||||
|
|
||||||
chatspace:
|
|
||||||
create_new_keys:
|
|
||||||
en: "Create New Keys"
|
|
||||||
appearance_tooltip:
|
|
||||||
en: "Change the app's appearance"
|
|
||||||
preferences_title:
|
|
||||||
en: "Preferences"
|
|
||||||
preferences_tooltip:
|
|
||||||
en: "Open Preferences"
|
|
||||||
languages_tooltip:
|
|
||||||
en: "Change the app's language"
|
|
||||||
share_profile:
|
|
||||||
en: "Share Profile"
|
|
||||||
|
|
||||||
relays:
|
relays:
|
||||||
|
button_label:
|
||||||
|
en: "Configure the Messaging Relays to receive messages"
|
||||||
|
modal_title:
|
||||||
|
en: "Set Up Messaging Relays"
|
||||||
description:
|
description:
|
||||||
en: "In order to receive messages from others, you need to setup at least one Messaging Relay. You can use the recommend relays or add more."
|
en: "In order to receive messages from others, you need to set up at least one Messaging Relay."
|
||||||
add_some_relays:
|
add_some_relays:
|
||||||
en: "Please add some relays."
|
en: "Please add some relays."
|
||||||
invalid:
|
invalid:
|
||||||
en: "Relay URL is not valid."
|
en: "Relay URL is not valid."
|
||||||
|
empty:
|
||||||
|
en: "You need to add at least 1 relay to receive messages."
|
||||||
|
recommended:
|
||||||
|
en: "Recommended:"
|
||||||
|
|
||||||
subject:
|
subject:
|
||||||
title:
|
title:
|
||||||
@@ -191,16 +219,14 @@ profile:
|
|||||||
en: "No bio."
|
en: "No bio."
|
||||||
|
|
||||||
preferences:
|
preferences:
|
||||||
modal_relays_title:
|
|
||||||
en: "Edit your Messaging Relays"
|
|
||||||
media_description:
|
media_description:
|
||||||
en: "Coop currently only supports NIP-96 media servers. If you're unsure, please keep the default value."
|
en: "Coop currently only supports NIP-96 media servers."
|
||||||
backup_description:
|
backup_description:
|
||||||
en: "When you send a message, Coop will also send it to your configured Messaging Relays. You can disable this if you want all sent messages to disappear when you log out."
|
en: "When you send a message, Coop will also forward it to your configured Messaging Relays. Disabling this will cause all messages sent during the current session to disappear when the app is closed."
|
||||||
screening_description:
|
screening_description:
|
||||||
en: "When opening a chat request, Coop will show a popup to help users verify the sender."
|
en: "When opening a chat request, Coop will show a popup to help you verify the sender."
|
||||||
bypass_description:
|
bypass_description:
|
||||||
en: "Requests from user's contacts will automatically go to inbox."
|
en: "Requests from your contacts will automatically go to inbox."
|
||||||
hide_avatar_description:
|
hide_avatar_description:
|
||||||
en: "Unload all avatar pictures to improve performance and reduce memory usage."
|
en: "Unload all avatar pictures to improve performance and reduce memory usage."
|
||||||
proxy_description:
|
proxy_description:
|
||||||
@@ -304,7 +330,7 @@ sidebar:
|
|||||||
trusted_contacts_tooltip:
|
trusted_contacts_tooltip:
|
||||||
en: "Only show rooms from trusted contacts"
|
en: "Only show rooms from trusted contacts"
|
||||||
retrieving_messages:
|
retrieving_messages:
|
||||||
en: "Retrieving messages..."
|
en: "Retrieving messages"
|
||||||
retrieving_messages_description:
|
retrieving_messages_description:
|
||||||
en: "This may take some time"
|
en: "This may take some time"
|
||||||
why_seeing_this_tooltip:
|
why_seeing_this_tooltip:
|
||||||
|
|||||||
Reference in New Issue
Block a user