Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3212951ed4 | ||
|
|
ceb69660be | ||
|
|
8f35ae92b7 | ||
|
|
30da985ac1 |
@@ -982,9 +982,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.9.0"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df"
|
||||
checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -1224,7 +1224,7 @@ name = "common"
|
||||
version = "2.0.0-alpha"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs",
|
||||
"dirs 5.0.1",
|
||||
"futures",
|
||||
"gpui-pre",
|
||||
"instant",
|
||||
@@ -1548,9 +1548,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.2"
|
||||
version = "1.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
||||
checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
@@ -1636,9 +1636,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.3"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574"
|
||||
checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0"
|
||||
dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
@@ -1789,7 +1789,16 @@ version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
dependencies = [
|
||||
"dirs-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1800,10 +1809,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"redox_users 0.4.6",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.3",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch"
|
||||
version = "0.2.0"
|
||||
@@ -2945,7 +2966,7 @@ dependencies = [
|
||||
"log",
|
||||
"rustls",
|
||||
"rustls-platform-verifier",
|
||||
"webpki-roots 1.0.10",
|
||||
"webpki-roots 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3659,7 +3680,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots 1.0.10",
|
||||
"webpki-roots 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3712,9 +3733,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
|
||||
checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
@@ -3726,9 +3747,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
|
||||
checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
@@ -3739,9 +3760,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
|
||||
checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
@@ -3753,15 +3774,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
|
||||
checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
|
||||
checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
@@ -3774,15 +3795,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
|
||||
checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.3.1"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
|
||||
checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
@@ -4396,9 +4417,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lyon_algorithms"
|
||||
version = "1.0.21"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdfa8785f95e57914ddb35e3b59994aeba6f5e79e9cfd03da1c269f010f36009"
|
||||
checksum = "f3a6b9d4e8b48acd531fbf1241ad9b270df6e60b6b92bdea1469f4f40899d805"
|
||||
dependencies = [
|
||||
"lyon_path",
|
||||
"num-traits",
|
||||
@@ -4406,9 +4427,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lyon_geom"
|
||||
version = "1.0.19"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92"
|
||||
checksum = "d82dd26aa4dc0b5b70b9115de59824842bcb43cf4a28e1b1e6d0ad148c93f93e"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"euclid",
|
||||
@@ -4427,9 +4448,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lyon_tessellation"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43b8dcf906637ecef61b3c0740c7a4e7f27caeb31257cfac0cc579ce15be6005"
|
||||
checksum = "fd4872650e70dc907dd4f7ebc449d7d657a0d2658e423b2e724970870bbbf75b"
|
||||
dependencies = [
|
||||
"float_next_after",
|
||||
"lyon_path",
|
||||
@@ -4535,9 +4556,9 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
version = "0.9.11"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
|
||||
checksum = "6b0ca1155a9ecc25aa12e8313a119f32edf0255fea2f44839c4ac0a12b2ac157"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -4841,7 +4862,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr"
|
||||
version = "0.45.5"
|
||||
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64 0.22.1",
|
||||
@@ -4867,7 +4888,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-blossom"
|
||||
version = "0.45.1"
|
||||
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bitcoin_hashes 1.2.0",
|
||||
@@ -4880,7 +4901,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-connect"
|
||||
version = "0.45.2"
|
||||
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"nostr",
|
||||
@@ -4893,7 +4914,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-database"
|
||||
version = "0.45.2"
|
||||
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
||||
dependencies = [
|
||||
"nostr",
|
||||
"opaquerr 0.2.0",
|
||||
@@ -4902,7 +4923,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-gossip"
|
||||
version = "0.45.1"
|
||||
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
||||
dependencies = [
|
||||
"nostr",
|
||||
"opaquerr 0.2.0",
|
||||
@@ -4911,7 +4932,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-gossip-memory"
|
||||
version = "0.45.1"
|
||||
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
||||
dependencies = [
|
||||
"indexmap 2.14.2",
|
||||
"lru",
|
||||
@@ -4923,7 +4944,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-lmdb"
|
||||
version = "0.45.3"
|
||||
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"flatbuffers",
|
||||
@@ -4938,7 +4959,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-memory"
|
||||
version = "0.45.2"
|
||||
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
||||
dependencies = [
|
||||
"btreecap",
|
||||
"nostr",
|
||||
@@ -4949,7 +4970,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-sdk"
|
||||
version = "0.45.4"
|
||||
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"async-wsocket",
|
||||
@@ -5539,9 +5560,9 @@ checksum = "4f933a4265d5cdad61d19bbdfc972ea5726d56cd8d3d57b8f2d3c365dd42bee9"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.4.4"
|
||||
version = "5.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc"
|
||||
checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95"
|
||||
dependencies = [
|
||||
"is-wsl",
|
||||
"libc",
|
||||
@@ -6377,6 +6398,16 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0"
|
||||
dependencies = [
|
||||
"libredox",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast"
|
||||
version = "1.0.27"
|
||||
@@ -6468,7 +6499,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots 1.0.10",
|
||||
"webpki-roots 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6753,9 +6784,9 @@ checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.15"
|
||||
version = "0.103.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||
checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
@@ -7199,7 +7230,7 @@ version = "3.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7311,9 +7342,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.3"
|
||||
version = "1.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
|
||||
checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8"
|
||||
|
||||
[[package]]
|
||||
name = "smol"
|
||||
@@ -7334,9 +7365,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smol_str"
|
||||
version = "0.3.6"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523"
|
||||
checksum = "eeb7109d23f319619d4cc0de5464613c8b7b1a09db2774a58101459cf6be2f10"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -7822,9 +7853,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
|
||||
checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
@@ -7838,9 +7869,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.54.0"
|
||||
version = "1.54.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0c168a474b95bfecfe908a65d4bdffc523144b649a74c09c286afb35d53e0bc"
|
||||
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -8207,10 +8238,13 @@ dependencies = [
|
||||
name = "ui"
|
||||
version = "2.0.0-alpha"
|
||||
dependencies = [
|
||||
"common",
|
||||
"gpui-base",
|
||||
"gpui-kit",
|
||||
"gpui-pre",
|
||||
"log",
|
||||
"nostr-sdk",
|
||||
"person",
|
||||
"smallvec",
|
||||
"uuid",
|
||||
"web-time",
|
||||
@@ -8326,9 +8360,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.4.2"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc"
|
||||
checksum = "cde93ff05758a36a851a398a772b0b79a82fbe3c186a46f2696baaeaf84c966b"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"cookie_store",
|
||||
@@ -8341,7 +8375,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"ureq-proto",
|
||||
"utf8-zero",
|
||||
"webpki-roots 1.0.10",
|
||||
"webpki-roots 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8443,9 +8477,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.28.0"
|
||||
version = "1.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
|
||||
checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
@@ -8739,9 +8773,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webbrowser"
|
||||
version = "1.2.4"
|
||||
version = "1.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94"
|
||||
checksum = "d2c8ddfbbf66e501434657400268cefe51b95140a0266236415cda7627732df7"
|
||||
dependencies = [
|
||||
"jni",
|
||||
"log",
|
||||
@@ -8755,9 +8789,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.10"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb"
|
||||
checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -8768,14 +8802,14 @@ version = "0.26.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||
dependencies = [
|
||||
"webpki-roots 1.0.10",
|
||||
"webpki-roots 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.10"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9"
|
||||
checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -9803,9 +9837,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
||||
checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
@@ -9941,7 +9975,7 @@ dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics 0.24.0",
|
||||
"core-text",
|
||||
"dirs",
|
||||
"dirs 5.0.1",
|
||||
"dwrote",
|
||||
"float-ord",
|
||||
"freetype-sys",
|
||||
@@ -10071,9 +10105,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
|
||||
checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
@@ -10082,9 +10116,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.6"
|
||||
version = "0.11.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
|
||||
checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -4,7 +4,7 @@ members = ["crates/*", "desktop", "web"]
|
||||
default-members = ["desktop"]
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0-alpha"
|
||||
version = "2.0.0-alpha2"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M2.75 12C2.75 17.1086 6.89137 21.25 12 21.25C17.1086 21.25 21.25 17.1086 21.25 12C21.25 6.89137 17.1086 2.75 12 2.75C6.89137 2.75 2.75 6.89137 2.75 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.25 13L12 16.25L8.75 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 7.75V15.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 571 B |
@@ -1,3 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M10 5.75L3.75 12L10 18.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M4.5 12H20.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path
|
||||
d="M10 5.75L3.75 12L10 18.25"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M4.5 12H20.25"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 319 B After Width: | Height: | Size: 418 B |
@@ -1,13 +1 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"> <path d="M9.25 9.75C7.317 9.75 5.75 8.183 5.75 6.25C5.75 4.317 7.317 2.75 9.25 2.75C11.183 2.75 12.75 4.317 12.75 6.25C12.75 8.183 11.183 9.75 9.25 9.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M15 2.75C16.933 2.75 18.5 4.317 18.5 6.25C18.5 8.183 16.933 9.75 15 9.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9.24806 12.75C5.46395 12.75 2.27489 15.1517 1.30259 18.2496C0.971822 19.3035 1.89349 20.2187 2.99806 20.2188H15.4981C16.6026 20.2188 17.5243 19.3035 17.1935 18.2496C16.2212 15.1517 13.0322 12.75 9.24806 12.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M20.75 20.25H21.25C22.3546 20.25 23.2748 19.3347 22.9405 18.2819C22.2598 16.1389 20.512 14.31 18.25 13.5088" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 310 B After Width: | Height: | Size: 1014 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M5.75 9.5L12 15.75L18.25 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 209 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9.5 18.25L15.75 12L9.5 5.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 209 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="9.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.75 12.9231L10.5625 15.75L15.25 8.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 341 B |
@@ -1 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M6.25 6.25L17.75 17.75M17.75 6.25L6.25 17.75" stroke="black" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M6.25 6.25L17.75 17.75M17.75 6.25L6.25 17.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 191 B After Width: | Height: | Size: 201 B |
@@ -1,14 +1,3 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="9.25" stroke="currentColor" stroke-width="1.5"/><ellipse cx="12" cy="12" rx="3.5" ry="9.25" stroke="currentColor" stroke-width="1.5"/><path d="M3.5 9.25H20.5" stroke="currentColor" stroke-width="1.5"/><path d="M3.5 14.75H20.5" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 351 B After Width: | Height: | Size: 377 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M2.75 21.25L21.25 21.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M4.75 21.25V4.75C4.75 3.64543 5.64543 2.75 6.75 2.75H17.25C18.3546 2.75 19.25 3.64543 19.25 4.75V21.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.75 12.25H8.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 522 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M21.25 12C21.25 17.1086 17.1086 21.25 12 21.25C6.89137 21.25 2.75 17.1086 2.75 12C2.75 6.89137 6.89137 2.75 12 2.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M19 1.75V8.25M15.75 5H22.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M10.75 9.9C10.75 11.0046 10.0784 11.75 9.25 11.75C8.42157 11.75 7.75 11.0046 7.75 9.9C7.75 8.79543 8.42157 8 9.25 8C10.0784 8 10.75 8.79543 10.75 9.9Z" fill="currentColor"/><path d="M16.25 9.9C16.25 11.0046 15.5784 11.75 14.75 11.75C13.9216 11.75 13.25 11.0046 13.25 9.9C13.25 8.79543 13.9216 8 14.75 8C15.5784 8 16.25 8.79543 16.25 9.9Z" fill="currentColor"/><path d="M16.1123 14.8493C16.1942 14.7105 16.2249 14.545 16.192 14.3857C16.1592 14.2263 16.0665 14.0867 15.933 13.9968C15.7996 13.9069 15.6354 13.8733 15.4754 13.9028C15.3154 13.9321 15.1736 14.0226 15.0757 14.1507C15.0008 14.2469 14.9237 14.3367 14.8415 14.4241C14.1096 15.2083 13.061 15.628 12.0035 15.625C10.946 15.6265 9.8972 15.2055 9.16254 14.4222C9.08002 14.3348 9.00261 14.2451 8.92738 14.1491C8.8291 14.0214 8.68699 13.9313 8.52686 13.9024C8.36679 13.8735 8.20268 13.9076 8.06954 13.9979C7.9364 14.0882 7.84406 14.2281 7.81174 14.3875C7.77938 14.547 7.81054 14.7123 7.89293 14.8509C7.97731 14.99 8.06686 15.1223 8.16553 15.2526C9.04297 16.4311 10.5292 17.1343 12.0024 17.125C13.4754 17.1367 14.965 16.4342 15.8405 15.2521C15.939 15.1215 16.0282 14.9888 16.1123 14.8493Z" fill="currentColor"/><path d="M21.25 12C21.25 17.1086 17.1086 21.25 12 21.25C6.89137 21.25 2.75 17.1086 2.75 12C2.75 6.89137 6.89137 2.75 12 2.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="8.75" r="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="4" cy="9.80005" r="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="20" cy="9.80005" r="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.25 16.625V16.5C7.25 13.8766 9.37665 11.75 12 11.75C14.6234 11.75 16.75 13.8766 16.75 16.5V16.625C16.75 17.5225 16.0225 18.25 15.125 18.25H8.875C7.97754 18.25 7.25 17.5225 7.25 16.625Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M4.25 17.2602H2.75C1.64543 17.2602 0.706551 16.3538 0.919944 15.2701C1.25877 13.5493 2.15049 12.3257 4 11.8301" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M19.75 17.2601H21.25C22.3546 17.2601 23.2935 16.3538 23.08 15.27C22.7412 13.5493 21.8495 12.3257 20 11.8301" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M11.75 6.75H19.25C20.3546 6.75 21.25 7.64543 21.25 8.75V15.25C21.25 16.3546 20.3546 17.25 19.25 17.25H11.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.75 6.75H4.75C3.64543 6.75 2.75 7.64543 2.75 8.75V15.25C2.75 16.3546 3.64543 17.25 4.75 17.25H5.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.75 3.75V20.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 604 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M4.75 10.9853V4.75C4.75 3.64543 5.64543 2.75 6.75 2.75H17.25C18.3546 2.75 19.25 3.64543 19.25 4.75V10.9853M9.75 7.75H14.25M12.617 13.5499L19.9415 11.1744C20.5875 10.9649 21.25 11.4465 21.25 12.1256V18.25C21.25 19.3546 20.3546 20.25 19.25 20.25H4.75C3.64543 20.25 2.75 19.3546 2.75 18.25V12.1256C2.75 11.4465 3.41249 10.9649 4.0585 11.1744L11.383 13.5499C11.784 13.68 12.216 13.68 12.617 13.5499Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 576 B |
@@ -1,14 +1,3 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9.75027 5.52371L10.7168 4.55722C13.1264 2.14759 17.0332 2.14759 19.4428 4.55722C21.8524 6.96684 21.8524 10.8736 19.4428 13.2832L18.4742 14.2519M5.52886 9.74513L4.55722 10.7168C2.14759 13.1264 2.1476 17.0332 4.55722 19.4428C6.96684 21.8524 10.8736 21.8524 13.2832 19.4428L14.2478 18.4782M9.5 14.5L14.5 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 359 B After Width: | Height: | Size: 462 B |
@@ -1,14 +1,3 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 8.75003C11.1716 8.75003 10.5 9.4216 10.5 10.25C10.5 11.0785 11.1716 11.75 12 11.75C12.8284 11.75 13.5 11.0785 13.5 10.25C13.5 9.4216 12.8284 8.75003 12 8.75003ZM12 8.75003V14.75M20.25 11.9124V6.94155C20.25 6.08069 19.6991 5.31641 18.8825 5.04418L12.6325 2.96085C12.2219 2.824 11.7781 2.824 11.3675 2.96085L5.11754 5.04418C4.30086 5.31641 3.75 6.08069 3.75 6.94155V11.9124C3.75 16.8848 8 19.25 12 21.4079C16 19.25 20.25 16.8848 20.25 11.9124Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 309 B After Width: | Height: | Size: 626 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M21.75 12C21.75 6.84375 17.9583 3.75 12 3.75C6.04167 3.75 2.25 6.84375 2.25 12C2.25 13.3368 3.17054 15.6055 3.3145 15.9522C3.32742 15.9833 3.34021 16.0117 3.3518 16.0433C3.45089 16.3136 3.85722 17.7527 2.25 19.8828C4.41667 20.914 6.71766 19.2188 6.71766 19.2188C8.30963 20.0597 10.2038 20.25 12 20.25C17.9583 20.25 21.75 17.1562 21.75 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 520 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M2.66936 5.12886C2.12122 3.64104 3.6759 2.24953 5.09409 2.95862L20.0468 10.435C21.3366 11.0799 21.3366 12.9205 20.0468 13.5655L5.09409 21.0418C3.67589 21.7509 2.12122 20.3594 2.66936 18.8715L4.92467 12.75H9.25021C9.66442 12.75 10.0002 12.4142 10.0002 12C10.0002 11.5858 9.66442 11.25 9.25021 11.25H4.92452L2.66936 5.12886Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 435 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"> <path d="M21.75 12C21.75 6.84375 17.9583 3.75 12 3.75C6.04167 3.75 2.25 6.84375 2.25 12C2.25 13.3368 3.17054 15.6055 3.3145 15.9522C3.32742 15.9833 3.34021 16.0117 3.3518 16.0433C3.45089 16.3136 3.85722 17.7527 2.25 19.8828C4.41667 20.914 6.71766 19.2188 6.71766 19.2188C8.30963 20.0597 10.2038 20.25 12 20.25C17.9583 20.25 21.75 17.1562 21.75 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/><path d="M12 8.75V15.25M8.75 12H15.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> </svg>
|
||||
|
After Width: | Height: | Size: 624 B |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 5.25V12M12 12V18.75M12 12H5.25M12 12H18.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M12 6.75V12M12 12V17.25M12 12H6.75M12 12H17.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 203 B After Width: | Height: | Size: 203 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M17.75 2.75H6.25C5.14543 2.75 4.25 3.64543 4.25 4.75V19.25C4.25 20.3546 5.14543 21.25 6.25 21.25H17.75C18.8546 21.25 19.75 20.3546 19.75 19.25V4.75C19.75 3.64543 18.8546 2.75 17.75 2.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12.25" r="2.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M16 21C16 18.7909 14.2091 17 12 17C9.79086 17 8 18.7909 8 21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M9.75 6.25H14.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 696 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="9.25" r="1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.75 21.25L11.75 9.25H12.25L16.25 21.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9.5 17.75H14.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.75693 12.7501C6.08102 10.7234 6.08103 7.77679 7.75693 5.75M16.2431 5.75C17.919 7.77679 17.919 10.7234 16.2431 12.7501" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.06494 2.7574C1.64285 6.40823 1.64502 12.1018 5.07145 15.75M18.9281 2.75C22.3572 6.40053 22.3573 12.0993 18.9285 15.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 899 B |
@@ -1 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M20.25 20.25L16.1265 16.1265M16.1265 16.1265C17.4385 14.8145 18.25 13.002 18.25 11C18.25 6.99594 15.0041 3.75 11 3.75C6.99594 3.75 3.75 6.99594 3.75 11C3.75 15.0041 6.99594 18.25 11 18.25C13.002 18.25 14.8145 17.4385 16.1265 16.1265Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 20L16.1265 16.1265M16.1265 16.1265C17.4385 14.8145 18.25 13.002 18.25 11C18.25 6.99594 15.0041 3.75 11 3.75C6.99594 3.75 3.75 6.99594 3.75 11C3.75 15.0041 6.99594 18.25 11 18.25C13.002 18.25 14.8145 17.4385 16.1265 16.1265Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 404 B After Width: | Height: | Size: 408 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M13 19.25H5.95C4.82989 19.25 4.26984 19.25 3.84202 19.032C3.46569 18.8403 3.15973 18.5343 2.96799 18.158C2.75 17.7302 2.75 17.1701 2.75 16.05V7.95C2.75 6.82989 2.75 6.26984 2.96799 5.84202C3.15973 5.46569 3.46569 5.15973 3.84202 4.96799C4.26984 4.75 4.8299 4.75 5.95 4.75H18.05C19.1701 4.75 19.7302 4.75 20.158 4.96799C20.5343 5.15973 20.8403 5.46569 21.032 5.84202C21.25 6.26984 21.25 6.8299 21.25 7.95V11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M3 5.63635L10.9761 10.3898C11.6069 10.7657 12.3931 10.7657 13.0239 10.3898L21 5.63635" stroke="currentColor" stroke-width="1.5"/><path d="M21.8148 15.375L21.1669 15.7491M16.1856 18.625L16.8335 18.2509M21.8147 18.625L21.1669 18.251M16.1855 15.375L16.8335 15.7491M19.0002 20.25L19.0002 19.4375M19.0002 13.75V14.5625M21.1669 17C21.1669 16.6053 21.0613 16.2352 20.8769 15.9165C20.5022 15.269 19.8021 14.8333 19.0002 14.8333C18.1983 14.8333 17.4981 15.269 17.1235 15.9165C16.9391 16.2352 16.8335 16.6053 16.8335 17C16.8335 17.3947 16.9391 17.7648 17.1235 18.0835C17.4982 18.731 18.1983 19.1667 19.0002 19.1667C19.8021 19.1667 20.5022 18.731 20.8769 18.0835C21.0613 17.7648 21.1669 17.3947 21.1669 17Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -1,15 +1 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 3v12" />
|
||||
<path d="m17 8-5-5-5 5" />
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"> <path d="M11.25 20.25H5.75C4.64543 20.25 3.75 19.3546 3.75 18.25V5.75C3.75 4.64543 4.64543 3.75 5.75 3.75H18.25C19.3546 3.75 20.25 4.64543 20.25 5.75V11.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M3.75 16.25L6.58579 13.4142C7.36683 12.6332 8.63317 12.6332 9.41421 13.4142L12 16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M14.5 11.5C15.6046 11.5 16.5 10.6046 16.5 9.5C16.5 8.39543 15.6046 7.5 14.5 7.5C13.3954 7.5 12.5 8.39543 12.5 9.5C12.5 10.6046 13.3954 11.5 14.5 11.5Z" stroke="currentColor" stroke-width="1.5"/><path d="M15.75 18.5H21.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M18.5 15.75V21.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> </svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 849 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="9.25" stroke="currentColor" stroke-width="1.5"/><path d="M11.3121 12.3511L11.0582 7.9983C11.0266 7.45662 11.4574 7 12 7C12.5426 7 12.9734 7.45662 12.9418 7.9983L12.6879 12.3511C12.6666 12.7154 12.365 13 12 13C11.635 13 11.3334 12.7154 11.3121 12.3511Z" fill="currentColor"/><circle cx="11.9999" cy="15.8998" r="1.1" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 445 B |
@@ -1,16 +1,13 @@
|
||||
use std::hash::Hash;
|
||||
use std::ops::Range;
|
||||
|
||||
use common::{EventExt, NostrParser, emojis_from_tags, extract_and_remove_media_urls};
|
||||
use common::{EventExt, NostrParser, emojis_from_tags, extract_and_remove_media_urls, nevent_refs};
|
||||
use gpui::{SharedString, SharedUri};
|
||||
use nostr_sdk::prelude::*;
|
||||
use state::FileAttachment;
|
||||
|
||||
pub(crate) const KIND_FILE_MESSAGE: Kind = Kind::Custom(15);
|
||||
|
||||
const SCHEME_WITH_COLON: &str = "nostr:";
|
||||
const NEVENT_PREFIX: &str = "nevent1";
|
||||
|
||||
/// Rendered message.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Message {
|
||||
@@ -248,61 +245,6 @@ fn extract_mentions(content: &str) -> Vec<Mention> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A `nevent` reference found in a content string.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct NeventRef {
|
||||
/// Range of the reference, including the optional `nostr:` scheme
|
||||
pub range: Range<usize>,
|
||||
/// Compact display label, like `https://njump.me/nevent1...d9sp`
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
/// Extracts valid `nevent1...` and `nostr:nevent1...` references from content.
|
||||
pub fn nevent_refs(content: &str) -> Vec<NeventRef> {
|
||||
let mut references = Vec::new();
|
||||
|
||||
for (match_start, _) in content.match_indices(NEVENT_PREFIX) {
|
||||
// Only lowercase references are matched, matching NIP-19 encodings in practice
|
||||
let start = if content[..match_start].ends_with(SCHEME_WITH_COLON) {
|
||||
match_start - SCHEME_WITH_COLON.len()
|
||||
} else {
|
||||
match_start
|
||||
};
|
||||
|
||||
// Skip matches glued to word characters, like the `nevent1` in `xnevent1...`
|
||||
if content[..start].ends_with(char::is_alphanumeric) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let end = match_start
|
||||
+ content[match_start..]
|
||||
.bytes()
|
||||
.take_while(u8::is_ascii_alphanumeric)
|
||||
.count();
|
||||
|
||||
// The bech32 token excludes the optional `nostr:` scheme
|
||||
let bech32 = &content[match_start..end];
|
||||
if Nip19Event::from_bech32(bech32).is_err() {
|
||||
continue;
|
||||
}
|
||||
|
||||
references.push(NeventRef {
|
||||
range: start..end,
|
||||
label: nevent_label(bech32),
|
||||
});
|
||||
}
|
||||
|
||||
references
|
||||
}
|
||||
|
||||
/// Compact display of a nevent link, like `https://njump.me/nevent1...d9sp`.
|
||||
fn nevent_label(bech32: &str) -> String {
|
||||
match bech32.len() {
|
||||
0..=4 => format!("https://njump.me/{bech32}"),
|
||||
_ => format!("https://njump.me/nevent1...{}", &bech32[bech32.len() - 4..]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts all reply (ids) from the event tags.
|
||||
fn extract_reply_ids(inner: &Tags) -> Vec<EventId> {
|
||||
let mut replies_to = vec![];
|
||||
|
||||
@@ -15,7 +15,6 @@ use gpui::{
|
||||
SystemNotificationAction, Task, WeakEntity, Window, div, img, list, px, red, relative,
|
||||
retain_all, svg, white,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::dock::{Panel, PanelEvent};
|
||||
@@ -34,16 +33,13 @@ use smallvec::{SmallVec, smallvec};
|
||||
use state::{
|
||||
FileAttachment, NostrRegistry, download_and_decrypt_to_file, upload, upload_encrypted,
|
||||
};
|
||||
use ui::{
|
||||
Avatar, COMPACT_REACTION_EMOJIS, IconName, MessageActions, MessageMedia, MessageRow,
|
||||
REACTION_EMOJIS, ReactionBar, RenderedText, ReplyPreview, WelcomeMessage, group_reactions,
|
||||
opens_run, rendered_text,
|
||||
};
|
||||
|
||||
use crate::file::*;
|
||||
use crate::message::{
|
||||
Avatar, MessageMedia, MessageRow, ReactionBar, ReplyPreview, WelcomeMessage, group_reactions,
|
||||
opens_run,
|
||||
};
|
||||
use crate::text::RenderedText;
|
||||
|
||||
const REACTION_EMOJIS: &[&str] = &["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"];
|
||||
const COMPACT_REACTION_EMOJIS: &[&str] = &["👍", "❤️", "👀"];
|
||||
|
||||
/// Notice shown when a conversation has no messages, and above the message list.
|
||||
const PRIVATE_NOTICE: &str =
|
||||
@@ -60,8 +56,6 @@ static CUSTOM_EMOJI_RE: LazyLock<Regex> =
|
||||
|
||||
mod actions;
|
||||
mod file;
|
||||
pub mod message;
|
||||
pub mod text;
|
||||
|
||||
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
|
||||
cx.new(|cx| ChatPanel::new(room, window, cx))
|
||||
@@ -1050,7 +1044,7 @@ impl ChatPanel {
|
||||
.rounded_full()
|
||||
.bg(cx.theme().warning.opacity(0.15))
|
||||
.text_color(cx.theme().warning)
|
||||
.child(Icon::new(IconName::TriangleAlert).small()),
|
||||
.child(Icon::new(IconName::Warning).small()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -1108,9 +1102,12 @@ impl ChatPanel {
|
||||
.rendered_texts_by_id
|
||||
.entry(message.id)
|
||||
.or_insert_with(|| {
|
||||
text::rendered_text(
|
||||
rendered_text(
|
||||
&message.content,
|
||||
&message.mentions,
|
||||
message
|
||||
.mentions
|
||||
.iter()
|
||||
.map(|mention| (mention.range.clone(), mention.public_key)),
|
||||
&message.emojis,
|
||||
&directory,
|
||||
cx,
|
||||
@@ -1155,6 +1152,7 @@ impl ChatPanel {
|
||||
.avatar(
|
||||
Avatar::new(author.avatar())
|
||||
.seed(author.avatar_seed())
|
||||
.small()
|
||||
.flex_shrink_0()
|
||||
.relative()
|
||||
.dropdown_menu(move |this, _window, _cx| {
|
||||
@@ -1391,83 +1389,34 @@ impl ChatPanel {
|
||||
public_key: &PublicKey,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
h_flex()
|
||||
.p_0p5()
|
||||
.gap_1()
|
||||
.invisible()
|
||||
.absolute()
|
||||
.right_4()
|
||||
.top_neg_2()
|
||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().background)
|
||||
.children({
|
||||
let mut items = vec![];
|
||||
let mut actions = MessageActions::new();
|
||||
|
||||
for emoji in COMPACT_REACTION_EMOJIS {
|
||||
items.push(
|
||||
Button::new(*emoji)
|
||||
.label(*emoji)
|
||||
.tooltip(*emoji)
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click({
|
||||
let emoji = *emoji;
|
||||
let id = *id;
|
||||
cx.listener(move |this, _event, window, cx| {
|
||||
this.send_reaction(emoji, &id, window, cx);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
for emoji in COMPACT_REACTION_EMOJIS {
|
||||
let id = *id;
|
||||
actions = actions.reaction(
|
||||
emoji,
|
||||
cx.listener(move |this, _event, window, cx| {
|
||||
this.send_reaction(emoji, &id, window, cx);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
items
|
||||
let reply_id = *id;
|
||||
let copy_id = *id;
|
||||
let dropdown_id = *id;
|
||||
let public_key = *public_key;
|
||||
|
||||
actions
|
||||
.reply(cx.listener(move |this, _event, _window, cx| {
|
||||
this.reply_to(&reply_id, cx);
|
||||
}))
|
||||
.copy(cx.listener(move |this, _event, _window, cx| {
|
||||
this.copy_message(©_id, cx);
|
||||
}))
|
||||
.dropdown(move |menu, _window, _cx| {
|
||||
menu.menu("Copy author", Box::new(Command::Copy(public_key)))
|
||||
.menu("Seen on", Box::new(Command::Trace(dropdown_id)))
|
||||
})
|
||||
.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
|
||||
.child(
|
||||
Button::new("reply")
|
||||
.icon(IconName::Reply)
|
||||
.tooltip("Reply")
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click({
|
||||
let id = id.to_owned();
|
||||
cx.listener(move |this, _event, _window, cx| {
|
||||
this.reply_to(&id, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("copy")
|
||||
.icon(IconName::Copy)
|
||||
.tooltip("Copy")
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click({
|
||||
let id = id.to_owned();
|
||||
cx.listener(move |this, _event, _window, cx| {
|
||||
this.copy_message(&id, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
|
||||
.child(
|
||||
Button::new("advance")
|
||||
.icon(IconName::Ellipsis)
|
||||
.small()
|
||||
.ghost()
|
||||
.dropdown_menu({
|
||||
let public_key = *public_key;
|
||||
let id = *id;
|
||||
move |this, _window, _cx| {
|
||||
this.menu("Copy author", Box::new(Command::Copy(public_key)))
|
||||
.menu("Seen on", Box::new(Command::Trace(id)))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.group_hover("", |this| this.visible())
|
||||
}
|
||||
|
||||
fn render_attachment(&self, url: &Url, cx: &Context<Self>) -> impl IntoElement {
|
||||
@@ -1493,7 +1442,7 @@ impl ChatPanel {
|
||||
.justify_center()
|
||||
.rounded_full()
|
||||
.bg(red())
|
||||
.child(Icon::new(IconName::X).size_2().text_color(white())),
|
||||
.child(Icon::new(IconName::Close).size_2().text_color(white())),
|
||||
)
|
||||
.on_click({
|
||||
let url = url.clone();
|
||||
@@ -1694,7 +1643,7 @@ impl ChatPanel {
|
||||
)
|
||||
.child(
|
||||
Button::new("remove-reply")
|
||||
.icon(IconName::X)
|
||||
.icon(IconName::Close)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.on_click({
|
||||
@@ -1745,7 +1694,6 @@ impl ChatPanel {
|
||||
.icon(IconName::Settings2)
|
||||
.tooltip("Configuration")
|
||||
.ghost()
|
||||
.large()
|
||||
.dropdown_menu(move |this, _window, _cx| {
|
||||
let auto = matches!(signer_kind, SignerKind::Auto);
|
||||
let encryption = matches!(signer_kind, SignerKind::Encryption);
|
||||
@@ -1791,9 +1739,8 @@ impl ChatPanel {
|
||||
.unwrap_or_default();
|
||||
|
||||
Button::new("emoji")
|
||||
.icon(IconName::Sticker)
|
||||
.icon(IconName::Emoji)
|
||||
.ghost()
|
||||
.large()
|
||||
.dropdown_menu_with_anchor(gpui::Anchor::BottomLeft, move |this, _window, _cx| {
|
||||
let menu = REACTION_EMOJIS.iter().fold(this, |this, emoji| {
|
||||
this.menu(*emoji, Box::new(Command::Insert(emoji.to_string())))
|
||||
@@ -1839,7 +1786,7 @@ impl Panel for ChatPanel {
|
||||
|
||||
Some(vec![
|
||||
Button::new("subject")
|
||||
.icon(IconName::TextCursorInput)
|
||||
.icon(IconName::Input)
|
||||
.tooltip("Change subject")
|
||||
.small()
|
||||
.ghost()
|
||||
@@ -1881,10 +1828,10 @@ impl Render for ChatPanel {
|
||||
.gap_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(Input::new(&self.subject_input).small())
|
||||
.child(Input::new(&self.subject_input))
|
||||
.child(
|
||||
Button::new("change")
|
||||
.icon(IconName::CircleCheck)
|
||||
.icon(IconName::CheckCircle)
|
||||
.label("Change")
|
||||
.secondary()
|
||||
.disabled(self.uploading)
|
||||
@@ -1949,7 +1896,6 @@ impl Render for ChatPanel {
|
||||
.loading(self.uploading)
|
||||
.disabled(self.uploading)
|
||||
.ghost()
|
||||
.large()
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
this.upload(window, cx);
|
||||
})),
|
||||
@@ -1963,10 +1909,9 @@ impl Render for ChatPanel {
|
||||
.child(self.render_config_menu(window, cx))
|
||||
.child(
|
||||
Button::new("send")
|
||||
.icon(IconName::SendHorizontal)
|
||||
.icon(IconName::PaperPlaneFill)
|
||||
.disabled(self.uploading)
|
||||
.ghost()
|
||||
.large()
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
this.send_text_message(window, cx);
|
||||
})),
|
||||
|
||||
@@ -4,6 +4,62 @@ use nostr::prelude::*;
|
||||
|
||||
const BECH32_SEPARATOR: u8 = b'1';
|
||||
const SCHEME_WITH_COLON: &str = "nostr:";
|
||||
const NEVENT_PREFIX: &str = "nevent1";
|
||||
|
||||
/// A `nevent` reference found in a content string.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct NeventRef {
|
||||
/// Range of the reference, including the optional `nostr:` scheme
|
||||
pub range: Range<usize>,
|
||||
/// Compact display label, like `https://njump.me/nevent1...d9sp`
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
/// Extracts valid `nevent1...` and `nostr:nevent1...` references from content.
|
||||
pub fn nevent_refs(content: &str) -> Vec<NeventRef> {
|
||||
let mut references = Vec::new();
|
||||
|
||||
for (match_start, _) in content.match_indices(NEVENT_PREFIX) {
|
||||
// Only lowercase references are matched, matching NIP-19 encodings in practice
|
||||
let start = if content[..match_start].ends_with(SCHEME_WITH_COLON) {
|
||||
match_start - SCHEME_WITH_COLON.len()
|
||||
} else {
|
||||
match_start
|
||||
};
|
||||
|
||||
// Skip matches glued to word characters, like the `nevent1` in `xnevent1...`
|
||||
if content[..start].ends_with(char::is_alphanumeric) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let end = match_start
|
||||
+ content[match_start..]
|
||||
.bytes()
|
||||
.take_while(u8::is_ascii_alphanumeric)
|
||||
.count();
|
||||
|
||||
// The bech32 token excludes the optional `nostr:` scheme
|
||||
let bech32 = &content[match_start..end];
|
||||
if Nip19Event::from_bech32(bech32).is_err() {
|
||||
continue;
|
||||
}
|
||||
|
||||
references.push(NeventRef {
|
||||
range: start..end,
|
||||
label: nevent_label(bech32),
|
||||
});
|
||||
}
|
||||
|
||||
references
|
||||
}
|
||||
|
||||
/// Compact display of a nevent link, like `https://njump.me/nevent1...d9sp`.
|
||||
fn nevent_label(bech32: &str) -> String {
|
||||
match bech32.len() {
|
||||
0..=4 => format!("https://njump.me/{bech32}"),
|
||||
_ => format!("https://njump.me/nevent1...{}", &bech32[bech32.len() - 4..]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Nostr parsed token with its range in the original text
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use chat_ui::message::WelcomeMessage;
|
||||
use chat_ui::text::{RenderedText, rendered_text};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
@@ -26,7 +24,7 @@ use serde::Deserialize;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{NostrRegistry, upload};
|
||||
use ui::Avatar;
|
||||
use ui::{Avatar, RenderedText, WelcomeMessage, rendered_text};
|
||||
use util::{attachment_name, display_name, opens_run, pin_label, report};
|
||||
|
||||
mod details;
|
||||
@@ -987,7 +985,10 @@ impl GroupPanel {
|
||||
.or_insert_with(|| {
|
||||
rendered_text(
|
||||
&message.content,
|
||||
&message.mentions,
|
||||
message
|
||||
.mentions
|
||||
.iter()
|
||||
.map(|mention| (mention.range.clone(), mention.public_key)),
|
||||
&message.emojis,
|
||||
&directory,
|
||||
cx,
|
||||
@@ -1065,7 +1066,6 @@ impl GroupPanel {
|
||||
.loading(self.uploading)
|
||||
.disabled(self.uploading)
|
||||
.ghost()
|
||||
.large()
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.upload(window, cx);
|
||||
})),
|
||||
@@ -1073,10 +1073,9 @@ impl GroupPanel {
|
||||
.child(Textarea::new(&self.input).appearance(false).flex_1())
|
||||
.child(
|
||||
Button::new("send")
|
||||
.icon(IconName::SendHorizontal)
|
||||
.icon(ui::IconName::PaperPlaneFill)
|
||||
.tooltip("Send")
|
||||
.ghost()
|
||||
.large()
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.send(window, cx);
|
||||
})),
|
||||
@@ -1141,7 +1140,7 @@ impl GroupPanel {
|
||||
)
|
||||
.child(
|
||||
Button::new("cancel-reply")
|
||||
.icon(IconName::X)
|
||||
.icon(ui::IconName::Close)
|
||||
.tooltip("Cancel reply")
|
||||
.ghost()
|
||||
.small()
|
||||
@@ -1178,7 +1177,7 @@ impl GroupPanel {
|
||||
)
|
||||
.child(
|
||||
Button::new(SharedString::from(format!("remove-attachment-{ix}")))
|
||||
.icon(IconName::X)
|
||||
.icon(ui::IconName::Close)
|
||||
.tooltip("Remove")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use chat::Message;
|
||||
use chat_ui::message::{MessageMedia, MessageRow, ReactionBar, ReplyPreview, group_reactions};
|
||||
use common::TimestampExt;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
@@ -8,19 +7,20 @@ use gpui::{
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenu, PopupMenuItem};
|
||||
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex};
|
||||
use nip29::{Activity, Group, Reaction};
|
||||
use nostr_sdk::prelude::{EventId, Kind};
|
||||
use person::Directory;
|
||||
use settings::AppSettings;
|
||||
use state::NostrRegistry;
|
||||
use ui::Avatar;
|
||||
use ui::{
|
||||
Avatar, MessageMedia, MessageRow, REACTION_EMOJIS, ReactionBar, ReplyPreview, group_reactions,
|
||||
react_menu,
|
||||
};
|
||||
|
||||
use crate::GroupPanel;
|
||||
|
||||
const REACTIONS: [&str; 6] = ["👍", "❤️", "😂", "🎉", "😮", "😢"];
|
||||
|
||||
pub(crate) struct Actions {
|
||||
pub group: WeakEntity<Group>,
|
||||
pub panel: WeakEntity<GroupPanel>,
|
||||
@@ -61,6 +61,7 @@ pub(crate) fn render(
|
||||
.avatar(
|
||||
Avatar::new(author.avatar())
|
||||
.seed(author.avatar_seed())
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
.author(author.name())
|
||||
@@ -88,6 +89,22 @@ pub(crate) fn render(
|
||||
let group = actions.group;
|
||||
let panel = actions.panel;
|
||||
|
||||
// Reading the panel here would re-enter its ongoing update, so resolve
|
||||
// the current user's custom emojis through the shared person directory.
|
||||
let custom = nostr
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.map(|current_user| {
|
||||
persons
|
||||
.read(cx)
|
||||
.person(¤t_user, cx)
|
||||
.emojis()
|
||||
.iter()
|
||||
.map(|(shortcode, _)| format!(":{shortcode}:"))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let row = div()
|
||||
.id(SharedString::from(format!("message-menu-{}", id.to_hex())))
|
||||
.w_full()
|
||||
@@ -106,8 +123,20 @@ pub(crate) fn render(
|
||||
.submenu("React", window, cx, {
|
||||
let group = group.clone();
|
||||
let panel = panel.clone();
|
||||
let custom = custom.clone();
|
||||
|
||||
move |menu, _window, cx| react_menu(menu, group.clone(), panel.clone(), id, cx)
|
||||
move |menu, _window, _cx| {
|
||||
let group = group.clone();
|
||||
let panel = panel.clone();
|
||||
|
||||
react_menu(menu, REACTION_EMOJIS, &custom, move |emoji, _window, cx| {
|
||||
if let Err(error) = panel.update_in(cx, |panel, window, cx| {
|
||||
panel.react(group.clone(), id, emoji.to_owned(), window, cx);
|
||||
}) {
|
||||
log::warn!("nip29: sending a reaction failed: {error}");
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if !admin {
|
||||
@@ -142,67 +171,6 @@ pub(crate) fn render(
|
||||
row.into_any_element()
|
||||
}
|
||||
|
||||
fn react_menu(
|
||||
menu: PopupMenu,
|
||||
group: WeakEntity<Group>,
|
||||
panel: WeakEntity<GroupPanel>,
|
||||
id: EventId,
|
||||
cx: &App,
|
||||
) -> PopupMenu {
|
||||
let menu = REACTIONS.iter().fold(menu, |menu, emoji| {
|
||||
let emoji = *emoji;
|
||||
let group = group.clone();
|
||||
let panel = panel.clone();
|
||||
|
||||
menu.item(
|
||||
PopupMenuItem::new(emoji).on_click(move |_event, _window, cx| {
|
||||
if let Err(error) = panel.update_in(cx, |panel, window, cx| {
|
||||
panel.react(group.clone(), id, emoji.to_owned(), window, cx);
|
||||
}) {
|
||||
log::warn!("nip29: sending a reaction failed: {error}");
|
||||
}
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
let emojis = panel
|
||||
.upgrade()
|
||||
.and_then(|panel| {
|
||||
let current_user = NostrRegistry::global(cx).read(cx).current_user()?;
|
||||
Some(
|
||||
panel
|
||||
.read(cx)
|
||||
.directory
|
||||
.read(cx)
|
||||
.person(¤t_user, cx)
|
||||
.emojis()
|
||||
.to_vec(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if emojis.is_empty() {
|
||||
return menu;
|
||||
}
|
||||
|
||||
let menu = menu.separator();
|
||||
emojis.into_iter().fold(menu, |menu, (shortcode, _)| {
|
||||
let emoji = format!(":{shortcode}:");
|
||||
let group = group.clone();
|
||||
let panel = panel.clone();
|
||||
|
||||
menu.item(
|
||||
PopupMenuItem::new(emoji.clone()).on_click(move |_event, _window, cx| {
|
||||
if let Err(error) = panel.update_in(cx, |panel, window, cx| {
|
||||
panel.react(group.clone(), id, emoji.clone(), window, cx);
|
||||
}) {
|
||||
log::warn!("nip29: sending a reaction failed: {error}");
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn render_activity(
|
||||
ix: usize,
|
||||
activity: &Activity,
|
||||
@@ -218,7 +186,7 @@ pub(crate) fn render_activity(
|
||||
.gap_2()
|
||||
.py_1()
|
||||
.px_3()
|
||||
.hover(|this| this.bg(cx.theme().primary))
|
||||
.hover(|this| this.bg(cx.theme().list_hover))
|
||||
.child(badge(activity, cx))
|
||||
.when(!hide_avatar, |this| {
|
||||
this.child(
|
||||
|
||||
@@ -8,13 +8,13 @@ use gpui::{
|
||||
};
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::dialog::DialogButtonProps;
|
||||
use gpui_kit::component::form::field as form_field;
|
||||
use gpui_kit::component::input::InputState;
|
||||
use gpui_kit::component::notification::Notification;
|
||||
use gpui_kit::component::switch::Switch;
|
||||
use gpui_kit::component::{ActiveTheme, Sizable, WindowExt, h_flex, v_flex};
|
||||
use nip29::{Group, GroupId, GroupMetadata};
|
||||
use nostr_sdk::prelude::RelayUrl;
|
||||
use ui::settings_row;
|
||||
|
||||
use crate::util::field;
|
||||
|
||||
@@ -126,73 +126,61 @@ pub(crate) fn open(group: WeakEntity<Group>, window: &mut Window, cx: &mut App)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
form_field()
|
||||
.label("Private")
|
||||
.description("Only members can read group messages")
|
||||
.child(
|
||||
Switch::new("private")
|
||||
.checked(current.private)
|
||||
.on_click(toggle(&flags, |flags| &mut flags.private)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
form_field()
|
||||
.label("Restricted")
|
||||
.description("Only members can post messages")
|
||||
.child(
|
||||
Switch::new("restricted")
|
||||
.checked(current.restricted)
|
||||
.on_click(toggle(&flags, |flags| {
|
||||
&mut flags.restricted
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
form_field()
|
||||
.label("Hidden")
|
||||
.description("Hidden from non-members, not discoverable")
|
||||
.child(
|
||||
Switch::new("hidden")
|
||||
.checked(current.hidden)
|
||||
.on_click(toggle(&flags, |flags| &mut flags.hidden)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
form_field()
|
||||
.label("Closed")
|
||||
.description("Join requests are ignored (invite-only)")
|
||||
.child(
|
||||
Switch::new("closed")
|
||||
.checked(current.closed)
|
||||
.on_click(toggle(&flags, |flags| &mut flags.closed)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
form_field()
|
||||
.label("LiveKit")
|
||||
.description(if supported {
|
||||
"Announce audio and video support."
|
||||
} else {
|
||||
"The relay doesn't announce LiveKit support."
|
||||
})
|
||||
.child(
|
||||
Switch::new("livekit").checked(current.livekit).on_click({
|
||||
let flags = flags.clone();
|
||||
let livekit = livekit.clone();
|
||||
.child(settings_row(
|
||||
"Private",
|
||||
"Only members can read group messages",
|
||||
Switch::new("private")
|
||||
.checked(current.private)
|
||||
.on_click(toggle(&flags, |flags| &mut flags.private)),
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Restricted",
|
||||
"Only members can post messages",
|
||||
Switch::new("restricted")
|
||||
.checked(current.restricted)
|
||||
.on_click(toggle(&flags, |flags| &mut flags.restricted)),
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Hidden",
|
||||
"Hidden from non-members, not discoverable",
|
||||
Switch::new("hidden")
|
||||
.checked(current.hidden)
|
||||
.on_click(toggle(&flags, |flags| &mut flags.hidden)),
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Closed",
|
||||
"Join requests are ignored (invite-only)",
|
||||
Switch::new("closed")
|
||||
.checked(current.closed)
|
||||
.on_click(toggle(&flags, |flags| &mut flags.closed)),
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"LiveKit",
|
||||
if supported {
|
||||
"Announce audio and video support."
|
||||
} else {
|
||||
"The relay doesn't announce LiveKit support."
|
||||
},
|
||||
Switch::new("livekit").checked(current.livekit).on_click({
|
||||
let flags = flags.clone();
|
||||
let livekit = livekit.clone();
|
||||
|
||||
move |next, _window, cx| {
|
||||
if *next && *livekit.read(cx) == Some(false) {
|
||||
return;
|
||||
}
|
||||
flags.update(cx, |flags, cx| {
|
||||
flags.livekit = *next;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
move |next, _window, cx| {
|
||||
if *next && *livekit.read(cx) == Some(false) {
|
||||
return;
|
||||
}
|
||||
flags.update(cx, |flags, cx| {
|
||||
flags.livekit = *next;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
cx,
|
||||
)),
|
||||
),
|
||||
)
|
||||
.on_ok({
|
||||
|
||||
@@ -35,7 +35,7 @@ pub(crate) fn opens_run(rows: &[Row], index: usize) -> bool {
|
||||
};
|
||||
|
||||
match (current, previous) {
|
||||
(Row::Message(current), Row::Message(previous)) => chat_ui::message::opens_run(
|
||||
(Row::Message(current), Row::Message(previous)) => ui::opens_run(
|
||||
Some((previous.author, previous.created_at.as_secs())),
|
||||
(current.author, current.created_at.as_secs()),
|
||||
),
|
||||
|
||||
@@ -4,7 +4,7 @@ use gpui::{Context, EventEmitter, Task};
|
||||
use instant::Duration;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
use state::{BOOTSTRAP_RELAYS, NostrRegistry};
|
||||
|
||||
/// Disable scoring when the follow set exceeds this size
|
||||
const MAX_FOLLOWS: usize = 2000;
|
||||
@@ -14,6 +14,10 @@ const MAX_TAGS_PER_LIST: usize = 5000;
|
||||
const SYNC_INTERVAL: u64 = 15 * 60;
|
||||
/// Longest wait between retries while the graph has no data yet
|
||||
const MAX_RETRY_WAIT: u64 = 60;
|
||||
/// Time for a relay to prove negentropy support before the reconcile gives up
|
||||
const SYNC_INITIAL_TIMEOUT: u64 = 30;
|
||||
/// Longest silence from a relay before the reconcile gives up
|
||||
const SYNC_IDLE_TIMEOUT: u64 = 60;
|
||||
|
||||
/// The web of trust graph changed
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -77,6 +81,36 @@ impl Snapshot {
|
||||
|
||||
snapshot
|
||||
}
|
||||
|
||||
/// Build the snapshot from the contact lists the local database holds
|
||||
async fn load(client: &Client, self_key: PublicKey) -> Option<Self> {
|
||||
let own = stored_contact_list(client, self_key).await?;
|
||||
let follows: HashSet<PublicKey> = own.tags.public_keys().take(MAX_FOLLOWS + 1).collect();
|
||||
|
||||
let events: Vec<Event> = if follows.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let authors: Vec<PublicKey> = follows.iter().copied().collect();
|
||||
let filter = Filter::new().kind(Kind::ContactList).authors(authors);
|
||||
|
||||
client
|
||||
.database()
|
||||
.query(filter)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect()
|
||||
};
|
||||
|
||||
let snapshot = Self::build(self_key, follows, events);
|
||||
log::debug!(
|
||||
"wot loaded from database: {} follows, {} scored keys",
|
||||
snapshot.follows.len(),
|
||||
snapshot.scores.len()
|
||||
);
|
||||
|
||||
Some(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<WoTEvent> for WebOfTrust {}
|
||||
@@ -88,8 +122,10 @@ impl EventEmitter<WoTEvent> for WebOfTrust {}
|
||||
pub struct WebOfTrust {
|
||||
/// Current user's public key
|
||||
self_key: Option<PublicKey>,
|
||||
/// Latest built snapshot, none until the first refresh completes
|
||||
/// Latest built snapshot, none until the first load completes
|
||||
snapshot: Option<Snapshot>,
|
||||
/// Whether a reconciliation with the relays is in flight
|
||||
syncing: bool,
|
||||
/// In-flight refresh, replaced by newer ones
|
||||
refresh_task: Option<Task<()>>,
|
||||
/// Keeps the periodic reconciliation loop alive
|
||||
@@ -110,9 +146,9 @@ impl WebOfTrust {
|
||||
|
||||
let mut has_data = false;
|
||||
|
||||
// Ignore errors, the next refresh will retry
|
||||
// Ignore errors, the next sync will retry
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
this.refresh(cx);
|
||||
this.sync(cx);
|
||||
has_data = this.has_data();
|
||||
});
|
||||
|
||||
@@ -133,34 +169,14 @@ impl WebOfTrust {
|
||||
Self {
|
||||
self_key: None,
|
||||
snapshot: None,
|
||||
syncing: false,
|
||||
refresh_task: None,
|
||||
_tasks: tasks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the graph for the current account
|
||||
/// Load the graph for the current account from the local database
|
||||
pub(crate) fn hydrate(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let Some(self_key) = nostr.read(cx).current_user() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.self_key = Some(self_key);
|
||||
self.refresh(cx);
|
||||
}
|
||||
|
||||
/// Clear the graph on sign out or account switch
|
||||
pub(crate) fn reset(&mut self, cx: &mut Context<Self>) {
|
||||
self.self_key = None;
|
||||
self.snapshot = None;
|
||||
self.refresh_task = None;
|
||||
|
||||
cx.emit(WoTEvent);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Reconcile the follow set's contact lists with the relays and rebuild.
|
||||
fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
|
||||
@@ -168,6 +184,55 @@ impl WebOfTrust {
|
||||
return;
|
||||
};
|
||||
|
||||
self.self_key = Some(self_key);
|
||||
// Replacing the task below cancels any in-flight work
|
||||
self.syncing = false;
|
||||
|
||||
self.refresh_task = Some(cx.spawn(async move |this, cx| {
|
||||
let Some(snapshot) = Snapshot::load(&client, self_key).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.snapshot = Some(snapshot);
|
||||
cx.emit(WoTEvent);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
||||
/// Clear the graph on sign out or account switch
|
||||
pub(crate) fn reset(&mut self, cx: &mut Context<Self>) {
|
||||
self.self_key = None;
|
||||
self.snapshot = None;
|
||||
self.syncing = false;
|
||||
self.refresh_task = None;
|
||||
|
||||
cx.emit(WoTEvent);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Reconcile the follow set's contact lists with the bootstrap relays and rebuild.
|
||||
fn sync(&mut self, cx: &mut Context<Self>) {
|
||||
if self.syncing {
|
||||
return;
|
||||
}
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
|
||||
let Some(self_key) = nostr.read(cx).current_user() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// The defaults are too short for relays streaming large batches
|
||||
let opts = SyncOptions::new()
|
||||
.initial_timeout(Duration::from_secs(SYNC_INITIAL_TIMEOUT))
|
||||
.idle_timeout(Duration::from_secs(SYNC_IDLE_TIMEOUT));
|
||||
|
||||
self.syncing = true;
|
||||
|
||||
self.refresh_task = Some(cx.spawn(async move |this, cx| {
|
||||
// Scope the reconcile to the follow set known so far
|
||||
let known: HashSet<PublicKey> = stored_contact_list(&client, self_key)
|
||||
@@ -178,60 +243,36 @@ impl WebOfTrust {
|
||||
let mut authors: Vec<PublicKey> = known.iter().copied().collect();
|
||||
authors.push(self_key);
|
||||
|
||||
// Negentropy transfers only the lists the relays hold
|
||||
// and the database is missing
|
||||
let filter = Filter::new().kind(Kind::ContactList).authors(authors);
|
||||
if let Err(error) = client.sync(filter).await {
|
||||
log::warn!("Failed to reconcile contact lists: {error}");
|
||||
match client.sync(filter).with(BOOTSTRAP_RELAYS).opts(opts).await {
|
||||
Ok(output) => {
|
||||
for (url, error) in output.failed {
|
||||
log::warn!("Failed to reconcile contact lists with {url}: {error}");
|
||||
}
|
||||
}
|
||||
Err(error) => log::warn!("Failed to reconcile contact lists: {error}"),
|
||||
}
|
||||
|
||||
// The follow set after the reconcile
|
||||
let Some(own) = stored_contact_list(&client, self_key).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let follows: HashSet<PublicKey> =
|
||||
own.tags.public_keys().take(MAX_FOLLOWS + 1).collect();
|
||||
|
||||
let mut authors: Vec<PublicKey> = follows.iter().copied().collect();
|
||||
authors.push(self_key);
|
||||
|
||||
let events: Vec<Event> = if follows.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let filter = Filter::new().kind(Kind::ContactList).authors(authors);
|
||||
client
|
||||
.database()
|
||||
.query(filter)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect()
|
||||
};
|
||||
|
||||
let snapshot = Snapshot::build(self_key, follows.clone(), events);
|
||||
log::debug!(
|
||||
"Web of trust rebuilt: {} follows, {} scored keys",
|
||||
snapshot.follows.len(),
|
||||
snapshot.scores.len()
|
||||
);
|
||||
|
||||
// Newly followed authors were not part of the reconcile
|
||||
let grew = !known.is_superset(&follows);
|
||||
let snapshot = Snapshot::load(&client, self_key).await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.snapshot = Some(snapshot);
|
||||
cx.emit(WoTEvent);
|
||||
cx.notify();
|
||||
this.syncing = false;
|
||||
|
||||
if let Some(snapshot) = snapshot {
|
||||
// Newly followed authors were not part of the reconcile
|
||||
let grew = !known.is_superset(&snapshot.follows);
|
||||
|
||||
this.snapshot = Some(snapshot);
|
||||
cx.emit(WoTEvent);
|
||||
cx.notify();
|
||||
|
||||
if grew {
|
||||
this.sync(cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
if grew {
|
||||
this.update(cx, |this, cx| {
|
||||
this.refresh(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Error, anyhow};
|
||||
use common::config_dir;
|
||||
use gpui::{App, AppContext, Context, Entity, Global, ImageSource, Subscription, Task};
|
||||
use gpui::{App, AppContext, Context, Entity, Global, ImageSource, Subscription, Task, px};
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
@@ -31,6 +31,33 @@ impl AppearanceMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// The default base font size for the interface
|
||||
pub const DEFAULT_FONT_SIZE: f32 = 16.0;
|
||||
|
||||
/// User tuning applied on top of the native OS theme
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ThemeSettings {
|
||||
/// The base UI font size in pixels
|
||||
pub font_size: f32,
|
||||
/// Draw a ring around focused controls
|
||||
pub focus_ring: bool,
|
||||
/// Corner radius for general elements; follows the OS theme when unset
|
||||
pub radius: Option<f32>,
|
||||
/// Corner radius for large elements like dialogs; follows the OS theme when unset
|
||||
pub radius_lg: Option<f32>,
|
||||
}
|
||||
|
||||
impl Default for ThemeSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_size: DEFAULT_FONT_SIZE,
|
||||
focus_ring: false,
|
||||
radius: None,
|
||||
radius_lg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AppSettings::set_global(cx.new(AppSettings::new), cx)
|
||||
}
|
||||
@@ -40,17 +67,34 @@ pub fn init(cx: &mut App) {
|
||||
/// The async load fills the global settings later; the native theme must be
|
||||
/// applied before the first window opens, so boot reads the mode straight
|
||||
/// from the settings file instead.
|
||||
pub fn persisted_appearance_mode() -> AppearanceMode {
|
||||
fn persisted_settings() -> Option<Settings> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let path = config_dir().join(".settings");
|
||||
if let Ok(content) = std::fs::read_to_string(path)
|
||||
&& let Ok(settings) = serde_json::from_str::<Settings>(&content)
|
||||
{
|
||||
return settings.appearance_mode;
|
||||
return Some(settings);
|
||||
}
|
||||
}
|
||||
AppearanceMode::default()
|
||||
None
|
||||
}
|
||||
|
||||
pub fn persisted_appearance_mode() -> AppearanceMode {
|
||||
persisted_settings()
|
||||
.map(|settings| settings.appearance_mode)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Read the persisted theme tuning synchronously.
|
||||
///
|
||||
/// Like [`persisted_appearance_mode`], the native theme is applied before the
|
||||
/// async settings load completes, so boot reads the tuning straight from the
|
||||
/// settings file instead.
|
||||
pub fn persisted_theme() -> ThemeSettings {
|
||||
persisted_settings()
|
||||
.map(|settings| settings.theme)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
const DEFAULT_FILE_SERVER: &str = "https://nostr.download/";
|
||||
@@ -109,6 +153,7 @@ setting_accessors! {
|
||||
}
|
||||
|
||||
setting_getters! {
|
||||
pub theme: ThemeSettings,
|
||||
pub pinned_rooms: Vec<u64>,
|
||||
pub pinned_groups: Vec<String>,
|
||||
}
|
||||
@@ -179,6 +224,9 @@ pub struct Settings {
|
||||
/// Hide the notice that messages in public groups are not encrypted
|
||||
#[serde(default)]
|
||||
pub hide_group_notice: bool,
|
||||
/// Tuning applied on top of the native OS theme
|
||||
#[serde(default)]
|
||||
pub theme: ThemeSettings,
|
||||
/// Show an image behind chat and group panels
|
||||
#[serde(default)]
|
||||
pub panel_background: bool,
|
||||
@@ -206,6 +254,7 @@ impl Default for Settings {
|
||||
wot_auto_trust: true,
|
||||
nip4e: false,
|
||||
hide_group_notice: false,
|
||||
theme: ThemeSettings::default(),
|
||||
panel_background: true,
|
||||
panel_background_path: None,
|
||||
trusted_relays: vec![],
|
||||
@@ -270,6 +319,34 @@ impl AppSettings {
|
||||
});
|
||||
}
|
||||
|
||||
/// Edit the theme tuning
|
||||
pub fn update_theme(edit: impl FnOnce(&mut ThemeSettings), cx: &mut App) {
|
||||
Self::global(cx).update(cx, |this, cx| {
|
||||
this.inner.update(cx, |inner, cx| {
|
||||
edit(&mut inner.theme);
|
||||
cx.notify();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Apply the persisted theme tuning on top of the active theme
|
||||
pub fn apply_theme_style(cx: &mut App) {
|
||||
use gpui_kit::component::Theme;
|
||||
|
||||
let theme = Self::get_theme(cx);
|
||||
let styled = Theme::global_mut(cx);
|
||||
styled.font_size = px(theme.font_size);
|
||||
styled.focus_ring = theme.focus_ring;
|
||||
if let Some(radius) = theme.radius {
|
||||
styled.radius = px(radius);
|
||||
}
|
||||
if let Some(radius_lg) = theme.radius_lg {
|
||||
styled.radius_lg = px(radius_lg);
|
||||
}
|
||||
Theme::sync_base(cx);
|
||||
cx.refresh_windows();
|
||||
}
|
||||
|
||||
/// Load settings
|
||||
fn load(&mut self, cx: &mut Context<Self>) {
|
||||
let task: Task<Result<Settings, Error>> = cx.background_spawn(async move {
|
||||
@@ -382,3 +459,27 @@ impl AppSettings {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Settings written before theme tuning existed must keep loading.
|
||||
#[test]
|
||||
fn settings_without_theme_keep_defaults() {
|
||||
let settings: Settings = serde_json::from_str(
|
||||
r#"{
|
||||
"appearance_mode": "Dark",
|
||||
"hide_avatar": false,
|
||||
"screening": true,
|
||||
"wot_auto_trust": true,
|
||||
"nip4e": false,
|
||||
"trusted_relays": [],
|
||||
"file_server": "https://nostr.download/"
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(settings.theme, ThemeSettings::default());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@ publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
common = { path = "../common" }
|
||||
person = { path = "../person" }
|
||||
gpui.workspace = true
|
||||
gpui-base.workspace = true
|
||||
gpui-kit.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
web-time = "1.1"
|
||||
smallvec.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -1,64 +1,124 @@
|
||||
use gpui::{
|
||||
AbsoluteLength, AnyElement, App, Bounds, Div, Hsla, ImageSource, InteractiveElement,
|
||||
Interactivity, IntoElement, ObjectFit, ParentElement, PathBuilder, Pixels, Point, RenderOnce,
|
||||
SharedString, StyleRefinement, Styled, StyledImage, Window, canvas, div, img, point, px,
|
||||
App, Div, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce,
|
||||
SharedString, StyleRefinement, Styled, Window, div, px,
|
||||
};
|
||||
use gpui_base::AvatarImage;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::menu::DropdownMenu;
|
||||
use gpui_kit::component::{ActiveTheme as _, Selectable, Sizable, Size};
|
||||
use gpui_kit::component::{ActiveTheme, Selectable, Sizable, Size};
|
||||
|
||||
/// Number of rows and columns in the generated pixel grid.
|
||||
const PIXEL_GRID: usize = 8;
|
||||
/// Number of rows and columns in the pixel grid.
|
||||
const GRID_SIZE: usize = 8;
|
||||
/// Empty cells kept between the pattern and the avatar edge.
|
||||
const MARGIN: usize = 1;
|
||||
/// Probability that a cell in the left half of the pattern area is filled.
|
||||
const FILL_PROBABILITY: f32 = 0.42;
|
||||
/// Probability that a filled cell uses the accent shade instead of the main color.
|
||||
const ACCENT_PROBABILITY: f32 = 0.25;
|
||||
/// Minimum number of filled left-half cells, so a pattern never reads as empty.
|
||||
/// Minimum number of filled left-half cells.
|
||||
const MIN_FILLED: usize = 5;
|
||||
/// Fallback seed for an avatar that has neither a picture nor a seed of its own.
|
||||
/// Seed for an avatar that has neither a picture nor a seed of its own.
|
||||
const FALLBACK_SEED: &str = "coop";
|
||||
/// Number of segments used to approximate the avatar circle.
|
||||
const CIRCLE_SEGMENTS: usize = 32;
|
||||
|
||||
/// Returns the size of the avatar based on the given [`Size].
|
||||
fn avatar_size(size: Size) -> AbsoluteLength {
|
||||
match size {
|
||||
Size::Large => px(64.).into(),
|
||||
Size::Medium => px(32.).into(),
|
||||
Size::Small => px(24.).into(),
|
||||
Size::XSmall => px(18.).into(),
|
||||
Size::Size(size) => size.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A deterministic, offline pixel-art avatar derived from a seed.
|
||||
/// An avatar for a user or other entity.
|
||||
#[derive(IntoElement)]
|
||||
struct PixelAvatar {
|
||||
seed: u64,
|
||||
pub struct Avatar {
|
||||
base: Div,
|
||||
picture: Option<SharedString>,
|
||||
seed: Option<String>,
|
||||
size: Size,
|
||||
selected: bool,
|
||||
style: StyleRefinement,
|
||||
}
|
||||
|
||||
impl PixelAvatar {
|
||||
/// Creates a pixel avatar from the given seed.
|
||||
fn new(seed: impl AsRef<str>) -> Self {
|
||||
impl Avatar {
|
||||
/// Create an avatar showing `picture`, falling back to pixel art seeded by [`Avatar::seed`].
|
||||
pub fn new(picture: Option<SharedString>) -> Self {
|
||||
Self {
|
||||
seed: fnv1a(seed.as_ref().as_bytes()),
|
||||
base: div(),
|
||||
picture,
|
||||
seed: None,
|
||||
size: Size::Medium,
|
||||
selected: false,
|
||||
style: StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The seed for the generated pixel avatar.
|
||||
pub fn seed(mut self, seed: impl Into<String>) -> Self {
|
||||
self.seed = Some(seed.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for PixelAvatar {
|
||||
impl Selectable for Avatar {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Avatar {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Avatar {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Avatar {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.base.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl DropdownMenu for Avatar {}
|
||||
|
||||
impl RenderOnce for Avatar {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let side = side_length(self.size);
|
||||
|
||||
let content = match self.picture {
|
||||
Some(url) => div()
|
||||
.size_full()
|
||||
.child(AvatarImage::new(url).size_full().rounded_full())
|
||||
.into_any_element(),
|
||||
None => PixelAvatar {
|
||||
seed: fnv1a(self.seed.unwrap_or_else(|| FALLBACK_SEED.into()).as_bytes()),
|
||||
side,
|
||||
style: StyleRefinement::default(),
|
||||
}
|
||||
.into_any_element(),
|
||||
};
|
||||
|
||||
self.base
|
||||
.size(side)
|
||||
.flex_shrink_0()
|
||||
.rounded_full()
|
||||
.overflow_hidden()
|
||||
.bg(cx.theme().secondary)
|
||||
.child(content)
|
||||
.refine_style(&self.style)
|
||||
}
|
||||
}
|
||||
|
||||
/// A deterministic, offline pixel-art avatar.
|
||||
#[derive(IntoElement)]
|
||||
struct PixelAvatar {
|
||||
seed: u64,
|
||||
side: Pixels,
|
||||
style: StyleRefinement,
|
||||
}
|
||||
|
||||
impl Styled for PixelAvatar {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
@@ -66,76 +126,89 @@ impl Styled for PixelAvatar {
|
||||
}
|
||||
|
||||
impl RenderOnce for PixelAvatar {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let side = avatar_size(self.size).to_pixels(window.rem_size());
|
||||
let seed = self.seed;
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let pattern = pattern(self.seed);
|
||||
let mut cells = Vec::new();
|
||||
|
||||
let main = Hsla {
|
||||
h: self.seed as f32 / u64::MAX as f32,
|
||||
s: 0.6,
|
||||
l: if theme.is_dark() { 0.6 } else { 0.45 },
|
||||
a: 1.,
|
||||
};
|
||||
|
||||
let shade = if theme.is_dark() {
|
||||
Hsla {
|
||||
l: (main.l * 1.6).min(0.95),
|
||||
..main
|
||||
}
|
||||
} else {
|
||||
Hsla {
|
||||
l: (main.l * 0.45).max(0.18),
|
||||
..main
|
||||
}
|
||||
};
|
||||
|
||||
for row in 0..GRID_SIZE {
|
||||
for col in 0..GRID_SIZE {
|
||||
let value = pattern[row * GRID_SIZE + col];
|
||||
if value != 0 {
|
||||
let color = if value == 2 { shade } else { main };
|
||||
cells.push(
|
||||
div()
|
||||
.row_start(row as i16 + 1)
|
||||
.row_end(row as i16 + 2)
|
||||
.col_start(col as i16 + 1)
|
||||
.col_end(col as i16 + 2)
|
||||
.bg(color),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pad the grid so the art stays clear of the circular edge, keeping the
|
||||
// inset look of signed's rounded-square frame.
|
||||
let inset = self.side / (GRID_SIZE as f32 * 2.);
|
||||
|
||||
div()
|
||||
.refine_style(&self.style)
|
||||
.size(side)
|
||||
.grid()
|
||||
.grid_cols(GRID_SIZE as u16)
|
||||
.grid_rows(GRID_SIZE as u16)
|
||||
.size(self.side)
|
||||
.p(inset)
|
||||
.flex_shrink_0()
|
||||
.child(canvas(
|
||||
move |_bounds, _window, _cx| seed,
|
||||
move |bounds, seed, window, cx| {
|
||||
let theme = cx.theme();
|
||||
let main = Hsla {
|
||||
h: seed as f32 / u64::MAX as f32,
|
||||
s: 0.6,
|
||||
l: if theme.is_dark() { 0.6 } else { 0.45 },
|
||||
a: 1.,
|
||||
};
|
||||
let shade = if theme.is_dark() {
|
||||
Hsla {
|
||||
l: (main.l * 1.6).min(0.95),
|
||||
..main
|
||||
}
|
||||
} else {
|
||||
Hsla {
|
||||
l: (main.l * 0.45).max(0.18),
|
||||
..main
|
||||
}
|
||||
};
|
||||
|
||||
let circle = circle_polygon(bounds.center(), bounds.size.width.as_f32() / 2.);
|
||||
paint_polygons(window, std::iter::once(&circle), main.opacity(0.16));
|
||||
|
||||
let pattern = pixel_pattern(seed);
|
||||
let mut cells = Vec::new();
|
||||
|
||||
for (value, color) in [(1u8, main), (2u8, shade)] {
|
||||
cells.clear();
|
||||
|
||||
for row in 0..PIXEL_GRID {
|
||||
for col in 0..PIXEL_GRID {
|
||||
if pattern[row * PIXEL_GRID + col] != value {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cell = clip_polygon(&cell_polygon(&bounds, row, col), &circle);
|
||||
if cell.len() >= 3 {
|
||||
cells.push(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paint_polygons(window, cells.iter(), color);
|
||||
}
|
||||
},
|
||||
))
|
||||
.rounded_full()
|
||||
.overflow_hidden()
|
||||
.bg(main.opacity(0.16))
|
||||
.children(cells)
|
||||
.refine_style(&self.style)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the mirrored fill pattern for the given seed.
|
||||
fn pixel_pattern(seed: u64) -> [u8; PIXEL_GRID * PIXEL_GRID] {
|
||||
/// The rendered side length of an avatar at `size`.
|
||||
fn side_length(size: Size) -> Pixels {
|
||||
match size {
|
||||
Size::XSmall => px(16.),
|
||||
Size::Small => px(24.),
|
||||
Size::Medium => px(48.),
|
||||
Size::Large => px(80.),
|
||||
Size::Size(size) => size,
|
||||
}
|
||||
}
|
||||
|
||||
fn pattern(seed: u64) -> [u8; GRID_SIZE * GRID_SIZE] {
|
||||
let mut rng = PixelRng::new(seed);
|
||||
let mut pattern = [0u8; PIXEL_GRID * PIXEL_GRID];
|
||||
let mut pattern = [0u8; GRID_SIZE * GRID_SIZE];
|
||||
let mut filled = 0usize;
|
||||
|
||||
let art_rows = PIXEL_GRID - 2 * MARGIN;
|
||||
let art_columns = PIXEL_GRID / 2 - MARGIN;
|
||||
// Only the inner rows and the inner left half are candidates; mirroring
|
||||
// then keeps the art within the same inset, leaving the outer ring empty.
|
||||
let art_rows = GRID_SIZE - 2 * MARGIN;
|
||||
let art_columns = GRID_SIZE / 2 - MARGIN;
|
||||
|
||||
for row in MARGIN..PIXEL_GRID - MARGIN {
|
||||
for col in MARGIN..PIXEL_GRID / 2 {
|
||||
for row in MARGIN..GRID_SIZE - MARGIN {
|
||||
for col in MARGIN..GRID_SIZE / 2 {
|
||||
if rng.chance(FILL_PROBABILITY) {
|
||||
let accent = rng.chance(ACCENT_PROBABILITY);
|
||||
set_cell(&mut pattern, row, col, if accent { 2 } else { 1 });
|
||||
@@ -157,7 +230,7 @@ fn pixel_pattern(seed: u64) -> [u8; PIXEL_GRID * PIXEL_GRID] {
|
||||
let row = MARGIN + ix / art_columns;
|
||||
let col = MARGIN + ix % art_columns;
|
||||
|
||||
if pattern[row * PIXEL_GRID + col] == 0 {
|
||||
if pattern[row * GRID_SIZE + col] == 0 {
|
||||
set_cell(&mut pattern, row, col, 1);
|
||||
filled += 1;
|
||||
}
|
||||
@@ -167,146 +240,10 @@ fn pixel_pattern(seed: u64) -> [u8; PIXEL_GRID * PIXEL_GRID] {
|
||||
pattern
|
||||
}
|
||||
|
||||
/// Paints `polygons as a single anti-aliased filled path in `color.
|
||||
fn paint_polygons<'a>(
|
||||
window: &mut Window,
|
||||
polygons: impl IntoIterator<Item = &'a Vec<Point<Pixels>>>,
|
||||
color: Hsla,
|
||||
) {
|
||||
let mut builder = PathBuilder::fill();
|
||||
let mut painted = false;
|
||||
|
||||
for polygon in polygons {
|
||||
if polygon.len() >= 3 {
|
||||
builder.add_polygon(polygon, true);
|
||||
painted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if painted && let Ok(path) = builder.build() {
|
||||
window.paint_path(path, color);
|
||||
}
|
||||
}
|
||||
|
||||
/// Approximates the circle of `radius around `center as a convex polygon wound left-interior.
|
||||
fn circle_polygon(center: Point<Pixels>, radius: f32) -> Vec<Point<Pixels>> {
|
||||
let center_x = center.x.as_f32();
|
||||
let center_y = center.y.as_f32();
|
||||
|
||||
(0..CIRCLE_SEGMENTS)
|
||||
.map(|index| {
|
||||
let angle = std::f32::consts::TAU * index as f32 / CIRCLE_SEGMENTS as f32;
|
||||
point(
|
||||
px(center_x + radius * angle.cos()),
|
||||
px(center_y + radius * angle.sin()),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The four corners of cell `(row, col) of the grid laid out in `bounds.
|
||||
fn cell_polygon(bounds: &Bounds<Pixels>, row: usize, col: usize) -> [Point<Pixels>; 4] {
|
||||
let cell = bounds.size.width.as_f32() / PIXEL_GRID as f32;
|
||||
let left = bounds.origin.x.as_f32() + col as f32 * cell;
|
||||
let top = bounds.origin.y.as_f32() + row as f32 * cell;
|
||||
|
||||
[
|
||||
point(px(left), px(top)),
|
||||
point(px(left + cell), px(top)),
|
||||
point(px(left + cell), px(top + cell)),
|
||||
point(px(left), px(top + cell)),
|
||||
]
|
||||
}
|
||||
|
||||
/// Clips `subject to the convex `clip polygon, keeping the part inside it.
|
||||
fn clip_polygon(subject: &[Point<Pixels>], clip: &[Point<Pixels>]) -> Vec<Point<Pixels>> {
|
||||
let mut current = subject.to_vec();
|
||||
let mut next = Vec::with_capacity(subject.len() + 4);
|
||||
|
||||
for (&start, &end) in clip.iter().zip(clip.iter().cycle().skip(1)) {
|
||||
if current.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
next.clear();
|
||||
let mut previous = match current.last() {
|
||||
Some(&vertex) => vertex,
|
||||
None => break,
|
||||
};
|
||||
|
||||
for &vertex in current.iter() {
|
||||
let previous_inside = is_inside(start, end, previous);
|
||||
let vertex_inside = is_inside(start, end, vertex);
|
||||
|
||||
if vertex_inside {
|
||||
if !previous_inside
|
||||
&& let Some(crossing) = line_intersection(start, end, previous, vertex)
|
||||
{
|
||||
next.push(crossing);
|
||||
}
|
||||
next.push(vertex);
|
||||
} else if previous_inside
|
||||
&& let Some(crossing) = line_intersection(start, end, previous, vertex)
|
||||
{
|
||||
next.push(crossing);
|
||||
}
|
||||
|
||||
previous = vertex;
|
||||
}
|
||||
|
||||
std::mem::swap(&mut current, &mut next);
|
||||
}
|
||||
|
||||
current
|
||||
}
|
||||
|
||||
/// Whether `vertex lies on the interior side of the directed edge `start -> end.
|
||||
fn is_inside(start: Point<Pixels>, end: Point<Pixels>, vertex: Point<Pixels>) -> bool {
|
||||
let start_x = start.x.as_f32();
|
||||
let start_y = start.y.as_f32();
|
||||
let edge_x = end.x.as_f32() - start_x;
|
||||
let edge_y = end.y.as_f32() - start_y;
|
||||
let to_vertex_x = vertex.x.as_f32() - start_x;
|
||||
let to_vertex_y = vertex.y.as_f32() - start_y;
|
||||
|
||||
edge_x * to_vertex_y - edge_y * to_vertex_x >= 0.
|
||||
}
|
||||
|
||||
/// The intersection of segment `from -> to with the infinite line `start -> end.
|
||||
fn line_intersection(
|
||||
start: Point<Pixels>,
|
||||
end: Point<Pixels>,
|
||||
from: Point<Pixels>,
|
||||
to: Point<Pixels>,
|
||||
) -> Option<Point<Pixels>> {
|
||||
let start_x = start.x.as_f32();
|
||||
let start_y = start.y.as_f32();
|
||||
let edge_x = end.x.as_f32() - start_x;
|
||||
let edge_y = end.y.as_f32() - start_y;
|
||||
let from_x = from.x.as_f32();
|
||||
let from_y = from.y.as_f32();
|
||||
let segment_x = to.x.as_f32() - from_x;
|
||||
let segment_y = to.y.as_f32() - from_y;
|
||||
let denominator = edge_x * segment_y - edge_y * segment_x;
|
||||
|
||||
if denominator.abs() < f32::EPSILON {
|
||||
return None;
|
||||
}
|
||||
|
||||
let offset_x = from_x - start_x;
|
||||
let offset_y = from_y - start_y;
|
||||
let t = (edge_y * offset_x - edge_x * offset_y) / denominator;
|
||||
|
||||
Some(point(
|
||||
px(from_x + segment_x * t),
|
||||
px(from_y + segment_y * t),
|
||||
))
|
||||
}
|
||||
|
||||
/// Fills `cell (row, col) and its horizontal mirror.
|
||||
fn set_cell(pattern: &mut [u8; PIXEL_GRID * PIXEL_GRID], row: usize, col: usize, value: u8) {
|
||||
pattern[row * PIXEL_GRID + col] = value;
|
||||
pattern[row * PIXEL_GRID + (PIXEL_GRID - 1 - col)] = value;
|
||||
/// Fill `cell (row, col)` and its horizontal mirror.
|
||||
fn set_cell(pattern: &mut [u8; GRID_SIZE * GRID_SIZE], row: usize, col: usize, value: u8) {
|
||||
pattern[row * GRID_SIZE + col] = value;
|
||||
pattern[row * GRID_SIZE + (GRID_SIZE - 1 - col)] = value;
|
||||
}
|
||||
|
||||
/// FNV-1a 64-bit hash, stable across platforms and runs.
|
||||
@@ -340,109 +277,3 @@ impl PixelRng {
|
||||
self.next() as f32 / (u64::MAX as f32) < probability
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the generated pixel avatar shown in place of a missing picture.
|
||||
fn generated_avatar(seed: Option<&str>, size: Pixels) -> AnyElement {
|
||||
PixelAvatar::new(seed.unwrap_or(FALLBACK_SEED))
|
||||
.with_size(size)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// An element that renders a user avatar with customizable appearance options.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Avatar {
|
||||
base: Div,
|
||||
picture: Option<ImageSource>,
|
||||
seed: Option<String>,
|
||||
style: StyleRefinement,
|
||||
size: Size,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl Avatar {
|
||||
/// Creates an avatar for an entity whose profile picture may be missing.
|
||||
pub fn new(picture: Option<SharedString>) -> Self {
|
||||
Self::from_source(picture)
|
||||
}
|
||||
|
||||
/// Creates an avatar from an already-resolved source, which may be missing.
|
||||
pub fn from_source(picture: Option<impl Into<ImageSource>>) -> Self {
|
||||
Self::from_picture(picture.map(Into::into))
|
||||
}
|
||||
|
||||
fn from_picture(picture: Option<ImageSource>) -> Self {
|
||||
Avatar {
|
||||
base: div(),
|
||||
picture,
|
||||
seed: None,
|
||||
style: StyleRefinement::default(),
|
||||
size: Size::Medium,
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the seed for the generated pixel avatar.
|
||||
pub fn seed(mut self, seed: impl Into<String>) -> Self {
|
||||
self.seed = Some(seed.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Avatar {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Avatar {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for Avatar {
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Avatar {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
self.base.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl DropdownMenu for Avatar {}
|
||||
|
||||
impl RenderOnce for Avatar {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let image_size = avatar_size(self.size).to_pixels(window.rem_size());
|
||||
|
||||
let content = match self.picture {
|
||||
Some(picture) => {
|
||||
let seed = self.seed;
|
||||
|
||||
img(picture)
|
||||
.size(image_size)
|
||||
.rounded_full()
|
||||
.object_fit(ObjectFit::Cover)
|
||||
.with_fallback(move || generated_avatar(seed.as_deref(), image_size))
|
||||
.into_any_element()
|
||||
}
|
||||
None => generated_avatar(self.seed.as_deref(), image_size),
|
||||
};
|
||||
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.size(image_size)
|
||||
.rounded_full()
|
||||
.overflow_hidden()
|
||||
.child(content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
use gpui::{App, Div, IntoElement, ParentElement, SharedString, Styled, div};
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::{ActiveTheme, h_flex, v_flex};
|
||||
|
||||
/// A settings row: the label and description on the left, the control on the right.
|
||||
pub fn settings_row(
|
||||
label: impl Into<SharedString>,
|
||||
description: impl Into<SharedString>,
|
||||
control: impl IntoElement,
|
||||
cx: &App,
|
||||
) -> Div {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_4()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(div().text_sm().font_semibold().child(label.into()))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(description.into()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_40()
|
||||
.flex_shrink_0()
|
||||
.justify_end()
|
||||
.items_center()
|
||||
.child(control),
|
||||
)
|
||||
}
|
||||
|
||||
/// A full-width settings block with the label and description in one header.
|
||||
pub fn settings_block(
|
||||
label: impl Into<SharedString>,
|
||||
description: impl Into<SharedString>,
|
||||
control: impl IntoElement,
|
||||
cx: &App,
|
||||
) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(div().text_sm().font_semibold().child(label.into()))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.truncate()
|
||||
.child(description.into()),
|
||||
),
|
||||
)
|
||||
.child(control)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use gpui::SharedString;
|
||||
use gpui_kit::assets::IconNamed;
|
||||
|
||||
/// Coop icons kept from before the gpui-kit migration; absent from the kit bundle.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum IconName {
|
||||
ArrowDownCircle,
|
||||
ArrowLeft,
|
||||
Book,
|
||||
CaretDown,
|
||||
CaretRight,
|
||||
CheckCircle,
|
||||
Close,
|
||||
Compass,
|
||||
Door,
|
||||
Emoji,
|
||||
Group,
|
||||
Input,
|
||||
Invite,
|
||||
Link,
|
||||
Lock,
|
||||
Message,
|
||||
PaperPlaneFill,
|
||||
Plus,
|
||||
PlusChat,
|
||||
Profile,
|
||||
Relay,
|
||||
Search,
|
||||
Settings,
|
||||
Settings2,
|
||||
Upload,
|
||||
Warning,
|
||||
}
|
||||
|
||||
impl IconNamed for IconName {
|
||||
fn path(self) -> SharedString {
|
||||
match self {
|
||||
Self::ArrowDownCircle => "icons/arrow-down-circle.svg",
|
||||
Self::ArrowLeft => "icons/arrow-left.svg",
|
||||
Self::Book => "icons/book.svg",
|
||||
Self::CaretDown => "icons/caret-down.svg",
|
||||
Self::CaretRight => "icons/caret-right.svg",
|
||||
Self::CheckCircle => "icons/check-circle.svg",
|
||||
Self::Close => "icons/close.svg",
|
||||
Self::Compass => "icons/compass.svg",
|
||||
Self::Door => "icons/door.svg",
|
||||
Self::Emoji => "icons/emoji.svg",
|
||||
Self::Group => "icons/group.svg",
|
||||
Self::Input => "icons/input.svg",
|
||||
Self::Invite => "icons/invite.svg",
|
||||
Self::Link => "icons/link.svg",
|
||||
Self::Lock => "icons/lock.svg",
|
||||
Self::Message => "icons/message.svg",
|
||||
Self::PaperPlaneFill => "icons/paper-plane-fill.svg",
|
||||
Self::Plus => "icons/plus.svg",
|
||||
Self::PlusChat => "icons/plus-chat.svg",
|
||||
Self::Profile => "icons/profile.svg",
|
||||
Self::Relay => "icons/relay.svg",
|
||||
Self::Search => "icons/search.svg",
|
||||
Self::Settings => "icons/settings.svg",
|
||||
Self::Settings2 => "icons/settings2.svg",
|
||||
Self::Upload => "icons/upload.svg",
|
||||
Self::Warning => "icons/warning.svg",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
pub use avatar::Avatar;
|
||||
pub use form::*;
|
||||
pub use icon::IconName;
|
||||
pub use message::*;
|
||||
pub use text::*;
|
||||
pub use window_drag::title_bar_drag_handlers;
|
||||
|
||||
pub mod avatar;
|
||||
pub mod form;
|
||||
pub mod icon;
|
||||
pub mod message;
|
||||
pub mod text;
|
||||
pub mod window_drag;
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, MouseButton,
|
||||
MouseDownEvent, ObjectFit, ParentElement as _, RenderOnce, SharedString, SharedUri,
|
||||
StatefulInteractiveElement as _, StyleRefinement, Styled, StyledImage, Window, div, img, px,
|
||||
AnyElement, App, ClickEvent, Context, ElementId, InteractiveElement as _, IntoElement,
|
||||
MouseButton, MouseDownEvent, ObjectFit, ParentElement as _, RenderOnce, SharedString,
|
||||
SharedUri, StatefulInteractiveElement as _, StyleRefinement, Styled, StyledImage, Window, div,
|
||||
img, px,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants as _};
|
||||
use gpui_kit::component::menu::{DropdownMenu as _, PopupMenu, PopupMenuItem};
|
||||
use gpui_kit::component::{
|
||||
ActiveTheme, InteractiveElementExt as _, StyledExt as _, h_flex, v_flex,
|
||||
ActiveTheme, InteractiveElementExt as _, Sizable as _, StyledExt as _, h_flex, v_flex,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
pub use ui::Avatar;
|
||||
|
||||
type MouseDownListener = Box<dyn Fn(&MouseDownEvent, &mut Window, &mut App) + 'static>;
|
||||
type ClickListener = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||
type SharedClickListener = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
|
||||
type DropdownBuilder = Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>;
|
||||
|
||||
/// Messages from the same author within this window are grouped into one run.
|
||||
pub const RUN_WINDOW_SECS: u64 = 300;
|
||||
@@ -179,7 +184,7 @@ impl RenderOnce for MessageRow {
|
||||
.group("")
|
||||
.relative()
|
||||
.w_full()
|
||||
.py_1()
|
||||
.py_0p5()
|
||||
.when(show_author, |this| this.pt_2())
|
||||
.px_3()
|
||||
.refine_style(&style)
|
||||
@@ -191,10 +196,10 @@ impl RenderOnce for MessageRow {
|
||||
if show_author {
|
||||
match avatar {
|
||||
Some(avatar) => this.child(avatar),
|
||||
None => this.child(div().flex_shrink_0().w(px(32.))),
|
||||
None => this.child(div().flex_shrink_0().w(px(24.))),
|
||||
}
|
||||
} else {
|
||||
this.child(div().flex_shrink_0().w(px(32.)))
|
||||
this.child(div().flex_shrink_0().w(px(24.)))
|
||||
}
|
||||
})
|
||||
.child(
|
||||
@@ -204,6 +209,7 @@ impl RenderOnce for MessageRow {
|
||||
.min_w_0()
|
||||
.flex_initial()
|
||||
.overflow_hidden()
|
||||
.text_size(px(15.))
|
||||
.when(show_author, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
@@ -229,7 +235,7 @@ impl RenderOnce for MessageRow {
|
||||
.when_some(on_double_click, |this, listener| {
|
||||
this.on_double_click(listener)
|
||||
})
|
||||
.hover(|this| this.bg(cx.theme().accent))
|
||||
.hover(|this| this.bg(cx.theme().list_hover))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
@@ -297,7 +303,7 @@ impl RenderOnce for ReplyPreview {
|
||||
.refine_style(&style)
|
||||
.child(div().font_semibold().child(author))
|
||||
.child(div().w_full().text_ellipsis().line_clamp(1).child(preview))
|
||||
.hover(|this| this.bg(cx.theme().secondary))
|
||||
.hover(|this| this.bg(cx.theme().list_hover))
|
||||
.when_some(on_click, |this, on_click| this.on_click(on_click))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -554,3 +560,174 @@ impl RenderOnce for WelcomeMessage {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Quick reaction emojis offered in message menus.
|
||||
pub const REACTION_EMOJIS: &[&str] = &["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"];
|
||||
|
||||
/// Quick reaction emojis shown on a hovered message's action bar.
|
||||
pub const COMPACT_REACTION_EMOJIS: &[&str] = &["👍", "❤️", "👀"];
|
||||
|
||||
/// Fill a popup menu with reactions to a message, offering `emojis` then the custom ones.
|
||||
///
|
||||
/// `react` receives the reaction content: an emoji, or a `:shortcode:` for a custom one.
|
||||
pub fn react_menu(
|
||||
menu: PopupMenu,
|
||||
emojis: &[&'static str],
|
||||
custom: &[String],
|
||||
react: impl Fn(&str, &mut Window, &mut App) + Clone + 'static,
|
||||
) -> PopupMenu {
|
||||
let menu = emojis.iter().fold(menu, |menu, emoji| {
|
||||
let emoji = *emoji;
|
||||
let react = react.clone();
|
||||
menu.item(PopupMenuItem::new(emoji).on_click(move |_, window, cx| react(emoji, window, cx)))
|
||||
});
|
||||
|
||||
if custom.is_empty() {
|
||||
return menu;
|
||||
}
|
||||
|
||||
custom.iter().fold(menu.separator(), |menu, emoji| {
|
||||
let emoji = emoji.clone();
|
||||
let react = react.clone();
|
||||
menu.item(
|
||||
PopupMenuItem::new(emoji.clone())
|
||||
.on_click(move |_, window, cx| react(&emoji, window, cx)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// A hovered message's action bar: quick reactions, reply, copy, and an overflow menu.
|
||||
#[derive(IntoElement)]
|
||||
pub struct MessageActions {
|
||||
style: StyleRefinement,
|
||||
reactions: SmallVec<[(&'static str, SharedClickListener); 4]>,
|
||||
reply: Option<SharedClickListener>,
|
||||
copy: Option<SharedClickListener>,
|
||||
dropdown: Option<DropdownBuilder>,
|
||||
}
|
||||
|
||||
impl MessageActions {
|
||||
/// Create an empty action bar.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
style: StyleRefinement::default(),
|
||||
reactions: SmallVec::new(),
|
||||
reply: None,
|
||||
copy: None,
|
||||
dropdown: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a quick reaction button.
|
||||
pub fn reaction(
|
||||
mut self,
|
||||
emoji: &'static str,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.reactions.push((emoji, Rc::new(handler)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add the reply button.
|
||||
pub fn reply(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.reply = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add the copy button.
|
||||
pub fn copy(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.copy = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Fill the bar's overflow menu.
|
||||
pub fn dropdown(
|
||||
mut self,
|
||||
dropdown: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
) -> Self {
|
||||
self.dropdown = Some(Rc::new(dropdown));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MessageActions {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for MessageActions {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for MessageActions {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let Self {
|
||||
style,
|
||||
reactions,
|
||||
reply,
|
||||
copy,
|
||||
dropdown,
|
||||
} = self;
|
||||
|
||||
let has_center = reply.is_some() || copy.is_some();
|
||||
|
||||
h_flex()
|
||||
.p_0p5()
|
||||
.gap_1()
|
||||
.invisible()
|
||||
.absolute()
|
||||
.right_4()
|
||||
.top_neg_2()
|
||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().background)
|
||||
.children(reactions.into_iter().map(|(emoji, handler)| {
|
||||
Button::new(emoji)
|
||||
.label(emoji)
|
||||
.tooltip(emoji)
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click(move |event, window, cx| handler(event, window, cx))
|
||||
}))
|
||||
.when(has_center, |this| {
|
||||
this.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
|
||||
})
|
||||
.when_some(reply, |this, reply| {
|
||||
this.child(
|
||||
Button::new("reply")
|
||||
.icon(IconName::Reply)
|
||||
.tooltip("Reply")
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click(move |event, window, cx| reply(event, window, cx)),
|
||||
)
|
||||
})
|
||||
.when_some(copy, |this, copy| {
|
||||
this.child(
|
||||
Button::new("copy")
|
||||
.icon(IconName::Copy)
|
||||
.tooltip("Copy")
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click(move |event, window, cx| copy(event, window, cx)),
|
||||
)
|
||||
})
|
||||
.when_some(dropdown, |this, dropdown| {
|
||||
this.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
|
||||
.child(
|
||||
Button::new("more")
|
||||
.icon(IconName::Ellipsis)
|
||||
.small()
|
||||
.ghost()
|
||||
.dropdown_menu(move |menu, window, cx| dropdown(menu, window, cx)),
|
||||
)
|
||||
})
|
||||
.group_hover("", |this| this.visible())
|
||||
.refine_style(&style)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use chat::{Mention, nevent_refs};
|
||||
use common::nevent_refs;
|
||||
use gpui::{AnyElement, App, ElementId, Entity, IntoElement as _, SharedString, SharedUri, Window};
|
||||
use gpui_kit::component::text::TextView;
|
||||
use nostr_sdk::prelude::Url;
|
||||
use nostr_sdk::prelude::{PublicKey, Url};
|
||||
use person::Directory;
|
||||
|
||||
/// A span of the source content, by byte range, that renders as `text` instead.
|
||||
@@ -121,22 +121,22 @@ fn nevent_bech32(url: &str) -> Option<&str> {
|
||||
bech32.starts_with("nevent1").then_some(bech32)
|
||||
}
|
||||
|
||||
/// Composes a message's rendered text: mention names, NIP-30 emojis, and nevent links.
|
||||
///
|
||||
/// `mentions` are the `(byte range, mentioned public key)` pairs found in `content`.
|
||||
pub fn rendered_text(
|
||||
content: &str,
|
||||
mentions: &[Mention],
|
||||
mentions: impl IntoIterator<Item = (Range<usize>, PublicKey)>,
|
||||
emojis: &[(String, Url)],
|
||||
persons: &Entity<Directory>,
|
||||
cx: &App,
|
||||
) -> RenderedText {
|
||||
let mut replacements = mentions
|
||||
.iter()
|
||||
.map(|mention| {
|
||||
.into_iter()
|
||||
.map(|(range, public_key)| {
|
||||
InlineReplacement::new(
|
||||
mention.range.clone(),
|
||||
format!(
|
||||
"@{}",
|
||||
persons.read(cx).person(&mention.public_key, cx).name()
|
||||
),
|
||||
range,
|
||||
format!("@{}", persons.read(cx).person(&public_key, cx).name()),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -338,7 +338,7 @@ impl NewChat {
|
||||
|
||||
Nav::new(start + offset)
|
||||
.label(profile.name())
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.when(!AppSettings::get_hide_avatar(cx), |this| {
|
||||
this.prefix(
|
||||
Avatar::new(profile.avatar())
|
||||
|
||||
@@ -3,14 +3,14 @@ use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Div, Entity, FocusHandle, InteractiveElement as _, IntoElement,
|
||||
ParentElement, PathPromptOptions, Render, SharedString, StatefulInteractiveElement as _,
|
||||
Styled, Subscription, Task, WeakEntity, Window, div, px, rems,
|
||||
StyleRefinement, Styled, Subscription, Task, WeakEntity, Window, div, px, rems,
|
||||
};
|
||||
use gpui_base::{Popup, Select};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle};
|
||||
use gpui_kit::component::form::field;
|
||||
use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants};
|
||||
use gpui_kit::component::input::{Input, InputEvent, InputState, Textarea, TextareaState};
|
||||
use gpui_kit::component::separator::Separator;
|
||||
use gpui_kit::component::switch::Switch;
|
||||
@@ -19,14 +19,14 @@ use nip29::{GroupCreateOptions, GroupKey, GroupsEvent, GroupsRegistry};
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use state::upload;
|
||||
use ui::Avatar;
|
||||
use ui::{Avatar, settings_row};
|
||||
|
||||
const DEFAULT_RELAYS: [&str; 2] = ["wss://relay.ditto.net", "wss://chat.wisp.talk"];
|
||||
const PRIVATE_DESC: &str = "Only members can read group messages";
|
||||
const CLOSED_DESC: &str = "Join requests are ignored (invite-only)";
|
||||
const RESTRICTED_DESC: &str = "Only members can post messages";
|
||||
const HIDDEN_DESC: &str = "Hidden from non-members, not discoverable";
|
||||
const LIST_PRIVATE_DESC: &str = "Kept encrypted in your group list so others can't see you joined";
|
||||
const LIST_PRIVATE_DESC: &str = "Kept encrypted in your group list so others can't see";
|
||||
|
||||
pub fn open(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) {
|
||||
open_with(dock, None, window, cx);
|
||||
@@ -462,71 +462,72 @@ impl NewGroup {
|
||||
}
|
||||
|
||||
fn render_access(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let private = field().label("Private").description(PRIVATE_DESC).child(
|
||||
Switch::new("private")
|
||||
.checked(self.private)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.private = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
);
|
||||
|
||||
let closed = field().label("Closed").description(CLOSED_DESC).child(
|
||||
Switch::new("closed")
|
||||
.checked(self.closed)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.closed = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
);
|
||||
|
||||
let restricted = field()
|
||||
.label("Restricted")
|
||||
.description(RESTRICTED_DESC)
|
||||
.child(
|
||||
GroupBox::new()
|
||||
.fill()
|
||||
.title("Access settings")
|
||||
.title_style(group_box_title_style(cx))
|
||||
.child(settings_row(
|
||||
"Private",
|
||||
PRIVATE_DESC,
|
||||
Switch::new("private")
|
||||
.checked(self.private)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.private = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Closed",
|
||||
CLOSED_DESC,
|
||||
Switch::new("closed")
|
||||
.checked(self.closed)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.closed = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Restricted",
|
||||
RESTRICTED_DESC,
|
||||
Switch::new("restricted")
|
||||
.checked(self.restricted)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.restricted = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
);
|
||||
|
||||
let hidden = field().label("Hidden").description(HIDDEN_DESC).child(
|
||||
Switch::new("hidden")
|
||||
.checked(self.hidden)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.hidden = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(field_label("Access settings", cx))
|
||||
.child(switch_row(private, cx))
|
||||
.child(switch_row(closed, cx))
|
||||
.child(switch_row(restricted, cx))
|
||||
.child(switch_row(hidden, cx))
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Hidden",
|
||||
HIDDEN_DESC,
|
||||
Switch::new("hidden")
|
||||
.checked(self.hidden)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.hidden = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
fn render_list_setting(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let list_private = field()
|
||||
.label("Private on my list")
|
||||
.description(LIST_PRIVATE_DESC)
|
||||
.child(
|
||||
GroupBox::new()
|
||||
.fill()
|
||||
.title("List")
|
||||
.title_style(group_box_title_style(cx))
|
||||
.child(settings_row(
|
||||
"Private on my list",
|
||||
LIST_PRIVATE_DESC,
|
||||
Switch::new("list-private")
|
||||
.checked(self.list_private)
|
||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||
this.list_private = *checked;
|
||||
cx.notify();
|
||||
})),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(field_label("List", cx))
|
||||
.child(switch_row(list_private, cx))
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
fn render_error(
|
||||
@@ -662,13 +663,12 @@ fn field_label(text: &'static str, cx: &App) -> Div {
|
||||
.child(text)
|
||||
}
|
||||
|
||||
fn switch_row(switch: impl IntoElement, cx: &App) -> Div {
|
||||
div()
|
||||
.w_full()
|
||||
.p_2()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().secondary)
|
||||
.child(switch)
|
||||
/// Match [`field_label`]'s design for GroupBox titles.
|
||||
fn group_box_title_style(cx: &App) -> StyleRefinement {
|
||||
StyleRefinement::default()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.font_semibold()
|
||||
}
|
||||
|
||||
fn relay_label(relay: &RelayUrl) -> SharedString {
|
||||
|
||||
@@ -424,19 +424,19 @@ impl Render for ProfileDialog {
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.child(label("Name", cx))
|
||||
.child(Input::new(&self.name_input).small()),
|
||||
.child(Input::new(&self.name_input)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.child(label("About", cx))
|
||||
.child(Textarea::new(&self.bio_input).small()),
|
||||
.child(Textarea::new(&self.bio_input)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.child(label("Website", cx))
|
||||
.child(Input::new(&self.website_input).small()),
|
||||
.child(Input::new(&self.website_input)),
|
||||
)
|
||||
})
|
||||
.when(!self.editing, |this| {
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::input::{Input, InputEvent, InputState};
|
||||
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
|
||||
use gpui_kit::component::scroll::ScrollableElement;
|
||||
use gpui_kit::component::tab::{Tab, TabBar};
|
||||
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{Error, anyhow};
|
||||
@@ -15,6 +7,14 @@ use gpui::{
|
||||
ParentElement, Render, SharedString, Stateful, Styled, Subscription, Task, TextAlign,
|
||||
WeakEntity, Window, div, rems,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::input::{Input, InputEvent, InputState};
|
||||
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
|
||||
use gpui_kit::component::scroll::ScrollableElement;
|
||||
use gpui_kit::component::tab::{Tab, TabBar};
|
||||
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
|
||||
use instant::Duration;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
@@ -119,7 +119,7 @@ impl RelayManager {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.child(Input::new(&self.gossip.input).small().cleanable(true))
|
||||
.child(Input::new(&self.gossip.input).cleanable(true))
|
||||
.child(
|
||||
Button::new("metadata")
|
||||
.map(|this| {
|
||||
@@ -208,7 +208,7 @@ impl RelayManager {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.child(Input::new(&self.inbox.input).small().cleanable(true))
|
||||
.child(Input::new(&self.inbox.input).cleanable(true))
|
||||
.child(
|
||||
Button::new("add-inbox")
|
||||
.icon(IconName::Plus)
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
use gpui::http_client::Url;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, IntoElement, ParentElement, PathPromptOptions, Render,
|
||||
SharedString, Styled, Window, div,
|
||||
Styled, Subscription, Window, div,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::form::field;
|
||||
use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants};
|
||||
use gpui_kit::component::input::{Input, InputState};
|
||||
use gpui_kit::component::input::{Input, InputEvent, InputState, NumberInput};
|
||||
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
|
||||
use gpui_kit::component::notification::Notification;
|
||||
use gpui_kit::component::separator::Separator;
|
||||
use gpui_kit::component::switch::Switch;
|
||||
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
|
||||
use settings::AppSettings;
|
||||
use settings::AppearanceMode;
|
||||
use gpui_kit::component::{Disableable, Sizable, WindowExt, h_flex, v_flex};
|
||||
use settings::{AppSettings, AppearanceMode};
|
||||
use ui::{settings_block, settings_row};
|
||||
|
||||
/// Bounds for the theme number inputs
|
||||
const FONT_SIZE_RANGE: (f32, f32) = (10., 32.);
|
||||
const RADIUS_RANGE: (f32, f32) = (0., 24.);
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
||||
cx.new(|cx| Preferences::new(window, cx))
|
||||
@@ -22,6 +24,11 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
||||
|
||||
pub struct Preferences {
|
||||
file_input: Entity<InputState>,
|
||||
font_size_input: Entity<InputState>,
|
||||
radius_input: Entity<InputState>,
|
||||
radius_lg_input: Entity<InputState>,
|
||||
/// Keeps the control subscriptions alive while the dialog is open
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl Preferences {
|
||||
@@ -33,7 +40,91 @@ impl Preferences {
|
||||
.placeholder("https://myblossom.com")
|
||||
});
|
||||
|
||||
Self { file_input }
|
||||
let theme = AppSettings::get_theme(cx);
|
||||
let font_size_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.default_value(theme.font_size.to_string())
|
||||
.step(1.)
|
||||
.min(FONT_SIZE_RANGE.0 as f64)
|
||||
.max(FONT_SIZE_RANGE.1 as f64)
|
||||
});
|
||||
let radius_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.default_value(
|
||||
theme
|
||||
.radius
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.placeholder("System")
|
||||
.step(1.)
|
||||
.min(RADIUS_RANGE.0 as f64)
|
||||
.max(RADIUS_RANGE.1 as f64)
|
||||
});
|
||||
let radius_lg_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.default_value(
|
||||
theme
|
||||
.radius_lg
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.placeholder("System")
|
||||
.step(1.)
|
||||
.min(RADIUS_RANGE.0 as f64)
|
||||
.max(RADIUS_RANGE.1 as f64)
|
||||
});
|
||||
|
||||
let subscriptions = vec![
|
||||
Self::wire_number_input(&font_size_input, FONT_SIZE_RANGE, cx, |value, cx| {
|
||||
if let Some(font_size) = value {
|
||||
AppSettings::update_theme(|theme| theme.font_size = font_size, cx);
|
||||
AppSettings::apply_theme_style(cx);
|
||||
}
|
||||
}),
|
||||
Self::wire_number_input(&radius_input, RADIUS_RANGE, cx, |value, cx| {
|
||||
AppSettings::update_theme(|theme| theme.radius = value, cx);
|
||||
AppSettings::apply_theme_style(cx);
|
||||
}),
|
||||
Self::wire_number_input(&radius_lg_input, RADIUS_RANGE, cx, |value, cx| {
|
||||
AppSettings::update_theme(|theme| theme.radius_lg = value, cx);
|
||||
AppSettings::apply_theme_style(cx);
|
||||
}),
|
||||
];
|
||||
|
||||
Self {
|
||||
file_input,
|
||||
font_size_input,
|
||||
radius_input,
|
||||
radius_lg_input,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist a theme number as it changes; an empty value stores `None`
|
||||
fn wire_number_input(
|
||||
state: &Entity<InputState>,
|
||||
range: (f32, f32),
|
||||
cx: &mut Context<Self>,
|
||||
persist: impl Fn(Option<f32>, &mut App) + 'static,
|
||||
) -> Subscription {
|
||||
let (min, max) = range;
|
||||
let state = state.clone();
|
||||
|
||||
cx.subscribe(&state, move |_, state, event: &InputEvent, cx| {
|
||||
if !matches!(event, InputEvent::Change) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = state
|
||||
.read(cx)
|
||||
.value()
|
||||
.trim()
|
||||
.parse::<f32>()
|
||||
.ok()
|
||||
.map(|value| value.clamp(min, max));
|
||||
persist(value, cx);
|
||||
})
|
||||
}
|
||||
|
||||
/// Update the file server (blossom) URL
|
||||
@@ -90,15 +181,23 @@ impl Render for Preferences {
|
||||
const SCREENING: &str = "Show an screening dialog to verify the unknown sender.";
|
||||
const WOT: &str = "Treat people followed by your contacts as known senders.";
|
||||
const AVATAR: &str = "Hide all avatar pictures to improve performance.";
|
||||
const MODE: &str = "Follow the OS, or always use light or dark.";
|
||||
const APPEARANCE: &str =
|
||||
"Choose whether the app follows the system theme or uses a light/dark theme.";
|
||||
const FONT_SIZE: &str = "The base font size for the interface.";
|
||||
const RADIUS: &str = "Corner radius for UI elements. Empty follows the OS theme.";
|
||||
const RADIUS_LG: &str =
|
||||
"Corner radius for large elements like dialogs. Empty follows the OS theme.";
|
||||
const FOCUS_RING: &str = "Draw a ring around focused controls.";
|
||||
const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages.";
|
||||
const BACKGROUND: &str = "Show an image behind chat and group panels.";
|
||||
const MEDIA: &str = "Server for media attachments. Only blossom services are supported.";
|
||||
|
||||
let screening = AppSettings::get_screening(cx);
|
||||
let wot_auto_trust = AppSettings::get_wot_auto_trust(cx);
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
let nip4e = AppSettings::get_nip4e(cx);
|
||||
let appearance_mode = AppSettings::get_appearance_mode(cx);
|
||||
let theme = AppSettings::get_theme(cx);
|
||||
let panel_background = AppSettings::get_panel_background(cx);
|
||||
let panel_background_path = AppSettings::get_panel_background_path(cx);
|
||||
|
||||
@@ -108,185 +207,175 @@ impl Render for Preferences {
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_4()
|
||||
.w_full()
|
||||
.child(
|
||||
GroupBox::new()
|
||||
.id("general")
|
||||
.title("General")
|
||||
.fill()
|
||||
.child(field().label("Screening").description(SCREENING).child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.child(settings_row(
|
||||
"Screening",
|
||||
SCREENING,
|
||||
Switch::new("screening").checked(screening).on_click(
|
||||
move |_, _window, cx| {
|
||||
AppSettings::update_screening(!screening, cx);
|
||||
},
|
||||
),
|
||||
cx,
|
||||
))
|
||||
.child(
|
||||
field().label("Web of Trust").description(WOT).child(
|
||||
Switch::new("wot-auto-trust")
|
||||
.checked(wot_auto_trust)
|
||||
.on_click(move |_, _window, cx| {
|
||||
AppSettings::update_wot_auto_trust(!wot_auto_trust, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(field().label("Hide user avatar").description(AVATAR).child(
|
||||
.child(settings_row(
|
||||
"Web of Trust",
|
||||
WOT,
|
||||
Switch::new("wot-auto-trust")
|
||||
.checked(wot_auto_trust)
|
||||
.on_click(move |_, _window, cx| {
|
||||
AppSettings::update_wot_auto_trust(!wot_auto_trust, cx);
|
||||
}),
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Hide user avatar",
|
||||
AVATAR,
|
||||
Switch::new("avatar").checked(hide_avatar).on_click(
|
||||
move |_, _window, cx| {
|
||||
AppSettings::update_hide_avatar(!hide_avatar, cx);
|
||||
},
|
||||
),
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(Separator::horizontal())
|
||||
.child(settings_row(
|
||||
"Appearance",
|
||||
APPEARANCE,
|
||||
Button::new("appearance-mode")
|
||||
.label(appearance_mode.label())
|
||||
.ghost()
|
||||
.small()
|
||||
.dropdown_menu(|this, _window, _cx| {
|
||||
AppearanceMode::ALL.iter().fold(this, |menu, mode| {
|
||||
menu.item(PopupMenuItem::new(mode.label()).on_click(
|
||||
move |_, _window, cx| {
|
||||
Self::set_appearance_mode(*mode, cx);
|
||||
},
|
||||
))
|
||||
})
|
||||
}),
|
||||
cx,
|
||||
))
|
||||
.child(Separator::horizontal())
|
||||
.child(
|
||||
GroupBox::new()
|
||||
.id("appearance")
|
||||
.title("Appearance")
|
||||
.fill()
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.child(settings_row(
|
||||
"Font Size",
|
||||
FONT_SIZE,
|
||||
NumberInput::new(&self.font_size_input).w_24(),
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Corner Radius",
|
||||
RADIUS,
|
||||
NumberInput::new(&self.radius_input).w_24(),
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Large Corner Radius",
|
||||
RADIUS_LG,
|
||||
NumberInput::new(&self.radius_lg_input).w_24(),
|
||||
cx,
|
||||
))
|
||||
.child(settings_row(
|
||||
"Focus Ring",
|
||||
FOCUS_RING,
|
||||
Switch::new("focus-ring")
|
||||
.checked(theme.focus_ring)
|
||||
.on_click(move |_, _window, cx| {
|
||||
AppSettings::update_theme(
|
||||
|theme| theme.focus_ring = !theme.focus_ring,
|
||||
cx,
|
||||
);
|
||||
AppSettings::apply_theme_style(cx);
|
||||
}),
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(Separator::horizontal())
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.child(settings_row(
|
||||
"Panel background",
|
||||
BACKGROUND,
|
||||
Switch::new("panel-background")
|
||||
.checked(panel_background)
|
||||
.on_click(move |_, _window, cx| {
|
||||
AppSettings::update_panel_background(!panel_background, cx);
|
||||
cx.refresh_windows();
|
||||
}),
|
||||
cx,
|
||||
))
|
||||
.child(settings_block(
|
||||
"Image",
|
||||
background_label,
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.justify_between()
|
||||
.justify_end()
|
||||
.gap_1()
|
||||
.child(
|
||||
v_flex()
|
||||
.child(div().text_xs().child(SharedString::from("Mode")))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(MODE)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("appearance-mode")
|
||||
.label(appearance_mode.label())
|
||||
Button::new("choose-background")
|
||||
.label("Choose image")
|
||||
.ghost()
|
||||
.small()
|
||||
.dropdown_menu(|this, _window, _cx| {
|
||||
AppearanceMode::ALL.iter().fold(this, |menu, mode| {
|
||||
menu.item(PopupMenuItem::new(mode.label()).on_click(
|
||||
move |_, _window, cx| {
|
||||
Self::set_appearance_mode(*mode, cx);
|
||||
},
|
||||
))
|
||||
})
|
||||
.disabled(!panel_background)
|
||||
.on_click(|_, _window, cx| {
|
||||
Self::choose_background(cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
GroupBox::new()
|
||||
.id("background")
|
||||
.title("Background")
|
||||
.fill()
|
||||
.child(
|
||||
field()
|
||||
.label("Panel background")
|
||||
.description(BACKGROUND)
|
||||
)
|
||||
.child(
|
||||
Switch::new("panel-background")
|
||||
.checked(panel_background)
|
||||
.on_click(move |_, _window, cx| {
|
||||
AppSettings::update_panel_background(!panel_background, cx);
|
||||
Button::new("reset-background")
|
||||
.label("Reset")
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(!panel_background || panel_background_path.is_none())
|
||||
.on_click(|_, _window, cx| {
|
||||
AppSettings::update_panel_background_path(None, cx);
|
||||
cx.refresh_windows();
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(div().text_xs().child(SharedString::from("Image")))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.truncate()
|
||||
.child(SharedString::from(background_label)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("choose-background")
|
||||
.label("Choose image")
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(!panel_background)
|
||||
.on_click(|_, _window, cx| {
|
||||
Self::choose_background(cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset-background")
|
||||
.label("Reset")
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(
|
||||
!panel_background
|
||||
|| panel_background_path.is_none(),
|
||||
)
|
||||
.on_click(|_, _window, cx| {
|
||||
AppSettings::update_panel_background_path(None, cx);
|
||||
cx.refresh_windows();
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
GroupBox::new()
|
||||
.id("experiments")
|
||||
.title("Experiments")
|
||||
.fill()
|
||||
.child(Separator::horizontal())
|
||||
.child(settings_row(
|
||||
"Decoupling Encryption Key",
|
||||
NIP4E,
|
||||
Switch::new("nip4e")
|
||||
.checked(nip4e)
|
||||
.on_click(move |_, _window, cx| {
|
||||
AppSettings::update_nip4e(!nip4e, cx);
|
||||
}),
|
||||
cx,
|
||||
))
|
||||
.child(Separator::horizontal())
|
||||
.child(settings_block(
|
||||
"Media Upload Service",
|
||||
MEDIA,
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(div().flex_1().child(Input::new(&self.file_input)))
|
||||
.child(
|
||||
field()
|
||||
.label("Decoupling Encryption Key")
|
||||
.description(NIP4E)
|
||||
.child(Switch::new("nip4e").checked(nip4e).on_click(
|
||||
move |_, _window, cx| {
|
||||
AppSettings::update_nip4e(!nip4e, cx);
|
||||
},
|
||||
)),
|
||||
Button::new("update-file-server")
|
||||
.icon(IconName::Check)
|
||||
.ghost()
|
||||
.size_8()
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
this.update_file_server(window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
GroupBox::new()
|
||||
.id("media")
|
||||
.title("Media Upload Service")
|
||||
.fill()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Input::new(&self.file_input).small())
|
||||
.child(
|
||||
Button::new("update-file-server")
|
||||
.icon(IconName::Check)
|
||||
.ghost()
|
||||
.size_8()
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
this.update_file_server(window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_medium()
|
||||
.italic()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from("Only support blossom service")),
|
||||
),
|
||||
),
|
||||
)
|
||||
cx,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,7 +358,7 @@ impl CoopTabGroupSkin {
|
||||
this
|
||||
} else {
|
||||
this.text_color(cx.theme().tab_foreground)
|
||||
.bg(cx.theme().tab)
|
||||
.bg(cx.theme().list_hover)
|
||||
}
|
||||
})
|
||||
.selected(active)
|
||||
|
||||
@@ -275,7 +275,7 @@ impl Workspace {
|
||||
let view = settings::init(window, cx);
|
||||
|
||||
window.open_dialog(cx, move |this, _window, _cx| {
|
||||
this.width(px(520.))
|
||||
this.width(px(650.))
|
||||
.close_button(true)
|
||||
.pb_2()
|
||||
.title("Preferences")
|
||||
|
||||
@@ -6,7 +6,7 @@ use gpui::{
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
|
||||
Styled, Window, div,
|
||||
};
|
||||
use gpui_kit::component::{ActiveTheme, Selectable, StyledExt as _, h_flex, v_flex};
|
||||
use gpui_kit::component::{ActiveTheme, Selectable, StyledExt as _, h_flex};
|
||||
|
||||
/// A single navigation row, such as an entry in a sidebar list.
|
||||
#[derive(IntoElement)]
|
||||
@@ -19,6 +19,7 @@ pub struct Nav {
|
||||
suffix: Option<AnyElement>,
|
||||
selected: bool,
|
||||
clickable: bool,
|
||||
center_prefix: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
}
|
||||
@@ -38,6 +39,7 @@ impl Nav {
|
||||
suffix: None,
|
||||
selected: false,
|
||||
clickable: false,
|
||||
center_prefix: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
@@ -48,6 +50,12 @@ impl Nav {
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether to center the prefix in the avatar-width column.
|
||||
pub fn center_prefix(mut self, center: bool) -> Self {
|
||||
self.center_prefix = center;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element shown before the label, such as an avatar or icon.
|
||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||
self.prefix = Some(prefix.into_any_element());
|
||||
@@ -101,41 +109,34 @@ impl Styled for Nav {
|
||||
impl RenderOnce for Nav {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let clickable = self.on_click.is_some() || self.clickable;
|
||||
let text_color = if self.selected {
|
||||
cx.theme().foreground
|
||||
} else {
|
||||
cx.theme().muted_foreground
|
||||
};
|
||||
|
||||
v_flex().h_10().w_full().justify_center().child(
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.h_9()
|
||||
.w_full()
|
||||
.px_1p5()
|
||||
.gap_1p5()
|
||||
.rounded(cx.theme().radius)
|
||||
.text_color(text_color)
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
.child(
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.h_8()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.rounded(cx.theme().radius)
|
||||
.when_some(self.prefix, |this, prefix| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(div().flex_1().min_w_0().truncate().child(self.label))
|
||||
.when_some(self.suffix, |this, suffix| {
|
||||
this.child(div().flex_shrink_0().child(suffix))
|
||||
}),
|
||||
.flex_shrink_0()
|
||||
.when(self.center_prefix, |this| this.w_6().justify_center())
|
||||
.child(prefix),
|
||||
)
|
||||
.when(clickable, |this| {
|
||||
this.cursor_pointer()
|
||||
.hover(|this| this.bg(cx.theme().accent))
|
||||
.when(self.selected, |this| this.bg(cx.theme().list_active))
|
||||
})
|
||||
.when_some(self.on_click, |this, handler| {
|
||||
this.on_click(move |event, window, cx| handler(event, window, cx))
|
||||
})
|
||||
.refine_style(&self.style),
|
||||
)
|
||||
})
|
||||
.child(div().flex_1().min_w_0().truncate().child(self.label))
|
||||
.when_some(self.suffix, |this, suffix| {
|
||||
this.child(div().flex_shrink_0().child(suffix))
|
||||
})
|
||||
.when(clickable, |this| {
|
||||
this.cursor_pointer()
|
||||
.hover(|this| this.bg(cx.theme().list_hover))
|
||||
.when(self.selected, |this| this.bg(cx.theme().list_active))
|
||||
})
|
||||
.when_some(self.on_click, |this, handler| {
|
||||
this.on_click(move |event, window, cx| handler(event, window, cx))
|
||||
})
|
||||
.refine_style(&self.style)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ impl Render for BackupPanel {
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from("Public Key:")),
|
||||
)
|
||||
.child(Input::new(&self.npub_input).small().disabled(true)),
|
||||
.child(Input::new(&self.npub_input).disabled(true)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
@@ -171,7 +171,7 @@ impl Render for BackupPanel {
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from("Secret Key:")),
|
||||
)
|
||||
.child(Input::new(&self.nsec_input).small().disabled(true)),
|
||||
.child(Input::new(&self.nsec_input).disabled(true)),
|
||||
)
|
||||
.child(
|
||||
Button::new("copy")
|
||||
|
||||
@@ -297,7 +297,7 @@ impl BrowsePanel {
|
||||
.px_1()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().sidebar)
|
||||
.child(Input::new(&self.input).appearance(false).small().flex_1())
|
||||
.child(Input::new(&self.input).appearance(false).flex_1())
|
||||
.child(
|
||||
Button::new("relay-browse")
|
||||
.icon(IconName::ArrowRight)
|
||||
|
||||
@@ -337,7 +337,7 @@ impl Render for ContactListPanel {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.child(Input::new(&self.input).small().cleanable(true))
|
||||
.child(Input::new(&self.input).cleanable(true))
|
||||
.child(
|
||||
Button::new("add")
|
||||
.icon(IconName::Plus)
|
||||
|
||||
@@ -13,7 +13,6 @@ use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
|
||||
use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants};
|
||||
use gpui_kit::component::scroll::ScrollableElement;
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
|
||||
use nostr_sdk::prelude::*;
|
||||
@@ -232,7 +231,7 @@ impl GreeterPanel {
|
||||
.gap_2()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.hover(|this| this.bg(cx.theme().primary))
|
||||
.hover(|this| this.bg(cx.theme().list_hover))
|
||||
.on_click({
|
||||
let room = room.clone();
|
||||
let dock = dock.clone();
|
||||
@@ -245,13 +244,13 @@ impl GreeterPanel {
|
||||
.seed(seed)
|
||||
.small(),
|
||||
)
|
||||
.child(div().min_w_0().truncate().text_xs().child(name))
|
||||
.child(div().min_w_0().truncate().text_sm().child(name))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.italic()
|
||||
.child(preview),
|
||||
@@ -302,11 +301,24 @@ impl GreeterPanel {
|
||||
.collect()
|
||||
};
|
||||
|
||||
GroupBox::new()
|
||||
.id("requests")
|
||||
.title("Requests")
|
||||
.fill()
|
||||
.child(v_flex().gap_0().w_full().children(rows))
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.child("Requests"),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.bg(cx.theme().group_box)
|
||||
.text_color(cx.theme().group_box_foreground)
|
||||
.children(rows),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -338,7 +350,7 @@ impl GreeterPanel {
|
||||
.gap_2()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.hover(|this| this.bg(cx.theme().primary))
|
||||
.hover(|this| this.bg(cx.theme().list_hover))
|
||||
.on_click({
|
||||
let room = room.clone();
|
||||
let dock = dock.clone();
|
||||
@@ -351,13 +363,13 @@ impl GreeterPanel {
|
||||
.seed(seed)
|
||||
.small(),
|
||||
)
|
||||
.child(div().min_w_0().truncate().text_xs().child(name))
|
||||
.child(div().min_w_0().truncate().text_sm().child(name))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.italic()
|
||||
.child(preview),
|
||||
@@ -408,11 +420,24 @@ impl GreeterPanel {
|
||||
.collect();
|
||||
|
||||
Some(
|
||||
GroupBox::new()
|
||||
.id("follow-ups")
|
||||
.title("Follow ups")
|
||||
.fill()
|
||||
.child(v_flex().gap_0().w_full().children(rows))
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.child("Follow ups"),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.bg(cx.theme().group_box)
|
||||
.text_color(cx.theme().group_box_foreground)
|
||||
.children(rows),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
@@ -433,13 +458,9 @@ impl GreeterPanel {
|
||||
.w_full()
|
||||
.child(
|
||||
Nav::new("browse-group")
|
||||
.prefix(
|
||||
Icon::new(IconName::Compass)
|
||||
.small()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
.prefix(Icon::new(IconName::Compass))
|
||||
.label("Browse Public Groups")
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let panel = browse::init(this.dock.clone(), window, cx);
|
||||
add_panel_to(
|
||||
@@ -453,9 +474,9 @@ impl GreeterPanel {
|
||||
)
|
||||
.child(
|
||||
Nav::new("browse-posts")
|
||||
.prefix(Icon::new(IconName::Link).small())
|
||||
.prefix(Icon::new(IconName::Link))
|
||||
.label("Browse Nostr via Jumble")
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.on_click(|_, _, cx| {
|
||||
cx.open_url(JUMBLE_URL);
|
||||
}),
|
||||
|
||||
@@ -4,18 +4,16 @@ use std::rc::Rc;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, ClipboardItem, Context, Entity, InteractiveElement as _, IntoElement,
|
||||
ParentElement, SharedString, Styled, WeakEntity, div, relative, uniform_list,
|
||||
ParentElement, SharedString, Styled, WeakEntity, div, px, relative, uniform_list,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
|
||||
use gpui_kit::component::scroll::Scrollbar;
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
|
||||
use nip29::Group;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use state::NostrRegistry;
|
||||
use ui::Avatar;
|
||||
use ui::{Avatar, IconName};
|
||||
|
||||
use super::Sidebar;
|
||||
use crate::dialogs::profile;
|
||||
@@ -147,12 +145,11 @@ impl Sidebar {
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.px_2()
|
||||
.px_4()
|
||||
.flex_shrink_0()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_0p5()
|
||||
.gap_1()
|
||||
.flex_shrink_1()
|
||||
.child(Avatar::new(picture).seed(seed.clone()).small())
|
||||
@@ -164,13 +161,13 @@ impl Sidebar {
|
||||
div()
|
||||
.text_xs()
|
||||
.truncate()
|
||||
.line_height(relative(1.2))
|
||||
.line_height(relative(1.125))
|
||||
.font_semibold()
|
||||
.child(name),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_size(px(8.0))
|
||||
.font_medium()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(total_members),
|
||||
@@ -184,28 +181,17 @@ impl Sidebar {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.min_h_0()
|
||||
.flex_1()
|
||||
.child(
|
||||
uniform_list(
|
||||
"group-rows",
|
||||
rows.len(),
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
this.render_group_rows(
|
||||
range,
|
||||
rows.as_slice(),
|
||||
&member_group,
|
||||
admin,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.track_scroll(&self.group_scroll)
|
||||
.h_full()
|
||||
.px_2(),
|
||||
div().min_h_0().flex_1().child(
|
||||
uniform_list(
|
||||
"group-rows",
|
||||
rows.len(),
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
this.render_group_rows(range, rows.as_slice(), &member_group, admin, cx)
|
||||
}),
|
||||
)
|
||||
.child(Scrollbar::vertical(&self.group_scroll)),
|
||||
.h_full()
|
||||
.px_2(),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -236,9 +222,9 @@ impl Sidebar {
|
||||
GroupSection::Members => self.members_open,
|
||||
};
|
||||
let icon = if open {
|
||||
IconName::ChevronDown
|
||||
IconName::CaretDown
|
||||
} else {
|
||||
IconName::ChevronRight
|
||||
IconName::CaretRight
|
||||
};
|
||||
let label = section.label();
|
||||
|
||||
@@ -285,8 +271,7 @@ impl Sidebar {
|
||||
|
||||
let nav = Nav::new(public_key.to_hex())
|
||||
.label(person.name())
|
||||
.text_xs()
|
||||
.font_medium()
|
||||
.text_sm()
|
||||
.clickable(true)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
@@ -298,6 +283,7 @@ impl Sidebar {
|
||||
.when(!roles.is_empty(), |this| {
|
||||
this.suffix(
|
||||
div()
|
||||
.font_normal()
|
||||
.truncate()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
|
||||
@@ -2,18 +2,22 @@ use auto_update::AutoUpdater;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Div, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||
Stateful, Styled, div, px,
|
||||
Stateful, Styled, Window, div, px,
|
||||
};
|
||||
use gpui_kit::component::button::{Button, ButtonVariant, ButtonVariants};
|
||||
use gpui_kit::component::dialog::DialogButtonProps;
|
||||
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
|
||||
use gpui_kit::component::{
|
||||
ActiveTheme, Disableable, Icon, Selectable, Sizable, WindowExt, h_flex, v_flex,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::menu::DropdownMenu;
|
||||
use gpui_kit::component::{Disableable, Selectable, Sizable, h_flex};
|
||||
use nostr_sdk::prelude::*;
|
||||
use ui::Avatar;
|
||||
use state::NostrRegistry;
|
||||
use ui::{Avatar, IconName};
|
||||
|
||||
use super::Sidebar;
|
||||
use crate::Command;
|
||||
use crate::dock::TAB_BAR_HEIGHT;
|
||||
use crate::nav::Nav;
|
||||
|
||||
impl Sidebar {
|
||||
pub(super) fn render_user(
|
||||
@@ -30,93 +34,98 @@ impl Sidebar {
|
||||
.w_full()
|
||||
.h(TAB_BAR_HEIGHT)
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.px_2()
|
||||
.when(cfg!(target_os = "macos"), |this| this.pl(px(80.)))
|
||||
.child(
|
||||
Button::new("current-user")
|
||||
.child(
|
||||
Avatar::new(avatar.clone())
|
||||
.seed(profile.avatar_seed())
|
||||
.small(),
|
||||
)
|
||||
.small()
|
||||
.dropdown_caret(true)
|
||||
.compact()
|
||||
.text()
|
||||
.dropdown_caret(true)
|
||||
.min_w_0()
|
||||
.flex_shrink_1()
|
||||
.child(Avatar::new(avatar).seed(profile.avatar_seed()).small())
|
||||
.dropdown_menu(move |this, _window, _cx| {
|
||||
this.menu_with_icon(
|
||||
"Profile",
|
||||
IconName::UserRound,
|
||||
IconName::Profile,
|
||||
Box::new(Command::ShowProfile),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Relays",
|
||||
IconName::RadioTower,
|
||||
Box::new(Command::ShowRelayList),
|
||||
)
|
||||
.menu_with_icon("Relays", IconName::Relay, Box::new(Command::ShowRelayList))
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"Settings",
|
||||
IconName::Settings,
|
||||
Box::new(Command::ShowSettings),
|
||||
.item(
|
||||
PopupMenuItem::element(|_, cx| {
|
||||
div().text_color(cx.theme().danger).child("Log out")
|
||||
})
|
||||
.on_click(|_, window, cx| {
|
||||
open_logout_dialog(window, cx);
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.when_some(updater, |this, updater| {
|
||||
this.child(self.render_updater(updater, cx))
|
||||
})
|
||||
.child(
|
||||
Button::new("sidebar-search")
|
||||
.icon(IconName::Search)
|
||||
.tooltip("Search")
|
||||
.selected(self.search_open)
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.toggle_search(window, cx);
|
||||
})),
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when_some(updater, |this, updater| {
|
||||
this.child(self.render_updater(updater, cx))
|
||||
})
|
||||
.child(
|
||||
Button::new("sidebar-search")
|
||||
.icon(IconName::Search)
|
||||
.tooltip("Search")
|
||||
.selected(self.search_open)
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.toggle_search(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("sidebar-new")
|
||||
.icon(IconName::PlusChat)
|
||||
.tooltip("New")
|
||||
.ghost()
|
||||
.small()
|
||||
.dropdown_menu(|menu, _window, _cx| {
|
||||
menu.label("Public Group")
|
||||
.menu("New Group", Box::new(Command::NewGroup))
|
||||
.menu("Join Group", Box::new(Command::JoinGroup))
|
||||
.separator()
|
||||
.menu("New Chat", Box::new(Command::NewChat))
|
||||
}),
|
||||
)
|
||||
.when(self.group_open, |this| {
|
||||
this.child(
|
||||
Button::new("sidebar-back")
|
||||
.icon(IconName::ArrowLeft)
|
||||
.tooltip("Back")
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.hide_group(cx);
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// The bottom navigation, pinned below the list like signed's sidebar.
|
||||
pub(super) fn render_footer(&self, _cx: &mut Context<Self>) -> Div {
|
||||
v_flex()
|
||||
.p_2()
|
||||
.flex_shrink_0()
|
||||
.gap_1()
|
||||
.items_start()
|
||||
.justify_start()
|
||||
.child(
|
||||
Button::new("sidebar-new")
|
||||
.icon(IconName::MessageSquarePlus)
|
||||
.tooltip("New")
|
||||
.ghost()
|
||||
.small()
|
||||
.dropdown_menu(|menu, _window, _cx| {
|
||||
menu.label("Public Group")
|
||||
.menu_with_icon(
|
||||
"New Group",
|
||||
IconName::Users,
|
||||
Box::new(Command::NewGroup),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Join Group",
|
||||
IconName::DoorOpen,
|
||||
Box::new(Command::JoinGroup),
|
||||
)
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"New Chat",
|
||||
IconName::MessageSquare,
|
||||
Box::new(Command::NewChat),
|
||||
)
|
||||
Nav::new("sidebar-settings")
|
||||
.prefix(Icon::new(IconName::Settings).small())
|
||||
.center_prefix(true)
|
||||
.label("Settings")
|
||||
.text_sm()
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::ShowSettings), cx)
|
||||
}),
|
||||
)
|
||||
.when(self.group_open, |this| {
|
||||
this.child(
|
||||
Button::new("sidebar-back")
|
||||
.icon(IconName::ArrowLeft)
|
||||
.tooltip("Back")
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.hide_group(cx);
|
||||
})),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn render_updater(&self, updater: Entity<AutoUpdater>, cx: &mut App) -> AnyElement {
|
||||
@@ -129,7 +138,7 @@ impl Sidebar {
|
||||
.when(!up_to_date, |this| {
|
||||
this.child(
|
||||
Button::new("update")
|
||||
.icon(IconName::CircleArrowDown)
|
||||
.icon(IconName::ArrowDownCircle)
|
||||
.tooltip(status)
|
||||
.small()
|
||||
.ghost()
|
||||
@@ -139,7 +148,7 @@ impl Sidebar {
|
||||
.when(staged, |this| {
|
||||
this.child(
|
||||
Button::new("restart")
|
||||
.icon(IconName::CircleArrowDown)
|
||||
.icon(IconName::ArrowDownCircle)
|
||||
.tooltip("Quit and relaunch into the installed update")
|
||||
.small()
|
||||
.ghost()
|
||||
@@ -153,3 +162,25 @@ impl Sidebar {
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
/// Confirm signing out, then drop the stored credential like signed's exit dialog.
|
||||
fn open_logout_dialog(window: &mut Window, cx: &mut App) {
|
||||
const DESCRIPTION: &str = "Are you sure you want to log out? You will need to re-enter your credentials to sign back in.";
|
||||
|
||||
window.open_alert_dialog(cx, |alert, _, _| {
|
||||
alert
|
||||
.title("Log Out?")
|
||||
.description(DESCRIPTION)
|
||||
.button_props(
|
||||
DialogButtonProps::default()
|
||||
.ok_text("Log out")
|
||||
.ok_variant(ButtonVariant::Danger)
|
||||
.show_cancel(true),
|
||||
)
|
||||
.on_ok(|_, _, cx| {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
nostr.update(cx, |nostr, cx| nostr.sign_out(cx));
|
||||
true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -193,9 +193,8 @@ impl Sidebar {
|
||||
.h_20()
|
||||
.w_full()
|
||||
.px_4()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().background)
|
||||
.bg(cx.theme().sidebar)
|
||||
.text_color(cx.theme().sidebar_foreground)
|
||||
.child(
|
||||
Button::new("create-chat")
|
||||
.label(if self.selected_contacts.len() > 1 {
|
||||
@@ -213,7 +212,9 @@ impl Sidebar {
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Selected")
|
||||
.text_center()
|
||||
.justify_center()
|
||||
.child("Selected:")
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
|
||||
@@ -5,12 +5,11 @@ use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled, div, uniform_list,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::scroll::Scrollbar;
|
||||
use gpui_kit::component::{ActiveTheme, Icon, h_flex, v_flex};
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
|
||||
use nip29::GroupsRegistry;
|
||||
use settings::AppSettings;
|
||||
use ui::IconName;
|
||||
|
||||
use super::Sidebar;
|
||||
use super::rows::SidebarRow;
|
||||
@@ -44,12 +43,12 @@ impl Sidebar {
|
||||
// Whether to show the create button for the contact picker
|
||||
let create_button = contacts_open && !self.selected_contacts.is_empty();
|
||||
|
||||
let (list_id, scroll_handle, list_title) = if contacts_open {
|
||||
("sidebar-contacts", &self.contacts_scroll, Some("Contacts"))
|
||||
let (list_id, title) = if contacts_open {
|
||||
("sidebar-contacts", Some("Contacts"))
|
||||
} else if requests_open {
|
||||
("sidebar-requests", &self.requests_scroll, Some("Requests"))
|
||||
("sidebar-requests", Some("Requests"))
|
||||
} else {
|
||||
("sidebar-list", &self.list_scroll, None)
|
||||
("sidebar-list", None)
|
||||
};
|
||||
|
||||
let request_count = ChatRegistry::global(cx)
|
||||
@@ -61,13 +60,15 @@ impl Sidebar {
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.relative()
|
||||
.gap_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.items_start()
|
||||
.justify_start()
|
||||
.child(
|
||||
Nav::new("browse")
|
||||
.prefix(Icon::new(IconName::Compass))
|
||||
.prefix(Icon::new(IconName::Compass).small())
|
||||
.label("Browse")
|
||||
.text_sm()
|
||||
.on_click(|_, window, cx| {
|
||||
@@ -76,14 +77,13 @@ impl Sidebar {
|
||||
)
|
||||
.child(
|
||||
Nav::new("reqs")
|
||||
.prefix(Icon::new(IconName::UserRoundPlus))
|
||||
.prefix(Icon::new(IconName::Invite).small())
|
||||
.label("Requests")
|
||||
.text_sm()
|
||||
.when(request_count > 0, |this| {
|
||||
this.suffix(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.child(
|
||||
h_flex()
|
||||
.py_0p5()
|
||||
@@ -117,7 +117,7 @@ impl Sidebar {
|
||||
)
|
||||
.child(
|
||||
Nav::new("contacts")
|
||||
.prefix(Icon::new(IconName::Book))
|
||||
.prefix(Icon::new(IconName::Book).small())
|
||||
.label("Contacts")
|
||||
.text_sm()
|
||||
.when(contacts_open, |this| this.bg(cx.theme().list_active))
|
||||
@@ -126,33 +126,30 @@ impl Sidebar {
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when_some(list_title, |this, list_title| {
|
||||
.when_some(title, |this, title| {
|
||||
this.child(
|
||||
div()
|
||||
h_flex()
|
||||
.px_4()
|
||||
.h_10()
|
||||
.w_full()
|
||||
.flex_shrink_0()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(list_title),
|
||||
.child(title),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.min_h_0()
|
||||
.flex_1()
|
||||
.child(
|
||||
uniform_list(
|
||||
list_id,
|
||||
rows.len(),
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
this.render_rows(range, rows.as_slice(), cx)
|
||||
}),
|
||||
)
|
||||
.track_scroll(scroll_handle)
|
||||
.h_full()
|
||||
.px_2(),
|
||||
)
|
||||
.child(Scrollbar::vertical(scroll_handle)),
|
||||
uniform_list(
|
||||
list_id,
|
||||
rows.len(),
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
this.render_rows(range, rows.as_slice(), cx)
|
||||
}),
|
||||
)
|
||||
.min_h_0()
|
||||
.flex_1()
|
||||
.px_2(),
|
||||
)
|
||||
.when(create_button, |this| {
|
||||
this.child(self.render_create_overlay(cx))
|
||||
|
||||
@@ -7,7 +7,7 @@ use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
||||
IntoElement, ObjectFit, ParentElement, Render, SharedString, Styled, StyledImage, Subscription,
|
||||
Task, UniformListScrollHandle, WeakEntity, Window, div, img, retain_all,
|
||||
Task, WeakEntity, Window, div, img, retain_all,
|
||||
};
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::dock::{DockArea, Panel, PanelEvent};
|
||||
@@ -38,7 +38,6 @@ use search::SearchResult;
|
||||
|
||||
pub struct Sidebar {
|
||||
focus_handle: FocusHandle,
|
||||
list_scroll: UniformListScrollHandle,
|
||||
dock: WeakEntity<DockArea>,
|
||||
/// Random background shown behind the signed-out screen.
|
||||
banner: SharedString,
|
||||
@@ -47,8 +46,6 @@ pub struct Sidebar {
|
||||
group: Option<WeakEntity<Group>>,
|
||||
/// Whether the sidebar is showing the group's information instead of its list.
|
||||
group_open: bool,
|
||||
/// Scroll state of the group's member list.
|
||||
group_scroll: UniformListScrollHandle,
|
||||
/// Whether the group view's admins section is expanded.
|
||||
admins_open: bool,
|
||||
/// Whether the group view's members section is expanded.
|
||||
@@ -69,13 +66,10 @@ pub struct Sidebar {
|
||||
directory: Entity<Directory>,
|
||||
/// Contacts selected in the contact picker.
|
||||
selected_contacts: HashSet<PublicKey>,
|
||||
/// Scroll state of the contact picker's list.
|
||||
contacts_scroll: UniformListScrollHandle,
|
||||
|
||||
/// Whether the main list is showing chat requests instead of chats and groups.
|
||||
requests_open: bool,
|
||||
new_requests: bool,
|
||||
requests_scroll: UniformListScrollHandle,
|
||||
|
||||
/// Whether the search bar is shown below the header.
|
||||
search_open: bool,
|
||||
@@ -93,8 +87,6 @@ pub struct Sidebar {
|
||||
global_results: Option<Vec<PublicKey>>,
|
||||
/// The async global profile search.
|
||||
global_task: Option<Task<()>>,
|
||||
/// Scroll state of the search results list.
|
||||
search_scroll: UniformListScrollHandle,
|
||||
|
||||
tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
|
||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||
@@ -208,12 +200,10 @@ impl Sidebar {
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
list_scroll: UniformListScrollHandle::new(),
|
||||
dock,
|
||||
banner: onboarding::pick_banner(),
|
||||
group: None,
|
||||
group_open: false,
|
||||
group_scroll: UniformListScrollHandle::new(),
|
||||
admins_open: true,
|
||||
members_open: true,
|
||||
pins_open: true,
|
||||
@@ -223,9 +213,7 @@ impl Sidebar {
|
||||
contacts: None,
|
||||
directory: Directory::new(cx),
|
||||
selected_contacts: HashSet::new(),
|
||||
contacts_scroll: UniformListScrollHandle::new(),
|
||||
requests_open: false,
|
||||
requests_scroll: UniformListScrollHandle::new(),
|
||||
new_requests: false,
|
||||
search_open: false,
|
||||
global_open: false,
|
||||
@@ -235,7 +223,6 @@ impl Sidebar {
|
||||
global_finding: false,
|
||||
global_results: None,
|
||||
global_task: None,
|
||||
search_scroll: UniformListScrollHandle::new(),
|
||||
tasks: smallvec![],
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
@@ -305,49 +292,58 @@ impl Render for Sidebar {
|
||||
|
||||
let group = self.group.clone().and_then(|group| group.upgrade());
|
||||
let searching = self.search_open && !self.find_input.read(cx).value().is_empty();
|
||||
let picking_contacts = self.contacts_open && !self.requests_open;
|
||||
|
||||
v_flex()
|
||||
.image_cache(retain_all("sidebar"))
|
||||
.size_full()
|
||||
.relative()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.bg(cx.theme().sidebar)
|
||||
.when_some(current_user.as_ref(), |this, current_user| {
|
||||
this.child(title_bar_drag_handlers(
|
||||
self.render_user(current_user, cx),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.when(self.search_open, |this| this.child(self.render_find(cx)))
|
||||
.when(!logged_in, |this| {
|
||||
this.relative()
|
||||
.child(title_bar_drag_handlers(
|
||||
div()
|
||||
.id("onboarding-drag")
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.h(TAB_BAR_HEIGHT)
|
||||
.w_full(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.child(
|
||||
div().absolute().inset_0().child(
|
||||
img(self.banner.clone())
|
||||
.size_full()
|
||||
.object_fit(ObjectFit::Cover),
|
||||
),
|
||||
)
|
||||
.child(onboarding::render_signed_out(window, cx))
|
||||
})
|
||||
.map(|this| match group {
|
||||
Some(group) if self.group_open && !searching => {
|
||||
this.child(self.render_group(group, cx))
|
||||
}
|
||||
_ if searching => this.child(self.render_results(cx)),
|
||||
_ => this.child(self.render_list(cx)),
|
||||
.text_color(cx.theme().sidebar_foreground)
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.when_some(current_user.as_ref(), |this, current_user| {
|
||||
this.child(title_bar_drag_handlers(
|
||||
self.render_user(current_user, cx),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.when(self.search_open, |this| this.child(self.render_find(cx)))
|
||||
.when(!logged_in, |this| {
|
||||
this.child(title_bar_drag_handlers(
|
||||
div()
|
||||
.id("onboarding-drag")
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.h(TAB_BAR_HEIGHT)
|
||||
.w_full(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.child(
|
||||
div().absolute().inset_0().child(
|
||||
img(self.banner.clone())
|
||||
.size_full()
|
||||
.object_fit(ObjectFit::Cover),
|
||||
),
|
||||
)
|
||||
.child(onboarding::render_signed_out(window, cx))
|
||||
})
|
||||
.map(|this| match group {
|
||||
Some(group) if self.group_open && !searching => {
|
||||
this.child(self.render_group(group, cx))
|
||||
}
|
||||
_ if searching => this.child(self.render_results(cx)),
|
||||
_ => this.child(self.render_list(cx)),
|
||||
}),
|
||||
)
|
||||
.when(logged_in && !picking_contacts, |this| {
|
||||
this.child(self.render_footer(cx))
|
||||
})
|
||||
.when(loading && logged_in, |this| {
|
||||
this.child(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use gpui_kit::component::button::{Button, ButtonCustomVariant, ButtonVariants};
|
||||
use gpui_kit::component::{Sizable, v_flex};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, Window, img};
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::v_flex;
|
||||
|
||||
use crate::dialogs::import;
|
||||
|
||||
@@ -16,11 +16,12 @@ pub(super) fn pick_banner() -> SharedString {
|
||||
.map(|elapsed| elapsed.subsec_nanos())
|
||||
.unwrap_or_default();
|
||||
let index = nanos as usize % SIGNED_OUT_BANNERS.len();
|
||||
|
||||
SIGNED_OUT_BANNERS[index].into()
|
||||
}
|
||||
|
||||
/// The headline and identity import shown while signed out.
|
||||
pub(super) fn render_signed_out(_window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
pub(super) fn render_signed_out(_window: &mut Window, _cx: &mut App) -> AnyElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.justify_end()
|
||||
@@ -30,14 +31,7 @@ pub(super) fn render_signed_out(_window: &mut Window, cx: &mut App) -> AnyElemen
|
||||
.child(
|
||||
Button::new("import")
|
||||
.label("Import Identity")
|
||||
.custom(
|
||||
ButtonCustomVariant::new(cx)
|
||||
.color(gpui::white())
|
||||
.foreground(gpui::black())
|
||||
.hover(gpui::white().opacity(0.9))
|
||||
.active(gpui::white().opacity(0.8)),
|
||||
)
|
||||
.large()
|
||||
.primary()
|
||||
.on_click(|_, window, cx| {
|
||||
import::open(window, cx);
|
||||
}),
|
||||
|
||||
@@ -7,16 +7,15 @@ use gpui::{
|
||||
AnyElement, App, Context, Entity, InteractiveElement as _, IntoElement, ParentElement,
|
||||
SharedString, Styled, WeakEntity, Window, div, px,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::dialog::DialogButtonProps;
|
||||
use gpui_kit::component::dock::{ClosePanel, DockArea, DockPlacement, PanelHandle};
|
||||
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Selectable, Sizable, WindowExt, h_flex, v_flex};
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Selectable, Sizable, WindowExt, h_flex};
|
||||
use nip29::Group;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use ui::Avatar;
|
||||
use ui::{Avatar, IconName};
|
||||
|
||||
use super::Sidebar;
|
||||
use super::list::ListSection;
|
||||
@@ -100,7 +99,7 @@ impl Sidebar {
|
||||
.map(|(offset, row)| {
|
||||
let index = range.start + offset;
|
||||
|
||||
match row {
|
||||
let element = match row {
|
||||
SidebarRow::Section(section) => {
|
||||
let section = *section;
|
||||
let label = section.label();
|
||||
@@ -112,9 +111,9 @@ impl Sidebar {
|
||||
};
|
||||
|
||||
let icon = if open {
|
||||
IconName::ChevronDown
|
||||
IconName::CaretDown
|
||||
} else {
|
||||
IconName::ChevronRight
|
||||
IconName::CaretRight
|
||||
};
|
||||
|
||||
Nav::new(label)
|
||||
@@ -151,24 +150,13 @@ impl Sidebar {
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Empty(text) => v_flex()
|
||||
.h_10()
|
||||
SidebarRow::Empty(text) => h_flex()
|
||||
.h_8()
|
||||
.w_full()
|
||||
.px_1p5()
|
||||
.items_center()
|
||||
.child(
|
||||
h_flex()
|
||||
.h_9()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.border_color(cx.theme().border)
|
||||
.rounded(cx.theme().radius)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(*text),
|
||||
)
|
||||
.px_2()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(*text)
|
||||
.into_any_element(),
|
||||
SidebarRow::Room { room } => {
|
||||
let id = room.read(cx).id;
|
||||
@@ -406,7 +394,9 @@ impl Sidebar {
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
};
|
||||
// Keep a small gap between rows so selected rows read as pills.
|
||||
div().pb_0p5().child(element).into_any_element()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::input::Input;
|
||||
use gpui_kit::component::scroll::Scrollbar;
|
||||
use gpui_kit::component::{ActiveTheme, Selectable, Sizable, h_flex, v_flex};
|
||||
use std::collections::HashSet;
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -11,6 +6,10 @@ use gpui::{
|
||||
AnyElement, App, Context, Entity, IntoElement, ParentElement, SharedString, Styled, Task,
|
||||
Window, div, uniform_list,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::input::Input;
|
||||
use gpui_kit::component::{ActiveTheme, Selectable, Sizable, h_flex, v_flex};
|
||||
use nip29::{Group, GroupsRegistry};
|
||||
use nostr_sdk::prelude::*;
|
||||
use state::{FIND_LIMIT, NostrRegistry};
|
||||
@@ -206,6 +205,7 @@ impl Sidebar {
|
||||
|
||||
pub(super) fn render_find(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
h_flex()
|
||||
.mb_2()
|
||||
.px_2()
|
||||
.flex_shrink_0()
|
||||
.w_full()
|
||||
@@ -215,8 +215,8 @@ impl Sidebar {
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().secondary)
|
||||
.child(Input::new(&self.find_input).appearance(false).small())
|
||||
.bg(cx.theme().input_background())
|
||||
.child(Input::new(&self.find_input).appearance(false))
|
||||
.child(
|
||||
Button::new("sidebar-global")
|
||||
.icon(IconName::Compass)
|
||||
@@ -225,7 +225,7 @@ impl Sidebar {
|
||||
} else {
|
||||
"Enable global search"
|
||||
})
|
||||
.small()
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.selected(self.global_open)
|
||||
.loading(self.global_finding)
|
||||
@@ -245,22 +245,17 @@ impl Sidebar {
|
||||
.min_h_0()
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.min_h_0()
|
||||
.flex_1()
|
||||
.child(
|
||||
uniform_list(
|
||||
"sidebar-search",
|
||||
rows.len(),
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
this.render_rows(range, rows.as_slice(), cx)
|
||||
}),
|
||||
)
|
||||
.track_scroll(&self.search_scroll)
|
||||
.h_full()
|
||||
.px_2(),
|
||||
div().min_h_0().flex_1().child(
|
||||
uniform_list(
|
||||
"sidebar-search",
|
||||
rows.len(),
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
this.render_rows(range, rows.as_slice(), cx)
|
||||
}),
|
||||
)
|
||||
.child(Scrollbar::vertical(&self.search_scroll)),
|
||||
.h_full()
|
||||
.px_2(),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ product-name = "Coop"
|
||||
description = "Chat Freely, Stay Private on Nostr"
|
||||
identifier = "su.reya.coop"
|
||||
category = "SocialNetworking"
|
||||
version = "2.0.0-alpha"
|
||||
version = "2.0.0-alpha2"
|
||||
out-dir = "../dist"
|
||||
before-packaging-command = "cargo build --release"
|
||||
resources = ["Cargo.toml", "src"]
|
||||
|
||||
@@ -16,8 +16,6 @@ use workspace::TAB_BAR_HEIGHT;
|
||||
|
||||
actions!(coop, [Quit]);
|
||||
|
||||
const BASE_FONT_SIZE: f32 = 16.0;
|
||||
|
||||
fn main() {
|
||||
// Initialize logging
|
||||
tracing_subscriber::fmt::init();
|
||||
@@ -66,11 +64,13 @@ fn main() {
|
||||
settings::init(cx);
|
||||
|
||||
let appearance = settings::persisted_appearance_mode();
|
||||
match apply_native_theme(appearance, cx) {
|
||||
let font_size = settings::persisted_theme().font_size;
|
||||
match apply_native_theme(appearance, font_size, cx) {
|
||||
Ok(()) => start_theme_watcher(appearance, cx),
|
||||
Err(error) => {
|
||||
log::warn!("native theme unavailable ({error})");
|
||||
apply_appearance(appearance, cx);
|
||||
AppSettings::apply_theme_style(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,22 +171,30 @@ fn quit(_ev: &Quit, cx: &mut App) {
|
||||
cx.quit();
|
||||
}
|
||||
|
||||
/// Apply the native theme to the app, and then apply the persisted appearance mode
|
||||
fn apply_native_theme(appearance: AppearanceMode, cx: &mut App) -> native_theme::Result<()> {
|
||||
/// Apply the native theme to the app, scaled to the user's font size,
|
||||
/// then layer the persisted theme tuning on top
|
||||
fn apply_native_theme(
|
||||
appearance: AppearanceMode,
|
||||
font_size: f32,
|
||||
cx: &mut App,
|
||||
) -> native_theme::Result<()> {
|
||||
let mut system = native_theme_gpui::SystemTheme::from_system()?;
|
||||
let native = system.light.defaults.font.size;
|
||||
|
||||
if native > 0.0 {
|
||||
system.accessibility.text_scaling_factor *= BASE_FONT_SIZE / native;
|
||||
system.accessibility.text_scaling_factor *= font_size / native;
|
||||
}
|
||||
|
||||
apply_system_theme(&system, cx);
|
||||
apply_appearance(appearance, cx);
|
||||
// Applying the native theme replaces the whole theme, so the user's
|
||||
// tuning must be layered on afterwards.
|
||||
AppSettings::apply_theme_style(cx);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Honor the appearance mode against the installed (native) palettes.
|
||||
/// Honor the appearance mode against the installed (native) palettes
|
||||
fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
|
||||
match appearance {
|
||||
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
||||
@@ -222,10 +230,11 @@ fn start_theme_watcher(initial_appearance: AppearanceMode, cx: &mut App) {
|
||||
log::info!("OS theme changed; re-applying");
|
||||
cx.update(|cx| {
|
||||
let appearance = AppSettings::get_appearance_mode(cx);
|
||||
let font_size = AppSettings::get_theme(cx).font_size;
|
||||
if let Some(watcher) = cx.try_global::<ThemeWatcher>() {
|
||||
watcher.appearance.set(appearance);
|
||||
}
|
||||
if let Err(error) = apply_native_theme(appearance, cx) {
|
||||
if let Err(error) = apply_native_theme(appearance, font_size, cx) {
|
||||
log::warn!("failed to apply updated OS theme ({error})");
|
||||
}
|
||||
});
|
||||
|
||||