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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0453232ace82dee0dd0b4c87a59bd90f7b53b314f3e0f61fe2ee7c8a16482289"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
@@ -864,9 +870,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.30"
|
||||
version = "1.2.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
|
||||
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
@@ -1083,7 +1089,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collections"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"rustc-hash 2.1.1",
|
||||
@@ -1203,6 +1209,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"smol",
|
||||
"theme",
|
||||
"title_bar",
|
||||
"tracing-subscriber",
|
||||
"ui",
|
||||
]
|
||||
@@ -1436,9 +1443,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4735f265ba6a1188052ca32d461028a7d1125868be18e287e756019da7607b5"
|
||||
checksum = "ec09e802f5081de6157da9a75701d6c713d8dc3ba52571fd4bd25f412644e8a6"
|
||||
dependencies = [
|
||||
"ctor-proc-macro",
|
||||
"dtor",
|
||||
@@ -1446,9 +1453,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctor-proc-macro"
|
||||
version = "0.0.5"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d"
|
||||
checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2"
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
@@ -1484,7 +1491,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "derive_refineable"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1580,6 +1587,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.1"
|
||||
@@ -1630,9 +1646,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.19"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
@@ -1649,7 +1665,7 @@ dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml 0.9.2",
|
||||
"toml 0.9.4",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
]
|
||||
@@ -1824,6 +1840,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.3"
|
||||
@@ -1947,7 +1972,7 @@ checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3"
|
||||
dependencies = [
|
||||
"fontconfig-parser",
|
||||
"log",
|
||||
"memmap2",
|
||||
"memmap2 0.9.7",
|
||||
"slotmap",
|
||||
"tinyvec",
|
||||
"ttf-parser 0.20.0",
|
||||
@@ -1961,7 +1986,7 @@ checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"
|
||||
dependencies = [
|
||||
"fontconfig-parser",
|
||||
"log",
|
||||
"memmap2",
|
||||
"memmap2 0.9.7",
|
||||
"slotmap",
|
||||
"tinyvec",
|
||||
"ttf-parser 0.25.1",
|
||||
@@ -2018,6 +2043,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "freetype-sys"
|
||||
version = "0.20.1"
|
||||
@@ -2336,7 +2371,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"as-raw-xcb-connection",
|
||||
@@ -2429,7 +2464,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_macros"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -2441,7 +2476,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_tokio"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||
dependencies = [
|
||||
"gpui",
|
||||
"tokio",
|
||||
@@ -2485,6 +2520,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
@@ -2663,7 +2707,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "http_client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -2672,6 +2716,7 @@ dependencies = [
|
||||
"http",
|
||||
"http-body",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
@@ -2681,7 +2726,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "http_client_tls"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls-platform-verifier",
|
||||
@@ -3271,7 +3316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.53.2",
|
||||
"windows-targets 0.53.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3282,14 +3327,37 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.6"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0"
|
||||
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"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]]
|
||||
name = "linkify"
|
||||
version = "0.10.0"
|
||||
@@ -3359,9 +3427,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198"
|
||||
checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
@@ -3459,7 +3527,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "media"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bindgen 0.71.1",
|
||||
@@ -3478,6 +3546,15 @@ version = "2.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
version = "0.9.7"
|
||||
@@ -3681,8 +3758,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr"
|
||||
version = "0.42.1"
|
||||
source = "git+https://github.com/rust-nostr/nostr#dd8328ded8958c8c1133b293142da94c3e1d6f70"
|
||||
version = "0.43.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64",
|
||||
@@ -3704,8 +3781,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-connect"
|
||||
version = "0.42.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#dd8328ded8958c8c1133b293142da94c3e1d6f70"
|
||||
version = "0.43.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"nostr",
|
||||
@@ -3716,8 +3793,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-database"
|
||||
version = "0.42.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#dd8328ded8958c8c1133b293142da94c3e1d6f70"
|
||||
version = "0.43.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84"
|
||||
dependencies = [
|
||||
"flatbuffers",
|
||||
"lru",
|
||||
@@ -3727,10 +3804,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-lmdb"
|
||||
version = "0.42.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#dd8328ded8958c8c1133b293142da94c3e1d6f70"
|
||||
version = "0.43.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"flume",
|
||||
"heed",
|
||||
"nostr",
|
||||
"nostr-database",
|
||||
@@ -3740,8 +3818,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-relay-pool"
|
||||
version = "0.42.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#dd8328ded8958c8c1133b293142da94c3e1d6f70"
|
||||
version = "0.43.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"async-wsocket",
|
||||
@@ -3756,8 +3834,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nostr-sdk"
|
||||
version = "0.42.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#dd8328ded8958c8c1133b293142da94c3e1d6f70"
|
||||
version = "0.43.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"nostr",
|
||||
@@ -4149,6 +4227,16 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
@@ -4666,9 +4754,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rangemap"
|
||||
version = "1.5.1"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684"
|
||||
checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223"
|
||||
|
||||
[[package]]
|
||||
name = "rav1e"
|
||||
@@ -4770,9 +4858,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.15"
|
||||
version = "0.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec"
|
||||
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
@@ -4811,7 +4899,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "refineable"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||
dependencies = [
|
||||
"derive_refineable",
|
||||
"workspace-hack",
|
||||
@@ -4963,7 +5051,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "reqwest_client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -5112,10 +5200,20 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.25"
|
||||
name = "rust-ini"
|
||||
version = "0.17.0"
|
||||
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]]
|
||||
name = "rustc-hash"
|
||||
@@ -5166,9 +5264,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.29"
|
||||
version = "0.23.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1"
|
||||
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
@@ -5489,7 +5587,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
|
||||
[[package]]
|
||||
name = "semantic_version"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -5544,9 +5642,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.141"
|
||||
version = "1.0.142"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
|
||||
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
@@ -5884,7 +5982,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
[[package]]
|
||||
name = "sum_tree"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"log",
|
||||
@@ -6303,11 +6401,30 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "tokio"
|
||||
version = "1.46.1"
|
||||
version = "1.47.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
|
||||
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -6316,9 +6433,9 @@ dependencies = [
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.0",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6407,9 +6524,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.2"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac"
|
||||
checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -6857,7 +6974,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
[[package]]
|
||||
name = "util"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#af0c909924d5b5b46432847fce2afb4bc8d78ee2"
|
||||
source = "git+https://github.com/zed-industries/zed#4d79edc7533d6d0a8e29004c789bc6041a97993d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-fs",
|
||||
@@ -7125,13 +7242,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.10"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121"
|
||||
checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
"rustix 0.38.44",
|
||||
"rustix 1.0.8",
|
||||
"scoped-tls",
|
||||
"smallvec",
|
||||
"wayland-sys",
|
||||
@@ -7139,23 +7256,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-client"
|
||||
version = "0.31.10"
|
||||
version = "0.31.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61"
|
||||
checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"rustix 0.38.44",
|
||||
"rustix 1.0.8",
|
||||
"wayland-backend",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-cursor"
|
||||
version = "0.31.10"
|
||||
version = "0.31.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a65317158dec28d00416cb16705934070aef4f8393353d41126c54264ae0f182"
|
||||
checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29"
|
||||
dependencies = [
|
||||
"rustix 0.38.44",
|
||||
"rustix 1.0.8",
|
||||
"wayland-client",
|
||||
"xcursor",
|
||||
]
|
||||
@@ -7187,9 +7304,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-scanner"
|
||||
version = "0.31.6"
|
||||
version = "0.31.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484"
|
||||
checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml 0.37.5",
|
||||
@@ -7198,9 +7315,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-sys"
|
||||
version = "0.31.6"
|
||||
version = "0.31.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615"
|
||||
checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142"
|
||||
dependencies = [
|
||||
"dlib",
|
||||
"log",
|
||||
@@ -7489,7 +7606,7 @@ checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
||||
dependencies = [
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.3.1",
|
||||
"windows-targets 0.53.2",
|
||||
"windows-targets 0.53.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7581,7 +7698,7 @@ version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.2",
|
||||
"windows-targets 0.53.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7632,10 +7749,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.2"
|
||||
version = "0.53.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
|
||||
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm 0.53.0",
|
||||
"windows_aarch64_msvc 0.53.0",
|
||||
"windows_i686_gnu 0.53.0",
|
||||
@@ -7946,7 +8064,7 @@ name = "xim"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"ahash 0.8.12",
|
||||
"hashbrown 0.14.5",
|
||||
"log",
|
||||
"x11rb",
|
||||
@@ -7978,7 +8096,7 @@ checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9"
|
||||
dependencies = [
|
||||
"as-raw-xcb-connection",
|
||||
"libc",
|
||||
"memmap2",
|
||||
"memmap2 0.9.7",
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
@@ -8212,9 +8330,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.4.19"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a"
|
||||
checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089"
|
||||
dependencies = [
|
||||
"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">
|
||||
<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>
|
||||
|
||||
|
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">
|
||||
<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>
|
||||
|
||||
|
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 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?;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
assets = { path = "../assets" }
|
||||
ui = { path = "../ui" }
|
||||
title_bar = { path = "../title_bar" }
|
||||
identity = { path = "../identity" }
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Error;
|
||||
use client_keys::ClientKeys;
|
||||
use global::constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH};
|
||||
use global::nostr_client;
|
||||
use common::display::DisplayProfile;
|
||||
use global::constants::DEFAULT_SIDEBAR_WIDTH;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, relative, Action, App, AppContext, Axis, Context, Entity, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window,
|
||||
actions, div, px, rems, Action, App, AppContext, Axis, Context, Entity, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use nostr_connect::prelude::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::{Registry, RoomEmitter};
|
||||
use serde::Deserialize;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::{ActiveTheme, Theme, ThemeMode};
|
||||
use title_bar::TitleBar;
|
||||
use ui::actions::OpenProfile;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::dock::DockPlacement;
|
||||
use ui::dock_area::panel::PanelView;
|
||||
use ui::dock_area::{ClosePanel, DockArea, DockItem};
|
||||
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::user_profile::UserProfile;
|
||||
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> {
|
||||
@@ -44,6 +50,8 @@ pub fn new_account(window: &mut Window, cx: &mut App) {
|
||||
ChatSpace::set_center_panel(panel, window, cx);
|
||||
}
|
||||
|
||||
actions!(user, [DarkMode, Settings, Logout]);
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
pub enum PanelKind {
|
||||
Room(u64),
|
||||
@@ -70,14 +78,15 @@ pub struct ToggleModal {
|
||||
}
|
||||
|
||||
pub struct ChatSpace {
|
||||
title_bar: Entity<TitleBar>,
|
||||
dock: Entity<DockArea>,
|
||||
toolbar: bool,
|
||||
#[allow(unused)]
|
||||
subscriptions: SmallVec<[Subscription; 5]>,
|
||||
}
|
||||
|
||||
impl ChatSpace {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let title_bar = cx.new(|_| TitleBar::new());
|
||||
let dock = cx.new(|cx| {
|
||||
let panel = Arc::new(startup::init(window, cx));
|
||||
let center = DockItem::panel(panel);
|
||||
@@ -99,14 +108,17 @@ impl ChatSpace {
|
||||
window,
|
||||
|_this: &mut Self, state, window, cx| {
|
||||
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)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.confirm()
|
||||
.button_props(
|
||||
ModalButtonProps::default()
|
||||
.cancel_text(t!("chatspace.create_new_keys"))
|
||||
.cancel_text(t!("startup.create_new_keys"))
|
||||
.ok_text(t!("common.allow")),
|
||||
)
|
||||
.child(
|
||||
@@ -124,13 +136,9 @@ impl ChatSpace {
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("chatspace.warning"))),
|
||||
.child(title.clone()),
|
||||
)
|
||||
.child(div().line_height(relative(1.4)).child(
|
||||
SharedString::new(t!(
|
||||
"chatspace.allow_keychain_access"
|
||||
)),
|
||||
)),
|
||||
.child(desc.clone()),
|
||||
)
|
||||
.on_cancel(|_, _window, cx| {
|
||||
ClientKeys::global(cx).update(cx, |this, cx| {
|
||||
@@ -157,21 +165,21 @@ impl ChatSpace {
|
||||
window,
|
||||
|this: &mut Self, state, window, cx| {
|
||||
if !state.read(cx).has_signer() {
|
||||
this.open_onboarding(window, cx);
|
||||
this.set_onboarding_panels(window, cx);
|
||||
} 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| {
|
||||
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| {
|
||||
if let Some(window) = window {
|
||||
this.load(window, cx);
|
||||
@@ -196,7 +204,7 @@ impl ChatSpace {
|
||||
this.add_panel(panel, DockPlacement::Center, window, cx);
|
||||
});
|
||||
} else {
|
||||
window.push_notification(t!("chatspace.failed_to_open_room"), cx);
|
||||
window.push_notification(t!("common.room_error"), cx);
|
||||
}
|
||||
}
|
||||
RoomEmitter::Close(..) => {
|
||||
@@ -216,16 +224,13 @@ impl ChatSpace {
|
||||
|
||||
Self {
|
||||
dock,
|
||||
title_bar,
|
||||
subscriptions,
|
||||
toolbar: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// No active user, disable user's toolbar
|
||||
self.toolbar(false, cx);
|
||||
|
||||
pub fn set_onboarding_panels(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let panel = Arc::new(onboarding::init(window, cx));
|
||||
let center = DockItem::panel(panel);
|
||||
|
||||
@@ -235,17 +240,14 @@ impl ChatSpace {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn open_chats(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Enable the toolbar for logged in users
|
||||
self.toolbar(true, cx);
|
||||
|
||||
// Load all chat rooms from database
|
||||
Registry::global(cx).update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
});
|
||||
|
||||
pub fn set_chat_panels(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let registry = Registry::global(cx);
|
||||
let weak_dock = self.dock.downgrade();
|
||||
|
||||
// The left panel will render sidebar
|
||||
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(
|
||||
Axis::Vertical,
|
||||
vec![DockItem::tabs(
|
||||
@@ -261,66 +263,31 @@ impl ChatSpace {
|
||||
cx,
|
||||
);
|
||||
|
||||
// Update dock
|
||||
self.dock.update(cx, |this, cx| {
|
||||
this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx);
|
||||
this.set_center(center, window, cx);
|
||||
});
|
||||
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
let verify_messaging_relays = this.verify_messaging_relays(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();
|
||||
// Load all chat rooms from the database
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn open_settings(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let settings = preferences::init(window, cx);
|
||||
let title = SharedString::new(t!("chatspace.preferences_title"));
|
||||
pub fn on_settings(&mut self, _ev: &Settings, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let view = preferences::init(window, cx);
|
||||
let title = SharedString::new(t!("common.preferences"));
|
||||
|
||||
window.open_modal(cx, move |modal, _, _| {
|
||||
modal
|
||||
.title(title.clone())
|
||||
.width(px(DEFAULT_MODAL_WIDTH))
|
||||
.child(settings.clone())
|
||||
.width(px(480.))
|
||||
.child(view.clone())
|
||||
});
|
||||
}
|
||||
|
||||
fn toolbar(&mut self, status: bool, 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) {
|
||||
fn on_dark_mode(&mut self, _ev: &DarkMode, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if cx.theme().mode.is_dark() {
|
||||
Theme::change(ThemeMode::Light, Some(window), cx);
|
||||
} else {
|
||||
@@ -328,23 +295,80 @@ impl ChatSpace {
|
||||
}
|
||||
}
|
||||
|
||||
fn logout(&self, window: &mut Window, cx: &mut App) {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
fn on_sign_out(&mut self, _ev: &Logout, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let identity = Identity::global(cx);
|
||||
// TODO: save current session?
|
||||
identity.update(cx, |this, cx| {
|
||||
this.unload(window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_open_profile(&mut self, a: &OpenProfile, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let public_key = a.0;
|
||||
fn on_open_profile(&mut self, ev: &OpenProfile, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let public_key = ev.0;
|
||||
let profile = user_profile::init(public_key, window, cx);
|
||||
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
// user_profile::init(public_key, window, cx)
|
||||
this.child(profile.clone())
|
||||
this.alert()
|
||||
.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 Ok(chatspace) = root.read(cx).view().clone().downcast::<ChatSpace>() {
|
||||
let panel = Arc::new(panel);
|
||||
@@ -365,7 +389,27 @@ impl Render for ChatSpace {
|
||||
let modal_layer = Root::render_modal_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()
|
||||
.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))
|
||||
.relative()
|
||||
.size_full()
|
||||
@@ -375,58 +419,7 @@ impl Render for ChatSpace {
|
||||
.flex_col()
|
||||
.size_full()
|
||||
// Title Bar
|
||||
.child(
|
||||
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);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(self.title_bar.clone())
|
||||
// Dock
|
||||
.child(self.dock.clone()),
|
||||
)
|
||||
|
||||
@@ -5,21 +5,16 @@ use std::time::Duration;
|
||||
use anyhow::{anyhow, Error};
|
||||
use assets::Assets;
|
||||
use auto_update::AutoUpdater;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use global::constants::APP_NAME;
|
||||
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,
|
||||
};
|
||||
use global::{nostr_client, NostrSignal};
|
||||
use gpui::{
|
||||
actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
|
||||
WindowBounds, WindowKind, WindowOptions,
|
||||
actions, point, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
|
||||
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 nostr_sdk::prelude::*;
|
||||
use registry::Registry;
|
||||
@@ -138,7 +133,7 @@ fn main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let duration = smol::Timer::after(Duration::from_secs(75));
|
||||
let duration = smol::Timer::after(Duration::from_secs(30));
|
||||
|
||||
let recv = || async {
|
||||
// prevent inline format
|
||||
@@ -202,25 +197,22 @@ fn main() {
|
||||
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
|
||||
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 {
|
||||
title: Some(SharedString::new_static(APP_NAME)),
|
||||
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
||||
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()
|
||||
};
|
||||
|
||||
|
||||
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::emoji_picker::EmojiPicker;
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::text::RichText;
|
||||
@@ -813,7 +814,7 @@ impl Panel for Chat {
|
||||
}
|
||||
|
||||
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
|
||||
.room
|
||||
.read(cx)
|
||||
@@ -825,11 +826,31 @@ impl Panel for Chat {
|
||||
.icon(IconName::EditFill)
|
||||
.tooltip(t!("chat.change_subject_button"))
|
||||
.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| {
|
||||
this.title(SharedString::new(t!("chat.change_subject_modal_title")))
|
||||
.child(subject.clone())
|
||||
let room = room.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)
|
||||
.child(
|
||||
Button::new("upload")
|
||||
.icon(Icon::new(IconName::Upload))
|
||||
.icon(IconName::Upload)
|
||||
.ghost()
|
||||
.large()
|
||||
.disabled(self.uploading)
|
||||
.loading(self.uploading)
|
||||
.on_click(cx.listener(
|
||||
@@ -903,7 +925,8 @@ impl Render for Chat {
|
||||
)
|
||||
.child(
|
||||
EmojiPicker::new(self.input.downgrade())
|
||||
.icon(IconName::EmojiFill),
|
||||
.icon(IconName::EmojiFill)
|
||||
.large(),
|
||||
),
|
||||
)
|
||||
.child(TextInput::new(&self.input)),
|
||||
|
||||
@@ -3,92 +3,53 @@ use gpui::{
|
||||
Styled, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use registry::Registry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{ContextModal, Sizable};
|
||||
use ui::{v_flex, Sizable};
|
||||
|
||||
pub fn init(
|
||||
id: u64,
|
||||
subject: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Subject> {
|
||||
Subject::new(id, subject, window, cx)
|
||||
pub fn init(subject: Option<String>, window: &mut Window, cx: &mut App) -> Entity<Subject> {
|
||||
Subject::new(subject, window, cx)
|
||||
}
|
||||
|
||||
pub struct Subject {
|
||||
id: u64,
|
||||
input: Entity<InputState>,
|
||||
}
|
||||
|
||||
impl Subject {
|
||||
pub fn new(
|
||||
id: u64,
|
||||
subject: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
pub fn new(subject: Option<String>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let input = cx.new(|cx| {
|
||||
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
|
||||
});
|
||||
|
||||
cx.new(|_| Self { id, input })
|
||||
cx.new(|_| Self { input })
|
||||
}
|
||||
|
||||
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let registry = Registry::global(cx).read(cx);
|
||||
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);
|
||||
}
|
||||
pub fn new_subject(&self, cx: &App) -> SharedString {
|
||||
self.input.read(cx).value().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Subject {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.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"))),
|
||||
),
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("subject.title"))),
|
||||
)
|
||||
.child(TextInput::new(&self.input).small())
|
||||
.child(
|
||||
Button::new("submit")
|
||||
.label(t!("common.change"))
|
||||
.primary()
|
||||
.w_full()
|
||||
.on_click(cx.listener(|this, _, window, cx| this.update(window, cx))),
|
||||
div()
|
||||
.text_xs()
|
||||
.italic()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(SharedString::new(t!("subject.help_text"))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ use global::constants::BOOTSTRAP_RELAYS;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, px, red, relative, uniform_list, App, AppContext, Context, Entity,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, Window,
|
||||
div, px, relative, rems, uniform_list, AppContext, Context, Entity, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled,
|
||||
Subscription, Task, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use itertools::Itertools;
|
||||
@@ -21,13 +21,29 @@ use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smol::Timer;
|
||||
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::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> {
|
||||
cx.new(|cx| Compose::new(window, cx))
|
||||
pub fn compose_button() -> impl IntoElement {
|
||||
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)]
|
||||
@@ -147,13 +163,13 @@ impl Compose {
|
||||
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);
|
||||
|
||||
if public_keys.is_empty() {
|
||||
self.set_error(Some(t!("compose.receiver_required").into()), cx);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading spinner
|
||||
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 public_key = signer.get_public_key().await?;
|
||||
|
||||
@@ -187,15 +203,17 @@ impl Compose {
|
||||
match event.await {
|
||||
Ok(room) => {
|
||||
cx.update(|window, cx| {
|
||||
let registry = Registry::global(cx);
|
||||
// Reset local state
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_submitting(false, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
Registry::global(cx).update(cx, |this, cx| {
|
||||
// Create and insert the new room into the registry
|
||||
registry.update(cx, |this, cx| {
|
||||
this.push_room(cx.new(|_| room), cx);
|
||||
});
|
||||
|
||||
// Close the current modal
|
||||
window.close_modal(cx);
|
||||
})
|
||||
.ok();
|
||||
@@ -371,22 +389,20 @@ impl Compose {
|
||||
let selected = entity.read(cx).select;
|
||||
|
||||
items.push(
|
||||
div()
|
||||
h_flex()
|
||||
.id(ix)
|
||||
.px_1()
|
||||
.h_9()
|
||||
.w_full()
|
||||
.h_11()
|
||||
.py_1()
|
||||
.px_3()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.rounded(cx.theme().radius)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.gap_1p5()
|
||||
.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()),
|
||||
)
|
||||
.when(selected, |this| {
|
||||
@@ -414,51 +430,52 @@ impl Render for Compose {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let label = if self.submitting {
|
||||
t!("compose.creating_dm_button")
|
||||
} else if self.contacts.len() > 1 {
|
||||
} else if self.selected(cx).len() > 1 {
|
||||
t!("compose.create_group_dm_button")
|
||||
} else {
|
||||
t!("compose.create_dm_button")
|
||||
};
|
||||
|
||||
let error = self.error_message.read(cx).as_ref();
|
||||
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.mb_4()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("compose.description"))),
|
||||
)
|
||||
.when_some(self.error_message.read(cx).as_ref(), |this, msg| {
|
||||
this.child(div().text_xs().text_color(red()).child(msg.clone()))
|
||||
.when_some(error, |this, msg| {
|
||||
this.child(
|
||||
div()
|
||||
.italic()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().danger_foreground)
|
||||
.child(msg.clone()),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div().flex().flex_col().child(
|
||||
div()
|
||||
.h_10()
|
||||
.border_b_1()
|
||||
.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()
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.h_10()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_sm()
|
||||
.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()
|
||||
.child(
|
||||
div()
|
||||
@@ -467,9 +484,7 @@ impl Render for Compose {
|
||||
.child(SharedString::new(t!("compose.to_label"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
TextInput::new(&self.user_input)
|
||||
@@ -479,8 +494,8 @@ impl Render for Compose {
|
||||
.child(
|
||||
Button::new("add")
|
||||
.icon(IconName::PlusCircleFill)
|
||||
.small()
|
||||
.ghost()
|
||||
.loading(self.adding)
|
||||
.disabled(self.adding)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.add_and_select_contact(window, cx);
|
||||
@@ -491,14 +506,12 @@ impl Render for Compose {
|
||||
.map(|this| {
|
||||
if self.contacts.is_empty() {
|
||||
this.child(
|
||||
div()
|
||||
.w_full()
|
||||
v_flex()
|
||||
.h_24()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_align(TextAlign::Center)
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
@@ -525,7 +538,7 @@ impl Render for Compose {
|
||||
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")
|
||||
.label(label)
|
||||
.primary()
|
||||
.small()
|
||||
.w_full()
|
||||
.loading(self.submitting)
|
||||
.disabled(self.submitting || self.adding)
|
||||
.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 ui::button::{Button, ButtonVariants};
|
||||
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> {
|
||||
EditProfile::new(window, cx)
|
||||
@@ -166,10 +166,7 @@ impl EditProfile {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Show loading spinner
|
||||
self.set_submitting(true, cx);
|
||||
|
||||
pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Option<Event>, Error>> {
|
||||
let avatar = self.avatar_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();
|
||||
@@ -191,52 +188,25 @@ impl EditProfile {
|
||||
new_metadata = new_metadata.website(url);
|
||||
}
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
nostr_client().set_metadata(&new_metadata).await?;
|
||||
Ok(())
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
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| {
|
||||
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();
|
||||
}
|
||||
};
|
||||
Ok(event)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_loading = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_submitting(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_submitting = status;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for EditProfile {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.px_3()
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
@@ -306,17 +276,5 @@ impl Render for EditProfile {
|
||||
.child(SharedString::new(t!("profile.label_bio")))
|
||||
.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,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use i18n::{shared_t, t};
|
||||
use identity::Identity;
|
||||
use nostr_connect::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
@@ -53,8 +53,11 @@ impl Login {
|
||||
let key_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("nsec... or bunker://..."));
|
||||
|
||||
let relay_input =
|
||||
cx.new(|cx| InputState::new(window, cx).default_value(NOSTR_CONNECT_RELAY));
|
||||
let relay_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.default_value(NOSTR_CONNECT_RELAY)
|
||||
.placeholder(NOSTR_CONNECT_RELAY)
|
||||
});
|
||||
|
||||
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||
//
|
||||
@@ -556,12 +559,12 @@ impl Render for Login {
|
||||
.text_xl()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child(SharedString::new(t!("login.title"))),
|
||||
.child(shared_t!("login.title")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("login.key_description"))),
|
||||
.child(shared_t!("login.key_description")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -581,13 +584,12 @@ impl Render for Login {
|
||||
})),
|
||||
)
|
||||
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
||||
let msg = t!("login.approve_message", i = i);
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.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| {
|
||||
@@ -603,89 +605,90 @@ impl Render for Login {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(cx.theme().surface_background)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_3()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.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().flex_1().p_1().child(
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(cx.theme().surface_background)
|
||||
.rounded(cx.theme().radius)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_3()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.id("")
|
||||
.mb_2()
|
||||
.p_2()
|
||||
.size_72()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.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_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").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);
|
||||
})),
|
||||
),
|
||||
),
|
||||
),
|
||||
.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 compose;
|
||||
pub mod edit_profile;
|
||||
pub mod login;
|
||||
pub mod messaging_relays;
|
||||
pub mod new_account;
|
||||
pub mod onboarding;
|
||||
pub mod preferences;
|
||||
pub mod relays;
|
||||
pub mod screening;
|
||||
pub mod sidebar;
|
||||
pub mod startup;
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
use anyhow::anyhow;
|
||||
use common::nip96::nip96_upload;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten,
|
||||
FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString,
|
||||
Styled, Window,
|
||||
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
|
||||
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
|
||||
Render, SharedString, Styled, WeakEntity, Window,
|
||||
};
|
||||
use gpui_tokio::Tokio;
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use smol::fs;
|
||||
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::input::{InputState, TextInput};
|
||||
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> {
|
||||
NewAccount::new(window, cx)
|
||||
@@ -25,7 +27,6 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
||||
pub struct NewAccount {
|
||||
name_input: Entity<InputState>,
|
||||
avatar_input: Entity<InputState>,
|
||||
bio_input: Entity<InputState>,
|
||||
is_uploading: bool,
|
||||
is_submitting: bool,
|
||||
// Panel
|
||||
@@ -46,19 +47,12 @@ impl NewAccount {
|
||||
.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 =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.png"));
|
||||
|
||||
Self {
|
||||
name_input,
|
||||
avatar_input,
|
||||
bio_input,
|
||||
is_uploading: false,
|
||||
is_submitting: false,
|
||||
name: "New Account".into(),
|
||||
@@ -69,140 +63,97 @@ impl NewAccount {
|
||||
}
|
||||
|
||||
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 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) {
|
||||
metadata = metadata.picture(url);
|
||||
};
|
||||
|
||||
let current_view = cx.entity().downgrade();
|
||||
let pwd_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||
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()),
|
||||
)
|
||||
identity.update(cx, |this, cx| {
|
||||
this.new_identity(metadata, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let nip96 = AppSettings::get_media_server(cx);
|
||||
let avatar_input = self.avatar_input.downgrade();
|
||||
self.uploading(true, cx);
|
||||
|
||||
// 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 {
|
||||
files: true,
|
||||
directories: false,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
self.set_uploading(true, cx);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let task = Tokio::spawn(cx, async move {
|
||||
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
||||
Ok(Some(mut paths)) => {
|
||||
let Some(path) = paths.pop() else {
|
||||
cx.update(|_, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_uploading(false, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
if let Some(path) = paths.pop() {
|
||||
let file = fs::read(path).await?;
|
||||
let url = nip96_upload(nostr_client(), &nip96_server, file).await?;
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
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(url)
|
||||
} else {
|
||||
Err(anyhow!("Path not found"))
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
cx.update(|_, cx| {
|
||||
Ok(None) => Err(anyhow!("User cancelled")),
|
||||
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.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();
|
||||
}
|
||||
Err(_) => {}
|
||||
Ok(Err(e)) => {
|
||||
Self::notify_error(cx, this, e.to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
Self::notify_error(cx, this, e.to_string());
|
||||
}
|
||||
}
|
||||
})
|
||||
.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;
|
||||
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;
|
||||
cx.notify();
|
||||
}
|
||||
@@ -243,93 +194,72 @@ impl Focusable for NewAccount {
|
||||
}
|
||||
|
||||
impl Render for NewAccount {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.relative()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_10()
|
||||
.child(
|
||||
div()
|
||||
.text_center()
|
||||
.text_lg()
|
||||
.text_center()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child(SharedString::new(t!("new_account.title"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_72()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
v_flex()
|
||||
.w_96()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.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()
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child(SharedString::new(t!("profile.label_name")))
|
||||
.child(SharedString::new(t!("new_account.name")))
|
||||
.child(TextInput::new(&self.name_input).small()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child(SharedString::new(t!("profile.label_bio")))
|
||||
.child(TextInput::new(&self.bio_input).small()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.my_2()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(cx.theme().elevated_surface_background),
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.child(SharedString::new(t!("new_account.avatar"))),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_1()
|
||||
.h_32()
|
||||
.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(
|
||||
Button::new("submit")
|
||||
.label(SharedString::new(t!("common.continue")))
|
||||
|
||||
@@ -15,7 +15,7 @@ use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::checkbox::Checkbox;
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
@@ -244,12 +244,13 @@ impl Render for Onboarding {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().w_24().absolute().bottom_4().right_4().child(
|
||||
Button::new("unload")
|
||||
div().w_24().absolute().bottom_2().right_2().child(
|
||||
Button::new("logout")
|
||||
.icon(IconName::Logout)
|
||||
.label(SharedString::new(t!("common.logout")))
|
||||
.ghost()
|
||||
.small()
|
||||
.label(SharedString::new(t!("user.sign_out")))
|
||||
.danger()
|
||||
.xsmall()
|
||||
.rounded(ButtonRounded::Full)
|
||||
.disabled(self.loading)
|
||||
.on_click(|_, window, cx| {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use common::display::DisplayProfile;
|
||||
use global::constants::{DEFAULT_MODAL_WIDTH, NIP96_SERVER};
|
||||
use gpui::http_client::Url;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
@@ -14,10 +13,11 @@ use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::switch::Switch;
|
||||
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> {
|
||||
Preferences::new(window, cx)
|
||||
@@ -33,8 +33,8 @@ impl Preferences {
|
||||
let media_server = AppSettings::get_media_server(cx).to_string();
|
||||
let media_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.default_value(media_server)
|
||||
.placeholder(NIP96_SERVER)
|
||||
.default_value(media_server.clone())
|
||||
.placeholder(media_server)
|
||||
});
|
||||
|
||||
Self { media_input }
|
||||
@@ -42,25 +42,73 @@ impl Preferences {
|
||||
}
|
||||
|
||||
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"));
|
||||
|
||||
window.open_modal(cx, move |modal, _window, _cx| {
|
||||
let weak_view = weak_view.clone();
|
||||
|
||||
modal
|
||||
.confirm()
|
||||
.title(title.clone())
|
||||
.width(px(DEFAULT_MODAL_WIDTH))
|
||||
.child(edit_profile.clone())
|
||||
.child(view.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>) {
|
||||
let relays = relays::init(window, cx);
|
||||
let title = SharedString::new(t!("preferences.modal_relays_title"));
|
||||
let title = SharedString::new(t!("relays.modal_title"));
|
||||
let view = messaging_relays::init(window, cx);
|
||||
let weak_view = view.downgrade();
|
||||
|
||||
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())
|
||||
.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()
|
||||
.child(
|
||||
div()
|
||||
v_flex()
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
@@ -135,7 +181,7 @@ impl Render for Preferences {
|
||||
)
|
||||
.child(
|
||||
Button::new("relays")
|
||||
.label("DM Relays")
|
||||
.label("Messaging Relays")
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
@@ -146,11 +192,8 @@ impl Render for Preferences {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
v_flex()
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
@@ -162,6 +205,7 @@ impl Render for Preferences {
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.my_1()
|
||||
.flex()
|
||||
.items_start()
|
||||
.gap_1()
|
||||
@@ -193,10 +237,8 @@ impl Render for Preferences {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
v_flex()
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().border)
|
||||
@@ -208,9 +250,7 @@ impl Render for Preferences {
|
||||
.child(SharedString::new(t!("preferences.messages_header"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Switch::new("screening")
|
||||
@@ -242,10 +282,8 @@ impl Render for Preferences {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
v_flex()
|
||||
.py_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().border)
|
||||
@@ -257,9 +295,7 @@ impl Render for Preferences {
|
||||
.child(SharedString::new(t!("preferences.display_header"))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
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 common::debounced_delay::DebouncedDelay;
|
||||
use common::display::{DisplayProfile, TextUtils};
|
||||
use global::constants::{BOOTSTRAP_RELAYS, DEFAULT_MODAL_WIDTH, SEARCH_RELAYS};
|
||||
use common::display::TextUtils;
|
||||
use global::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, relative, rems, uniform_list, AnyElement, App, AppContext, ClipboardItem, Context,
|
||||
Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
||||
Task, Window,
|
||||
div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString,
|
||||
Styled, Subscription, Task, Window,
|
||||
};
|
||||
use gpui_tokio::Tokio;
|
||||
use i18n::t;
|
||||
@@ -25,7 +24,6 @@ use registry::{Registry, RoomEmitter};
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
@@ -34,8 +32,6 @@ use ui::popup_menu::PopupMenu;
|
||||
use ui::skeleton::Skeleton;
|
||||
use ui::{v_flex, ContextModal, IconName, Selectable, Sizable, StyledExt};
|
||||
|
||||
use crate::views::compose;
|
||||
|
||||
mod list_item;
|
||||
|
||||
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>) {
|
||||
let title = SharedString::new(t!("sidebar.loading_modal_title"));
|
||||
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> {
|
||||
(0..total).map(|_| {
|
||||
div()
|
||||
@@ -727,9 +668,6 @@ impl Focusable for Sidebar {
|
||||
impl Render for Sidebar {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
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
|
||||
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())
|
||||
.size_full()
|
||||
.relative()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
// Account
|
||||
.when_some(profile, |this, profile| {
|
||||
this.child(self.account(&profile, cx))
|
||||
})
|
||||
// Search Input
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.px_3()
|
||||
.mt_3()
|
||||
.px_2p5()
|
||||
.w_full()
|
||||
.h_7()
|
||||
.flex_none()
|
||||
@@ -781,14 +714,12 @@ impl Render for Sidebar {
|
||||
)
|
||||
// Chat Rooms
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.w_full()
|
||||
.flex_1()
|
||||
.overflow_y_hidden()
|
||||
.flex()
|
||||
.flex_col()
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.px_1p5()
|
||||
.w_full()
|
||||
.overflow_y_hidden()
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
@@ -879,8 +810,11 @@ impl Render for Sidebar {
|
||||
),
|
||||
)
|
||||
.when(registry.loading, |this| {
|
||||
let title = SharedString::new(t!("sidebar.retrieving_messages"));
|
||||
let desc = SharedString::new(t!("sidebar.retrieving_messages_description"));
|
||||
|
||||
this.child(
|
||||
div().absolute().bottom_4().px_4().w_full().child(
|
||||
div().absolute().bottom_3().px_3().w_full().child(
|
||||
div()
|
||||
.p_1()
|
||||
.w_full()
|
||||
@@ -890,35 +824,28 @@ impl Render for Sidebar {
|
||||
.justify_between()
|
||||
.bg(cx.theme().panel_background)
|
||||
.shadow_sm()
|
||||
// Empty div
|
||||
.child(div().size_6().flex_shrink_0())
|
||||
// Loading indicator
|
||||
// Loading
|
||||
.child(div().flex_shrink_0().pl_1().child(Indicator::new().small()))
|
||||
// Title
|
||||
.child(
|
||||
div()
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.line_height(relative(1.2))
|
||||
.child(Indicator::new().xsmall())
|
||||
.child(SharedString::new(t!(
|
||||
"sidebar.retrieving_messages"
|
||||
))),
|
||||
.child(title.clone()),
|
||||
)
|
||||
.child(div().text_color(cx.theme().text_muted).child(
|
||||
SharedString::new(t!(
|
||||
"sidebar.retrieving_messages_description"
|
||||
)),
|
||||
)),
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(desc.clone()),
|
||||
),
|
||||
)
|
||||
// Info button
|
||||
.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
|
||||
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||
return;
|
||||
@@ -100,11 +100,6 @@ impl UserProfile {
|
||||
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>) {
|
||||
let Ok(bech32) = self.public_key.to_bech32();
|
||||
let item = ClipboardItem::new_string(bech32);
|
||||
@@ -221,7 +216,7 @@ impl Render for UserProfile {
|
||||
.child(shared_bech32),
|
||||
)
|
||||
.child(
|
||||
Button::new("copy-pubkey")
|
||||
Button::new("copy")
|
||||
.icon({
|
||||
if self.copied {
|
||||
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
|
||||
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
|
||||
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
||||
|
||||
@@ -31,7 +31,10 @@ impl Global for GlobalIdentity {}
|
||||
|
||||
pub struct Identity {
|
||||
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)]
|
||||
subscriptions: SmallVec<[Subscription; 1]>,
|
||||
}
|
||||
@@ -66,14 +69,17 @@ impl Identity {
|
||||
this.set_logging_in(true, cx);
|
||||
this.load(window, cx);
|
||||
} else {
|
||||
this.set_public_key(None, cx);
|
||||
this.set_public_key(None, window, cx);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
Self {
|
||||
public_key: None,
|
||||
auto_logging_in_progress: false,
|
||||
relay_ready: None,
|
||||
need_backup: None,
|
||||
need_onboarding: false,
|
||||
logging_in: false,
|
||||
subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -105,8 +111,11 @@ impl Identity {
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_public_key(None, cx);
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_public_key(None, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -129,19 +138,24 @@ impl Identity {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| match task.await {
|
||||
Ok(_) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_public_key(None, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_public_key(None, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -158,13 +172,13 @@ impl Identity {
|
||||
self.login_with_bunker(uri, window, cx);
|
||||
} else {
|
||||
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) {
|
||||
self.login_with_keys(enc, window, cx);
|
||||
} else {
|
||||
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"),
|
||||
cx,
|
||||
);
|
||||
self.set_public_key(None, cx);
|
||||
self.set_public_key(None, window, cx);
|
||||
return;
|
||||
};
|
||||
// Automatically open auth url
|
||||
@@ -204,7 +218,7 @@ impl Identity {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_public_key(None, cx);
|
||||
this.set_public_key(None, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -239,10 +253,10 @@ impl Identity {
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.confirm()
|
||||
.on_cancel(move |_, _window, cx| {
|
||||
.on_cancel(move |_, window, cx| {
|
||||
entity
|
||||
.update(cx, |this, cx| {
|
||||
this.set_public_key(None, cx);
|
||||
this.set_public_key(None, window, cx);
|
||||
})
|
||||
.ok();
|
||||
// Close modal
|
||||
@@ -341,8 +355,10 @@ impl Identity {
|
||||
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
// Update signer
|
||||
client.set_signer(signer).await;
|
||||
|
||||
// Subscribe for user metadata
|
||||
Self::subscribe(client, public_key).await?;
|
||||
|
||||
@@ -352,8 +368,11 @@ impl Identity {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(public_key) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_public_key(Some(public_key), cx);
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_public_key(Some(public_key), window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -368,10 +387,9 @@ impl Identity {
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Creates a new identity with the given keys and metadata
|
||||
/// Creates a new identity with the given metadata
|
||||
pub fn new_identity(
|
||||
&mut self,
|
||||
password: String,
|
||||
metadata: Metadata,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -382,34 +400,32 @@ impl Identity {
|
||||
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let public_key = async_keys.public_key();
|
||||
|
||||
// Update signer
|
||||
client.set_signer(async_keys).await;
|
||||
|
||||
// Set metadata
|
||||
client.set_metadata(&metadata).await?;
|
||||
|
||||
// Create relay list
|
||||
let relay_list = EventBuilder::new(Kind::RelayList, "").tags(
|
||||
NIP65_RELAYS.into_iter().filter_map(|url| {
|
||||
if let Ok(url) = RelayUrl::parse(url) {
|
||||
Some(Tag::relay_metadata(url, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
NIP65_RELAYS
|
||||
.into_iter()
|
||||
.filter_map(|url| RelayUrl::parse(url).ok())
|
||||
.map(|url| Tag::relay_metadata(url, None)),
|
||||
);
|
||||
|
||||
// Create messaging relay list
|
||||
let dm_relay = EventBuilder::new(Kind::InboxRelays, "").tags(
|
||||
NIP17_RELAYS.into_iter().filter_map(|url| {
|
||||
if let Ok(url) = RelayUrl::parse(url) {
|
||||
Some(Tag::relay(url))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
NIP17_RELAYS
|
||||
.into_iter()
|
||||
.filter_map(|url| RelayUrl::parse(url).ok())
|
||||
.map(Tag::relay),
|
||||
);
|
||||
|
||||
// Set user's NIP65 relays
|
||||
client.send_event_builder(relay_list).await?;
|
||||
// Set user's NIP17 relays
|
||||
client.send_event_builder(dm_relay).await?;
|
||||
|
||||
// Subscribe for user metadata
|
||||
@@ -421,9 +437,13 @@ impl Identity {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(public_key) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.write_keys(&keys, password, cx);
|
||||
this.set_public_key(Some(public_key), cx);
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_public_key(Some(public_key), window, cx);
|
||||
this.set_need_backup(Some(keys), cx);
|
||||
this.set_need_onboarding(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -438,6 +458,40 @@ impl Identity {
|
||||
.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>) {
|
||||
let mut value = uri.to_string();
|
||||
|
||||
@@ -469,6 +523,7 @@ impl Identity {
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Writes the keys to the database
|
||||
pub fn write_keys(&self, keys: &Keys, password: String, cx: &mut Context<Self>) {
|
||||
let keys = keys.to_owned();
|
||||
let public_key = keys.public_key();
|
||||
@@ -495,9 +550,61 @@ impl Identity {
|
||||
.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;
|
||||
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
|
||||
@@ -510,12 +617,18 @@ impl Identity {
|
||||
self.public_key.is_some()
|
||||
}
|
||||
|
||||
pub fn logging_in(&self) -> bool {
|
||||
self.auto_logging_in_progress
|
||||
pub fn relay_ready(&self) -> Option<bool> {
|
||||
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>) {
|
||||
self.auto_logging_in_progress = status;
|
||||
self.logging_in = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,20 @@ use colors::{brand, hsl, neutral};
|
||||
use gpui::{px, App, Global, Hsla, Pixels, SharedString, Window, WindowAppearance};
|
||||
|
||||
use crate::colors::{danger, warning};
|
||||
use crate::platform_kind::PlatformKind;
|
||||
use crate::scrollbar_mode::ScrollBarMode;
|
||||
|
||||
mod colors;
|
||||
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) {
|
||||
Theme::sync_system_appearance(None, cx);
|
||||
}
|
||||
@@ -21,7 +31,7 @@ pub struct ThemeColor {
|
||||
pub panel_background: Hsla,
|
||||
pub overlay: Hsla,
|
||||
pub title_bar: Hsla,
|
||||
pub title_bar_border: Hsla,
|
||||
pub title_bar_inactive: Hsla,
|
||||
pub window_border: Hsla,
|
||||
|
||||
// Border colors
|
||||
@@ -78,6 +88,7 @@ pub struct ThemeColor {
|
||||
|
||||
// Ghost element colors
|
||||
pub ghost_element_background: Hsla,
|
||||
pub ghost_element_background_alt: Hsla,
|
||||
pub ghost_element_hover: Hsla,
|
||||
pub ghost_element_active: Hsla,
|
||||
pub ghost_element_selected: Hsla,
|
||||
@@ -116,7 +127,7 @@ impl ThemeColor {
|
||||
panel_background: gpui::white(),
|
||||
overlay: neutral().light_alpha().step_3(),
|
||||
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),
|
||||
|
||||
border: neutral().light().step_6(),
|
||||
@@ -158,16 +169,17 @@ impl ThemeColor {
|
||||
danger_disabled: danger().light_alpha().step_3(),
|
||||
|
||||
warning_foreground: warning().light().step_12(),
|
||||
warning_background: warning().light().step_9(),
|
||||
warning_hover: warning().light_alpha().step_10(),
|
||||
warning_active: warning().light().step_10(),
|
||||
warning_selected: warning().light().step_11(),
|
||||
warning_background: warning().light().step_3(),
|
||||
warning_hover: warning().light_alpha().step_4(),
|
||||
warning_active: warning().light().step_5(),
|
||||
warning_selected: warning().light().step_5(),
|
||||
warning_disabled: warning().light_alpha().step_3(),
|
||||
|
||||
ghost_element_background: gpui::transparent_black(),
|
||||
ghost_element_hover: neutral().light_alpha().step_3(),
|
||||
ghost_element_active: neutral().light_alpha().step_5(),
|
||||
ghost_element_selected: neutral().light_alpha().step_5(),
|
||||
ghost_element_background_alt: neutral().light().step_3(),
|
||||
ghost_element_hover: neutral().light_alpha().step_4(),
|
||||
ghost_element_active: neutral().light().step_5(),
|
||||
ghost_element_selected: neutral().light().step_5(),
|
||||
ghost_element_disabled: neutral().light_alpha().step_2(),
|
||||
|
||||
tab_inactive_background: neutral().light().step_3(),
|
||||
@@ -197,7 +209,7 @@ impl ThemeColor {
|
||||
panel_background: gpui::black(),
|
||||
overlay: neutral().dark_alpha().step_3(),
|
||||
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),
|
||||
|
||||
border: neutral().dark().step_6(),
|
||||
@@ -239,16 +251,17 @@ impl ThemeColor {
|
||||
danger_disabled: danger().dark_alpha().step_3(),
|
||||
|
||||
warning_foreground: warning().dark().step_12(),
|
||||
warning_background: warning().dark().step_9(),
|
||||
warning_hover: warning().dark_alpha().step_10(),
|
||||
warning_active: warning().dark().step_10(),
|
||||
warning_selected: warning().dark().step_11(),
|
||||
warning_background: warning().dark().step_3(),
|
||||
warning_hover: warning().dark_alpha().step_4(),
|
||||
warning_active: warning().dark().step_5(),
|
||||
warning_selected: warning().dark().step_5(),
|
||||
warning_disabled: warning().dark_alpha().step_3(),
|
||||
|
||||
ghost_element_background: gpui::transparent_black(),
|
||||
ghost_element_hover: neutral().dark_alpha().step_3(),
|
||||
ghost_element_active: neutral().dark_alpha().step_4(),
|
||||
ghost_element_selected: neutral().dark_alpha().step_5(),
|
||||
ghost_element_background_alt: neutral().dark().step_3(),
|
||||
ghost_element_hover: neutral().dark_alpha().step_4(),
|
||||
ghost_element_active: neutral().dark().step_5(),
|
||||
ghost_element_selected: neutral().dark().step_5(),
|
||||
ghost_element_disabled: neutral().dark_alpha().step_2(),
|
||||
|
||||
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)]
|
||||
pub struct Theme {
|
||||
pub colors: ThemeColor,
|
||||
@@ -335,6 +330,7 @@ pub struct Theme {
|
||||
pub font_size: Pixels,
|
||||
pub radius: Pixels,
|
||||
pub scrollbar_mode: ScrollBarMode,
|
||||
pub platform_kind: PlatformKind,
|
||||
}
|
||||
|
||||
impl Deref for Theme {
|
||||
@@ -412,6 +408,7 @@ impl From<ThemeColor> for Theme {
|
||||
font_family: ".SystemUIFont".into(),
|
||||
radius: px(5.),
|
||||
scrollbar_mode: ScrollBarMode::default(),
|
||||
platform_kind: PlatformKind::platform(),
|
||||
mode,
|
||||
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::tooltip::Tooltip;
|
||||
use crate::{Disableable, Icon, Selectable, Sizable, Size, StyledExt};
|
||||
use crate::{h_flex, Disableable, Icon, Selectable, Sizable, Size, StyledExt};
|
||||
|
||||
pub enum ButtonRounded {
|
||||
Normal,
|
||||
@@ -48,7 +48,12 @@ pub trait ButtonVariants: Sized {
|
||||
|
||||
/// With the ghost style for the Button.
|
||||
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.
|
||||
@@ -100,7 +105,7 @@ pub enum ButtonVariant {
|
||||
Secondary,
|
||||
Danger,
|
||||
Warning,
|
||||
Ghost,
|
||||
Ghost { alt: bool },
|
||||
Transparent,
|
||||
Custom(ButtonCustomVariant),
|
||||
}
|
||||
@@ -118,19 +123,26 @@ type OnClick = Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>
|
||||
pub struct Button {
|
||||
pub base: Div,
|
||||
id: ElementId,
|
||||
|
||||
icon: Option<Icon>,
|
||||
label: Option<SharedString>,
|
||||
tooltip: Option<SharedString>,
|
||||
children: Vec<AnyElement>,
|
||||
disabled: bool,
|
||||
|
||||
variant: ButtonVariant,
|
||||
rounded: ButtonRounded,
|
||||
size: Size,
|
||||
|
||||
disabled: bool,
|
||||
reverse: bool,
|
||||
bold: bool,
|
||||
tooltip: Option<SharedString>,
|
||||
on_click: OnClick,
|
||||
cta: bool,
|
||||
|
||||
loading: bool,
|
||||
loading_icon: Option<Icon>,
|
||||
|
||||
on_click: OnClick,
|
||||
|
||||
pub(crate) selected: bool,
|
||||
pub(crate) stop_propagation: bool,
|
||||
}
|
||||
@@ -159,6 +171,7 @@ impl Button {
|
||||
loading: false,
|
||||
reverse: false,
|
||||
bold: false,
|
||||
cta: false,
|
||||
children: Vec::new(),
|
||||
loading_icon: None,
|
||||
}
|
||||
@@ -200,28 +213,38 @@ impl Button {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set bold the button (label will be use the semi-bold font).
|
||||
pub fn bold(mut self) -> Self {
|
||||
self.bold = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
/// Set the cta style of the button.
|
||||
pub fn cta(mut self) -> Self {
|
||||
self.cta = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the stop propagation of the button.
|
||||
pub fn stop_propagation(mut self, val: bool) -> Self {
|
||||
self.stop_propagation = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the loading icon of the button.
|
||||
pub fn loading_icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.loading_icon = Some(icon.into());
|
||||
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 {
|
||||
@@ -280,7 +303,7 @@ impl RenderOnce for Button {
|
||||
let normal_style = style.normal(window, cx);
|
||||
let icon_size = match self.size {
|
||||
Size::Size(v) => Size::Size(v * 0.75),
|
||||
Size::Medium => Size::Small,
|
||||
Size::Large => Size::Medium,
|
||||
_ => self.size,
|
||||
};
|
||||
|
||||
@@ -300,25 +323,67 @@ impl RenderOnce for Button {
|
||||
// Icon Button
|
||||
match self.size {
|
||||
Size::Size(px) => this.size(px),
|
||||
Size::XSmall => this.size_5(),
|
||||
Size::Small => this.size_6(),
|
||||
Size::Medium => this.size_7(),
|
||||
_ => this.size_9(),
|
||||
Size::XSmall => {
|
||||
if self.cta {
|
||||
this.w_10().h_5()
|
||||
} 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 {
|
||||
// Normal Button
|
||||
match self.size {
|
||||
Size::Size(size) => this.px(size * 0.2),
|
||||
Size::XSmall => this.h_6().px_2(),
|
||||
Size::Small => {
|
||||
Size::XSmall => {
|
||||
if self.icon.is_some() {
|
||||
this.h_7().pl_2().pr_3()
|
||||
this.h_6().pl_2().pr_2p5()
|
||||
} 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()
|
||||
})
|
||||
.child({
|
||||
div()
|
||||
.flex()
|
||||
.when(self.reverse, |this| this.flex_row_reverse())
|
||||
h_flex()
|
||||
.id("label")
|
||||
.items_center()
|
||||
.when(self.reverse, |this| this.flex_row_reverse())
|
||||
.justify_center()
|
||||
.text_sm()
|
||||
.map(|this| match self.size {
|
||||
Size::XSmall => this.gap_0p5(),
|
||||
Size::Small => this.gap_1(),
|
||||
_ => this.gap_2().font_medium(),
|
||||
Size::XSmall => this.text_xs().gap_1(),
|
||||
Size::Small => this.text_sm().gap_1p5(),
|
||||
_ => this.text_sm().gap_2(),
|
||||
})
|
||||
.when(!self.loading, |this| {
|
||||
this.when_some(self.icon, |this, icon| {
|
||||
@@ -421,8 +483,15 @@ impl ButtonVariant {
|
||||
ButtonVariant::Secondary => cx.theme().elevated_surface_background,
|
||||
ButtonVariant::Danger => cx.theme().danger_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,
|
||||
_ => cx.theme().ghost_element_background,
|
||||
_ => gpui::transparent_black(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,7 +502,13 @@ impl ButtonVariant {
|
||||
ButtonVariant::Danger => cx.theme().danger_foreground,
|
||||
ButtonVariant::Warning => cx.theme().warning_foreground,
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -444,14 +519,14 @@ impl ButtonVariant {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_hover,
|
||||
ButtonVariant::Danger => cx.theme().danger_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::Custom(colors) => colors.hover,
|
||||
};
|
||||
|
||||
let fg = match self {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_foreground,
|
||||
ButtonVariant::Ghost => cx.theme().text,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().text,
|
||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||
_ => self.text_color(window, cx),
|
||||
};
|
||||
@@ -465,7 +540,7 @@ impl ButtonVariant {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_active,
|
||||
ButtonVariant::Danger => cx.theme().danger_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::Custom(colors) => colors.active,
|
||||
};
|
||||
@@ -485,7 +560,7 @@ impl ButtonVariant {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_selected,
|
||||
ButtonVariant::Danger => cx.theme().danger_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::Custom(colors) => colors.active,
|
||||
};
|
||||
@@ -501,7 +576,9 @@ impl ButtonVariant {
|
||||
|
||||
fn disabled(&self, _window: &Window, cx: &App) -> ButtonVariantStyle {
|
||||
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,
|
||||
_ => cx.theme().element_disabled,
|
||||
};
|
||||
|
||||
@@ -1,34 +1,21 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, Action, App, AppContext, Corner, Element, InteractiveElement, IntoElement,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, WeakEntity,
|
||||
Window,
|
||||
div, px, App, AppContext, Corner, Element, InteractiveElement, IntoElement, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::button::{Button, ButtonVariants};
|
||||
use crate::input::InputState;
|
||||
use crate::popover::{Popover, PopoverContent};
|
||||
use crate::Icon;
|
||||
use crate::{Icon, Sizable, Size};
|
||||
|
||||
/// Emit a emoji to target input
|
||||
#[derive(Action, PartialEq, Clone, Debug, Deserialize)]
|
||||
#[action(namespace = emoji, no_json)]
|
||||
pub struct EmitEmoji(pub SharedString);
|
||||
static EMOJIS: OnceLock<Vec<SharedString>> = OnceLock::new();
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct EmojiPicker {
|
||||
icon: Option<Icon>,
|
||||
anchor: Option<Corner>,
|
||||
target_input: WeakEntity<InputState>,
|
||||
emojis: Rc<Vec<SharedString>>,
|
||||
}
|
||||
|
||||
impl EmojiPicker {
|
||||
pub fn new(target_input: WeakEntity<InputState>) -> Self {
|
||||
fn get_emojis() -> &'static Vec<SharedString> {
|
||||
EMOJIS.get_or_init(|| {
|
||||
let mut emojis: Vec<SharedString> = vec![];
|
||||
|
||||
emojis.extend(
|
||||
@@ -38,9 +25,23 @@ impl EmojiPicker {
|
||||
.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 {
|
||||
target_input,
|
||||
emojis: emojis.into(),
|
||||
size: Size::default(),
|
||||
anchor: 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 {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
Popover::new("emoji-picker")
|
||||
@@ -70,10 +78,10 @@ impl RenderOnce for EmojiPicker {
|
||||
.trigger(
|
||||
Button::new("emoji-trigger")
|
||||
.when_some(self.icon, |this, icon| this.icon(icon))
|
||||
.ghost(),
|
||||
.ghost()
|
||||
.with_size(self.size),
|
||||
)
|
||||
.content(move |window, cx| {
|
||||
let emojis = self.emojis.clone();
|
||||
let input = self.target_input.clone();
|
||||
|
||||
cx.new(|cx| {
|
||||
@@ -83,7 +91,7 @@ impl RenderOnce for EmojiPicker {
|
||||
.flex_wrap()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.children(emojis.iter().map(|e| {
|
||||
.children(get_emojis().iter().map(|e| {
|
||||
div()
|
||||
.id(e.clone())
|
||||
.flex_auto()
|
||||
|
||||
@@ -3,7 +3,6 @@ pub use focusable::FocusableCycle;
|
||||
pub use icon::*;
|
||||
pub use root::{ContextModal, Root};
|
||||
pub use styled::*;
|
||||
pub use title_bar::*;
|
||||
pub use window_border::{window_border, WindowBorder};
|
||||
|
||||
pub use crate::Disableable;
|
||||
@@ -39,7 +38,6 @@ mod focusable;
|
||||
mod icon;
|
||||
mod root;
|
||||
mod styled;
|
||||
mod title_bar;
|
||||
mod window_border;
|
||||
|
||||
i18n::init!();
|
||||
|
||||
@@ -45,7 +45,7 @@ impl Default for ModalButtonProps {
|
||||
ok_text: None,
|
||||
ok_variant: ButtonVariant::Primary,
|
||||
cancel_text: None,
|
||||
cancel_variant: ButtonVariant::Ghost,
|
||||
cancel_variant: ButtonVariant::Ghost { alt: false },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,12 +450,18 @@ impl RenderOnce for Modal {
|
||||
.top(y)
|
||||
.w(self.width)
|
||||
.when_some(self.max_width, |this, w| this.max_w(w))
|
||||
.child(h_flex().h_4().px_3().justify_center().when_some(
|
||||
self.title,
|
||||
|this, title| {
|
||||
this.h_12().font_semibold().text_center().child(title)
|
||||
},
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.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| {
|
||||
this.child(
|
||||
Button::new("close")
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, AnyView, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
|
||||
ParentElement as _, Render, Styled, Window,
|
||||
div, AnyView, App, AppContext, Context, Decorations, Entity, FocusHandle, InteractiveElement,
|
||||
IntoElement, ParentElement as _, Render, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
|
||||
|
||||
use crate::input::InputState;
|
||||
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 {
|
||||
let base_font_size = cx.theme().font_size;
|
||||
let font_family = cx.theme().font_family.clone();
|
||||
let decorations = window.window_decorations();
|
||||
|
||||
window.set_rem_size(base_font_size);
|
||||
|
||||
window_border().child(
|
||||
div()
|
||||
.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()
|
||||
.size_full()
|
||||
.font_family(font_family)
|
||||
|
||||
@@ -9,7 +9,8 @@ use gpui::{
|
||||
IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
||||
Position, ScrollHandle, ScrollWheelEvent, Size, UniformListScrollHandle, Window,
|
||||
};
|
||||
use theme::{ActiveTheme, ScrollBarMode};
|
||||
use theme::scrollbar_mode::ScrollBarMode;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::AxisExt;
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@ pub fn v_flex() -> Div {
|
||||
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 {
|
||||
($fn:ident, $const:ident) => {
|
||||
/// [docs](https://tailwindcss.com/docs/font-weight)
|
||||
|
||||
@@ -233,7 +233,7 @@ impl Element for Switch {
|
||||
.when_some(self.description.clone(), |this, description| {
|
||||
this.child(
|
||||
div()
|
||||
.w_3_4()
|
||||
.pr_2()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.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,
|
||||
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);
|
||||
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);
|
||||
const WINDOW_BORDER_WIDTH: Pixels = Pixels(1.0);
|
||||
|
||||
/// Create a new window border.
|
||||
pub fn window_border() -> WindowBorder {
|
||||
@@ -29,7 +24,7 @@ pub fn window_paddings(window: &Window, _cx: &App) -> Edges<Pixels> {
|
||||
match window.window_decorations() {
|
||||
Decorations::Server => Edges::all(px(0.0)),
|
||||
Decorations::Client { tiling } => {
|
||||
let mut paddings = Edges::all(SHADOW_SIZE);
|
||||
let mut paddings = Edges::all(CLIENT_SIDE_DECORATION_SHADOW);
|
||||
if tiling.top {
|
||||
paddings.top = px(0.0);
|
||||
}
|
||||
@@ -62,9 +57,9 @@ impl ParentElement 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();
|
||||
window.set_client_inset(SHADOW_SIZE);
|
||||
window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW);
|
||||
|
||||
div()
|
||||
.id("window-backdrop")
|
||||
@@ -87,7 +82,9 @@ impl RenderOnce for WindowBorder {
|
||||
move |_bounds, hitbox, window, _cx| {
|
||||
let mouse = window.mouse_position();
|
||||
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;
|
||||
};
|
||||
window.set_cursor_style(
|
||||
@@ -113,20 +110,26 @@ impl RenderOnce for WindowBorder {
|
||||
.absolute(),
|
||||
)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(BORDER_RADIUS)
|
||||
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.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, |div| div.pb(SHADOW_SIZE))
|
||||
.when(!tiling.left, |div| div.pl(SHADOW_SIZE))
|
||||
.when(!tiling.right, |div| div.pr(SHADOW_SIZE))
|
||||
.when(!(tiling.bottom || tiling.right), |div| {
|
||||
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.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| {
|
||||
let size = window.window_bounds().get_bounds().size;
|
||||
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)
|
||||
};
|
||||
}),
|
||||
@@ -137,17 +140,22 @@ impl RenderOnce for WindowBorder {
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling } => div
|
||||
.border_color(cx.theme().window_border)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(BORDER_RADIUS)
|
||||
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.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, |div| div.border_b(BORDER_SIZE))
|
||||
.when(!tiling.left, |div| div.border_l(BORDER_SIZE))
|
||||
.when(!tiling.right, |div| div.border_r(BORDER_SIZE))
|
||||
.when(!(tiling.bottom || tiling.right), |div| {
|
||||
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.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| {
|
||||
div.shadow(vec![gpui::BoxShadow {
|
||||
color: Hsla {
|
||||
@@ -156,7 +164,7 @@ impl RenderOnce for WindowBorder {
|
||||
l: 0.,
|
||||
a: 0.3,
|
||||
},
|
||||
blur_radius: SHADOW_SIZE / 2.,
|
||||
blur_radius: CLIENT_SIDE_DECORATION_SHADOW / 2.,
|
||||
spread_radius: px(0.),
|
||||
offset: point(px(0.0), px(0.0)),
|
||||
}])
|
||||
|
||||
@@ -5,25 +5,41 @@ common:
|
||||
en: "Add"
|
||||
update:
|
||||
en: "Update"
|
||||
upload:
|
||||
en: "Upload"
|
||||
change:
|
||||
en: "Change"
|
||||
continue:
|
||||
en: "Continue"
|
||||
pubkey:
|
||||
en: "Public Key"
|
||||
pubkey_invalid:
|
||||
en: "Public Key is not valid"
|
||||
secret:
|
||||
en: "Secret Key"
|
||||
not_found:
|
||||
en: "Not Found"
|
||||
room_error:
|
||||
en: "Failed to open room. Please try again later."
|
||||
preferences:
|
||||
en: "Preferences"
|
||||
allow:
|
||||
en: "Allow"
|
||||
logout:
|
||||
en: "Logout"
|
||||
copied:
|
||||
en: "Copied"
|
||||
saved:
|
||||
en: "Your Secret Key has been saved"
|
||||
clear:
|
||||
en: "Clear"
|
||||
|
||||
user:
|
||||
dark_mode:
|
||||
en: "Dark Mode"
|
||||
settings:
|
||||
en: "Settings"
|
||||
sign_out:
|
||||
en: "Sign out"
|
||||
|
||||
welcome:
|
||||
title:
|
||||
en: "Welcome to Coop"
|
||||
@@ -41,6 +57,12 @@ onboarding:
|
||||
en: "Already have an account? Log in."
|
||||
|
||||
startup:
|
||||
client_keys_warning:
|
||||
en: "Warning"
|
||||
client_keys_desc:
|
||||
en: "Allow Coop to read the client keys stored in Keychain to continue"
|
||||
create_new_keys:
|
||||
en: "Create New Keys"
|
||||
auto_login_in_progress:
|
||||
en: "Auto login in progress"
|
||||
stuck:
|
||||
@@ -50,11 +72,23 @@ startup:
|
||||
|
||||
new_account:
|
||||
title:
|
||||
en: "Create New Account"
|
||||
password_invalid:
|
||||
en: "Password is invalid"
|
||||
set_password_prompt:
|
||||
en: "Set password to encrypt your key *"
|
||||
en: "Create a new identity"
|
||||
name:
|
||||
en: "What should people call you?"
|
||||
avatar:
|
||||
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:
|
||||
title:
|
||||
@@ -92,27 +126,21 @@ login:
|
||||
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:
|
||||
button_label:
|
||||
en: "Configure the Messaging Relays to receive messages"
|
||||
modal_title:
|
||||
en: "Set Up Messaging Relays"
|
||||
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:
|
||||
en: "Please add some relays."
|
||||
invalid:
|
||||
en: "Relay URL is not valid."
|
||||
empty:
|
||||
en: "You need to add at least 1 relay to receive messages."
|
||||
recommended:
|
||||
en: "Recommended:"
|
||||
|
||||
subject:
|
||||
title:
|
||||
@@ -191,16 +219,14 @@ profile:
|
||||
en: "No bio."
|
||||
|
||||
preferences:
|
||||
modal_relays_title:
|
||||
en: "Edit your Messaging Relays"
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
en: "Requests from user's contacts will automatically go to inbox."
|
||||
en: "Requests from your contacts will automatically go to inbox."
|
||||
hide_avatar_description:
|
||||
en: "Unload all avatar pictures to improve performance and reduce memory usage."
|
||||
proxy_description:
|
||||
@@ -304,7 +330,7 @@ sidebar:
|
||||
trusted_contacts_tooltip:
|
||||
en: "Only show rooms from trusted contacts"
|
||||
retrieving_messages:
|
||||
en: "Retrieving messages..."
|
||||
en: "Retrieving messages"
|
||||
retrieving_messages_description:
|
||||
en: "This may take some time"
|
||||
why_seeing_this_tooltip:
|
||||
|
||||
Reference in New Issue
Block a user