Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd9f4e2861 | ||
|
|
bebb4cebbf | ||
|
|
49f2d9edc1 | ||
|
|
b560ea4116 | ||
|
|
14b66d530c | ||
|
|
cab58f6332 | ||
|
|
6117c8184c | ||
|
|
d7160e196e | ||
|
|
d0ad1a6f57 | ||
|
|
0d4acf5b65 | ||
|
|
6f35c86afc |
@@ -982,9 +982,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.10.0"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
|
checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
@@ -1224,7 +1224,7 @@ name = "common"
|
|||||||
version = "2.0.0-alpha"
|
version = "2.0.0-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs 5.0.1",
|
"dirs",
|
||||||
"futures",
|
"futures",
|
||||||
"gpui-pre",
|
"gpui-pre",
|
||||||
"instant",
|
"instant",
|
||||||
@@ -1548,9 +1548,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.3"
|
version = "1.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055"
|
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
@@ -1636,9 +1636,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctutils"
|
name = "ctutils"
|
||||||
version = "0.4.5"
|
version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0"
|
checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cmov",
|
"cmov",
|
||||||
]
|
]
|
||||||
@@ -1789,16 +1789,7 @@ version = "5.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs-sys 0.4.1",
|
"dirs-sys",
|
||||||
]
|
|
||||||
|
|
||||||
[[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]]
|
[[package]]
|
||||||
@@ -1809,22 +1800,10 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users 0.4.6",
|
"redox_users",
|
||||||
"windows-sys 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "dispatch"
|
name = "dispatch"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2966,7 +2945,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
"webpki-roots 1.0.11",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3680,7 +3659,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"webpki-roots 1.0.11",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3733,9 +3712,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.4.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd"
|
checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"potential_utf",
|
"potential_utf",
|
||||||
@@ -3747,9 +3726,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_locale_core"
|
name = "icu_locale_core"
|
||||||
version = "2.4.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0"
|
checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"litemap",
|
"litemap",
|
||||||
@@ -3760,9 +3739,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer"
|
name = "icu_normalizer"
|
||||||
version = "2.4.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599"
|
checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"icu_collections",
|
"icu_collections",
|
||||||
"icu_normalizer_data",
|
"icu_normalizer_data",
|
||||||
@@ -3774,15 +3753,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer_data"
|
name = "icu_normalizer_data"
|
||||||
version = "2.4.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc"
|
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties"
|
name = "icu_properties"
|
||||||
version = "2.4.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f"
|
checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"icu_collections",
|
"icu_collections",
|
||||||
@@ -3795,15 +3774,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties_data"
|
name = "icu_properties_data"
|
||||||
version = "2.4.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf"
|
checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_provider"
|
name = "icu_provider"
|
||||||
version = "2.4.0"
|
version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514"
|
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"icu_locale_core",
|
"icu_locale_core",
|
||||||
@@ -4417,9 +4396,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lyon_algorithms"
|
name = "lyon_algorithms"
|
||||||
version = "1.0.22"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3a6b9d4e8b48acd531fbf1241ad9b270df6e60b6b92bdea1469f4f40899d805"
|
checksum = "cdfa8785f95e57914ddb35e3b59994aeba6f5e79e9cfd03da1c269f010f36009"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lyon_path",
|
"lyon_path",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
@@ -4427,9 +4406,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lyon_geom"
|
name = "lyon_geom"
|
||||||
version = "1.0.20"
|
version = "1.0.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d82dd26aa4dc0b5b70b9115de59824842bcb43cf4a28e1b1e6d0ad148c93f93e"
|
checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"euclid",
|
"euclid",
|
||||||
@@ -4448,9 +4427,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lyon_tessellation"
|
name = "lyon_tessellation"
|
||||||
version = "1.0.23"
|
version = "1.0.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd4872650e70dc907dd4f7ebc449d7d657a0d2658e423b2e724970870bbbf75b"
|
checksum = "43b8dcf906637ecef61b3c0740c7a4e7f27caeb31257cfac0cc579ce15be6005"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"float_next_after",
|
"float_next_after",
|
||||||
"lyon_path",
|
"lyon_path",
|
||||||
@@ -4556,9 +4535,9 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memmap2"
|
name = "memmap2"
|
||||||
version = "0.9.12"
|
version = "0.9.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b0ca1155a9ecc25aa12e8313a119f32edf0255fea2f44839c4ac0a12b2ac157"
|
checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@@ -4862,7 +4841,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.45.5"
|
version = "0.45.5"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -4888,7 +4867,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-blossom"
|
name = "nostr-blossom"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bitcoin_hashes 1.2.0",
|
"bitcoin_hashes 1.2.0",
|
||||||
@@ -4901,7 +4880,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.45.2"
|
version = "0.45.2"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4914,7 +4893,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.45.2"
|
version = "0.45.2"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nostr",
|
"nostr",
|
||||||
"opaquerr 0.2.0",
|
"opaquerr 0.2.0",
|
||||||
@@ -4923,7 +4902,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip"
|
name = "nostr-gossip"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nostr",
|
"nostr",
|
||||||
"opaquerr 0.2.0",
|
"opaquerr 0.2.0",
|
||||||
@@ -4932,7 +4911,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip-memory"
|
name = "nostr-gossip-memory"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.14.2",
|
"indexmap 2.14.2",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -4944,7 +4923,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.45.3"
|
version = "0.45.3"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
@@ -4959,7 +4938,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-memory"
|
name = "nostr-memory"
|
||||||
version = "0.45.2"
|
version = "0.45.2"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"btreecap",
|
"btreecap",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4970,7 +4949,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.45.4"
|
version = "0.45.4"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#f7ebc0a88ea27ac4e3d74b941e3a44ddb10b306c"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -5560,9 +5539,9 @@ checksum = "4f933a4265d5cdad61d19bbdfc972ea5726d56cd8d3d57b8f2d3c365dd42bee9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "open"
|
name = "open"
|
||||||
version = "5.4.5"
|
version = "5.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95"
|
checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"is-wsl",
|
"is-wsl",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -6398,16 +6377,6 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"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]]
|
[[package]]
|
||||||
name = "ref-cast"
|
name = "ref-cast"
|
||||||
version = "1.0.27"
|
version = "1.0.27"
|
||||||
@@ -6499,7 +6468,7 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots 1.0.11",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6784,9 +6753,9 @@ checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.16"
|
version = "0.103.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
|
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"ring",
|
"ring",
|
||||||
@@ -7230,7 +7199,7 @@ version = "3.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8"
|
checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs 6.0.0",
|
"dirs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7342,9 +7311,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.16.4"
|
version = "1.16.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8"
|
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smol"
|
name = "smol"
|
||||||
@@ -7365,9 +7334,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smol_str"
|
name = "smol_str"
|
||||||
version = "0.3.7"
|
version = "0.3.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eeb7109d23f319619d4cc0de5464613c8b7b1a09db2774a58101459cf6be2f10"
|
checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
@@ -7853,9 +7822,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.5"
|
version = "0.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a"
|
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
@@ -7869,9 +7838,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.54.1"
|
version = "1.54.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
|
checksum = "a0c168a474b95bfecfe908a65d4bdffc523144b649a74c09c286afb35d53e0bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -8238,13 +8207,10 @@ dependencies = [
|
|||||||
name = "ui"
|
name = "ui"
|
||||||
version = "2.0.0-alpha"
|
version = "2.0.0-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"common",
|
|
||||||
"gpui-base",
|
"gpui-base",
|
||||||
"gpui-kit",
|
"gpui-kit",
|
||||||
"gpui-pre",
|
"gpui-pre",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
|
||||||
"person",
|
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"uuid",
|
"uuid",
|
||||||
"web-time",
|
"web-time",
|
||||||
@@ -8360,9 +8326,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ureq"
|
name = "ureq"
|
||||||
version = "3.5.0"
|
version = "3.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cde93ff05758a36a851a398a772b0b79a82fbe3c186a46f2696baaeaf84c966b"
|
checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.23.1",
|
"base64 0.23.1",
|
||||||
"cookie_store",
|
"cookie_store",
|
||||||
@@ -8375,7 +8341,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"ureq-proto",
|
"ureq-proto",
|
||||||
"utf8-zero",
|
"utf8-zero",
|
||||||
"webpki-roots 1.0.11",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8477,9 +8443,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.29.0"
|
version = "1.28.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514"
|
checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -8773,9 +8739,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webbrowser"
|
name = "webbrowser"
|
||||||
version = "1.2.5"
|
version = "1.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2c8ddfbbf66e501434657400268cefe51b95140a0266236415cda7627732df7"
|
checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jni",
|
"jni",
|
||||||
"log",
|
"log",
|
||||||
@@ -8789,9 +8755,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-root-certs"
|
name = "webpki-root-certs"
|
||||||
version = "1.0.11"
|
version = "1.0.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a"
|
checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
@@ -8802,14 +8768,14 @@ version = "0.26.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"webpki-roots 1.0.11",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "1.0.11"
|
version = "1.0.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171"
|
checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
@@ -9837,9 +9803,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.4"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3"
|
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
"yoke-derive",
|
"yoke-derive",
|
||||||
@@ -9975,7 +9941,7 @@ dependencies = [
|
|||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics 0.24.0",
|
"core-graphics 0.24.0",
|
||||||
"core-text",
|
"core-text",
|
||||||
"dirs 5.0.1",
|
"dirs",
|
||||||
"dwrote",
|
"dwrote",
|
||||||
"float-ord",
|
"float-ord",
|
||||||
"freetype-sys",
|
"freetype-sys",
|
||||||
@@ -10105,9 +10071,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerovec"
|
name = "zerovec"
|
||||||
version = "0.11.9"
|
version = "0.11.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40"
|
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"yoke",
|
"yoke",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
@@ -10116,9 +10082,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerovec-derive"
|
name = "zerovec-derive"
|
||||||
version = "0.11.7"
|
version = "0.11.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a"
|
checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ members = ["crates/*", "desktop", "web"]
|
|||||||
default-members = ["desktop"]
|
default-members = ["desktop"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "2.0.0-alpha2"
|
version = "2.0.0-alpha"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 341 B |
@@ -1,3 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
<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>
|
||||||
<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: 201 B After Width: | Height: | Size: 191 B |
@@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 604 B |
@@ -1,3 +1,14 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
<svg
|
||||||
<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"/>
|
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>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 359 B |
@@ -1,3 +1,14 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
<svg
|
||||||
<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"/>
|
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>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 626 B After Width: | Height: | Size: 309 B |
@@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 435 B |
@@ -1,3 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
<path d="M12 6.75V12M12 12V17.25M12 12H6.75M12 12H17.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
<path d="M12 5.25V12M12 12V18.75M12 12H5.25M12 12H18.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 203 B After Width: | Height: | Size: 203 B |
@@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +1,15 @@
|
|||||||
<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>
|
<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>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 849 B After Width: | Height: | Size: 318 B |
@@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 445 B |
@@ -1,13 +1,16 @@
|
|||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use common::{EventExt, NostrParser, emojis_from_tags, extract_and_remove_media_urls, nevent_refs};
|
use common::{EventExt, NostrParser, emojis_from_tags, extract_and_remove_media_urls};
|
||||||
use gpui::{SharedString, SharedUri};
|
use gpui::{SharedString, SharedUri};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::FileAttachment;
|
use state::FileAttachment;
|
||||||
|
|
||||||
pub(crate) const KIND_FILE_MESSAGE: Kind = Kind::Custom(15);
|
pub(crate) const KIND_FILE_MESSAGE: Kind = Kind::Custom(15);
|
||||||
|
|
||||||
|
const SCHEME_WITH_COLON: &str = "nostr:";
|
||||||
|
const NEVENT_PREFIX: &str = "nevent1";
|
||||||
|
|
||||||
/// Rendered message.
|
/// Rendered message.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
@@ -245,6 +248,61 @@ fn extract_mentions(content: &str) -> Vec<Mention> {
|
|||||||
.collect()
|
.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.
|
/// Extracts all reply (ids) from the event tags.
|
||||||
fn extract_reply_ids(inner: &Tags) -> Vec<EventId> {
|
fn extract_reply_ids(inner: &Tags) -> Vec<EventId> {
|
||||||
let mut replies_to = vec![];
|
let mut replies_to = vec![];
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use gpui::{
|
|||||||
SystemNotificationAction, Task, WeakEntity, Window, div, img, list, px, red, relative,
|
SystemNotificationAction, Task, WeakEntity, Window, div, img, list, px, red, relative,
|
||||||
retain_all, svg, white,
|
retain_all, svg, white,
|
||||||
};
|
};
|
||||||
|
use gpui_kit::assets::IconName;
|
||||||
use gpui_kit::base::StyledExt as _;
|
use gpui_kit::base::StyledExt as _;
|
||||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||||
use gpui_kit::component::dock::{Panel, PanelEvent};
|
use gpui_kit::component::dock::{Panel, PanelEvent};
|
||||||
@@ -33,13 +34,16 @@ use smallvec::{SmallVec, smallvec};
|
|||||||
use state::{
|
use state::{
|
||||||
FileAttachment, NostrRegistry, download_and_decrypt_to_file, upload, upload_encrypted,
|
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::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.
|
/// Notice shown when a conversation has no messages, and above the message list.
|
||||||
const PRIVATE_NOTICE: &str =
|
const PRIVATE_NOTICE: &str =
|
||||||
@@ -56,6 +60,8 @@ static CUSTOM_EMOJI_RE: LazyLock<Regex> =
|
|||||||
|
|
||||||
mod actions;
|
mod actions;
|
||||||
mod file;
|
mod file;
|
||||||
|
pub mod message;
|
||||||
|
pub mod text;
|
||||||
|
|
||||||
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
|
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
|
||||||
cx.new(|cx| ChatPanel::new(room, window, cx))
|
cx.new(|cx| ChatPanel::new(room, window, cx))
|
||||||
@@ -1044,7 +1050,7 @@ impl ChatPanel {
|
|||||||
.rounded_full()
|
.rounded_full()
|
||||||
.bg(cx.theme().warning.opacity(0.15))
|
.bg(cx.theme().warning.opacity(0.15))
|
||||||
.text_color(cx.theme().warning)
|
.text_color(cx.theme().warning)
|
||||||
.child(Icon::new(IconName::Warning).small()),
|
.child(Icon::new(IconName::TriangleAlert).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -1102,12 +1108,9 @@ impl ChatPanel {
|
|||||||
.rendered_texts_by_id
|
.rendered_texts_by_id
|
||||||
.entry(message.id)
|
.entry(message.id)
|
||||||
.or_insert_with(|| {
|
.or_insert_with(|| {
|
||||||
rendered_text(
|
text::rendered_text(
|
||||||
&message.content,
|
&message.content,
|
||||||
message
|
&message.mentions,
|
||||||
.mentions
|
|
||||||
.iter()
|
|
||||||
.map(|mention| (mention.range.clone(), mention.public_key)),
|
|
||||||
&message.emojis,
|
&message.emojis,
|
||||||
&directory,
|
&directory,
|
||||||
cx,
|
cx,
|
||||||
@@ -1389,34 +1392,83 @@ impl ChatPanel {
|
|||||||
public_key: &PublicKey,
|
public_key: &PublicKey,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let mut actions = MessageActions::new();
|
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![];
|
||||||
|
|
||||||
for emoji in COMPACT_REACTION_EMOJIS {
|
for emoji in COMPACT_REACTION_EMOJIS {
|
||||||
let id = *id;
|
items.push(
|
||||||
actions = actions.reaction(
|
Button::new(*emoji)
|
||||||
emoji,
|
.label(*emoji)
|
||||||
cx.listener(move |this, _event, window, cx| {
|
.tooltip(*emoji)
|
||||||
this.send_reaction(emoji, &id, window, cx);
|
.small()
|
||||||
}),
|
.ghost()
|
||||||
);
|
.on_click({
|
||||||
}
|
let emoji = *emoji;
|
||||||
|
let id = *id;
|
||||||
|
cx.listener(move |this, _event, window, cx| {
|
||||||
|
this.send_reaction(emoji, &id, window, cx);
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let reply_id = *id;
|
items
|
||||||
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 {
|
fn render_attachment(&self, url: &Url, cx: &Context<Self>) -> impl IntoElement {
|
||||||
@@ -1442,7 +1494,7 @@ impl ChatPanel {
|
|||||||
.justify_center()
|
.justify_center()
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.bg(red())
|
.bg(red())
|
||||||
.child(Icon::new(IconName::Close).size_2().text_color(white())),
|
.child(Icon::new(IconName::X).size_2().text_color(white())),
|
||||||
)
|
)
|
||||||
.on_click({
|
.on_click({
|
||||||
let url = url.clone();
|
let url = url.clone();
|
||||||
@@ -1643,7 +1695,7 @@ impl ChatPanel {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("remove-reply")
|
Button::new("remove-reply")
|
||||||
.icon(IconName::Close)
|
.icon(IconName::X)
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.ghost()
|
.ghost()
|
||||||
.on_click({
|
.on_click({
|
||||||
@@ -1694,6 +1746,7 @@ impl ChatPanel {
|
|||||||
.icon(IconName::Settings2)
|
.icon(IconName::Settings2)
|
||||||
.tooltip("Configuration")
|
.tooltip("Configuration")
|
||||||
.ghost()
|
.ghost()
|
||||||
|
.large()
|
||||||
.dropdown_menu(move |this, _window, _cx| {
|
.dropdown_menu(move |this, _window, _cx| {
|
||||||
let auto = matches!(signer_kind, SignerKind::Auto);
|
let auto = matches!(signer_kind, SignerKind::Auto);
|
||||||
let encryption = matches!(signer_kind, SignerKind::Encryption);
|
let encryption = matches!(signer_kind, SignerKind::Encryption);
|
||||||
@@ -1739,8 +1792,9 @@ impl ChatPanel {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
Button::new("emoji")
|
Button::new("emoji")
|
||||||
.icon(IconName::Emoji)
|
.icon(IconName::Sticker)
|
||||||
.ghost()
|
.ghost()
|
||||||
|
.large()
|
||||||
.dropdown_menu_with_anchor(gpui::Anchor::BottomLeft, move |this, _window, _cx| {
|
.dropdown_menu_with_anchor(gpui::Anchor::BottomLeft, move |this, _window, _cx| {
|
||||||
let menu = REACTION_EMOJIS.iter().fold(this, |this, emoji| {
|
let menu = REACTION_EMOJIS.iter().fold(this, |this, emoji| {
|
||||||
this.menu(*emoji, Box::new(Command::Insert(emoji.to_string())))
|
this.menu(*emoji, Box::new(Command::Insert(emoji.to_string())))
|
||||||
@@ -1786,7 +1840,7 @@ impl Panel for ChatPanel {
|
|||||||
|
|
||||||
Some(vec![
|
Some(vec![
|
||||||
Button::new("subject")
|
Button::new("subject")
|
||||||
.icon(IconName::Input)
|
.icon(IconName::TextCursorInput)
|
||||||
.tooltip("Change subject")
|
.tooltip("Change subject")
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
@@ -1828,10 +1882,10 @@ impl Render for ChatPanel {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.border_b_1()
|
.border_b_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.child(Input::new(&self.subject_input))
|
.child(Input::new(&self.subject_input).small())
|
||||||
.child(
|
.child(
|
||||||
Button::new("change")
|
Button::new("change")
|
||||||
.icon(IconName::CheckCircle)
|
.icon(IconName::CircleCheck)
|
||||||
.label("Change")
|
.label("Change")
|
||||||
.secondary()
|
.secondary()
|
||||||
.disabled(self.uploading)
|
.disabled(self.uploading)
|
||||||
@@ -1896,6 +1950,7 @@ impl Render for ChatPanel {
|
|||||||
.loading(self.uploading)
|
.loading(self.uploading)
|
||||||
.disabled(self.uploading)
|
.disabled(self.uploading)
|
||||||
.ghost()
|
.ghost()
|
||||||
|
.large()
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
this.upload(window, cx);
|
this.upload(window, cx);
|
||||||
})),
|
})),
|
||||||
@@ -1909,9 +1964,10 @@ impl Render for ChatPanel {
|
|||||||
.child(self.render_config_menu(window, cx))
|
.child(self.render_config_menu(window, cx))
|
||||||
.child(
|
.child(
|
||||||
Button::new("send")
|
Button::new("send")
|
||||||
.icon(IconName::PaperPlaneFill)
|
.icon(IconName::SendHorizontal)
|
||||||
.disabled(self.uploading)
|
.disabled(self.uploading)
|
||||||
.ghost()
|
.ghost()
|
||||||
|
.large()
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
this.send_text_message(window, cx);
|
this.send_text_message(window, cx);
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -1,25 +1,19 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, ClickEvent, Context, ElementId, InteractiveElement as _, IntoElement,
|
AnyElement, App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, MouseButton,
|
||||||
MouseButton, MouseDownEvent, ObjectFit, ParentElement as _, RenderOnce, SharedString,
|
MouseDownEvent, ObjectFit, ParentElement as _, RenderOnce, SharedString, SharedUri,
|
||||||
SharedUri, StatefulInteractiveElement as _, StyleRefinement, Styled, StyledImage, Window, div,
|
StatefulInteractiveElement as _, StyleRefinement, Styled, StyledImage, Window, div, img, px,
|
||||||
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::{
|
use gpui_kit::component::{
|
||||||
ActiveTheme, InteractiveElementExt as _, Sizable as _, StyledExt as _, h_flex, v_flex,
|
ActiveTheme, InteractiveElementExt as _, StyledExt as _, h_flex, v_flex,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
pub use ui::Avatar;
|
||||||
|
|
||||||
type MouseDownListener = Box<dyn Fn(&MouseDownEvent, &mut Window, &mut App) + 'static>;
|
type MouseDownListener = Box<dyn Fn(&MouseDownEvent, &mut Window, &mut App) + 'static>;
|
||||||
type ClickListener = Box<dyn Fn(&ClickEvent, &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.
|
/// Messages from the same author within this window are grouped into one run.
|
||||||
pub const RUN_WINDOW_SECS: u64 = 300;
|
pub const RUN_WINDOW_SECS: u64 = 300;
|
||||||
@@ -184,7 +178,7 @@ impl RenderOnce for MessageRow {
|
|||||||
.group("")
|
.group("")
|
||||||
.relative()
|
.relative()
|
||||||
.w_full()
|
.w_full()
|
||||||
.py_0p5()
|
.py_1()
|
||||||
.when(show_author, |this| this.pt_2())
|
.when(show_author, |this| this.pt_2())
|
||||||
.px_3()
|
.px_3()
|
||||||
.refine_style(&style)
|
.refine_style(&style)
|
||||||
@@ -209,7 +203,6 @@ impl RenderOnce for MessageRow {
|
|||||||
.min_w_0()
|
.min_w_0()
|
||||||
.flex_initial()
|
.flex_initial()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.text_size(px(15.))
|
|
||||||
.when(show_author, |this| {
|
.when(show_author, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -303,7 +296,7 @@ impl RenderOnce for ReplyPreview {
|
|||||||
.refine_style(&style)
|
.refine_style(&style)
|
||||||
.child(div().font_semibold().child(author))
|
.child(div().font_semibold().child(author))
|
||||||
.child(div().w_full().text_ellipsis().line_clamp(1).child(preview))
|
.child(div().w_full().text_ellipsis().line_clamp(1).child(preview))
|
||||||
.hover(|this| this.bg(cx.theme().list_hover))
|
.hover(|this| this.bg(cx.theme().secondary))
|
||||||
.when_some(on_click, |this, on_click| this.on_click(on_click))
|
.when_some(on_click, |this, on_click| this.on_click(on_click))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -560,174 +553,3 @@ 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 std::ops::Range;
|
||||||
|
|
||||||
use common::nevent_refs;
|
use chat::{Mention, nevent_refs};
|
||||||
use gpui::{AnyElement, App, ElementId, Entity, IntoElement as _, SharedString, SharedUri, Window};
|
use gpui::{AnyElement, App, ElementId, Entity, IntoElement as _, SharedString, SharedUri, Window};
|
||||||
use gpui_kit::component::text::TextView;
|
use gpui_kit::component::text::TextView;
|
||||||
use nostr_sdk::prelude::{PublicKey, Url};
|
use nostr_sdk::prelude::Url;
|
||||||
use person::Directory;
|
use person::Directory;
|
||||||
|
|
||||||
/// A span of the source content, by byte range, that renders as `text` instead.
|
/// 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)
|
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(
|
pub fn rendered_text(
|
||||||
content: &str,
|
content: &str,
|
||||||
mentions: impl IntoIterator<Item = (Range<usize>, PublicKey)>,
|
mentions: &[Mention],
|
||||||
emojis: &[(String, Url)],
|
emojis: &[(String, Url)],
|
||||||
persons: &Entity<Directory>,
|
persons: &Entity<Directory>,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> RenderedText {
|
) -> RenderedText {
|
||||||
let mut replacements = mentions
|
let mut replacements = mentions
|
||||||
.into_iter()
|
.iter()
|
||||||
.map(|(range, public_key)| {
|
.map(|mention| {
|
||||||
InlineReplacement::new(
|
InlineReplacement::new(
|
||||||
range,
|
mention.range.clone(),
|
||||||
format!("@{}", persons.read(cx).person(&public_key, cx).name()),
|
format!(
|
||||||
|
"@{}",
|
||||||
|
persons.read(cx).person(&mention.public_key, cx).name()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -4,62 +4,6 @@ use nostr::prelude::*;
|
|||||||
|
|
||||||
const BECH32_SEPARATOR: u8 = b'1';
|
const BECH32_SEPARATOR: u8 = b'1';
|
||||||
const SCHEME_WITH_COLON: &str = "nostr:";
|
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
|
/// Nostr parsed token with its range in the original text
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use chat_ui::message::WelcomeMessage;
|
||||||
|
use chat_ui::text::{RenderedText, rendered_text};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
@@ -24,7 +26,7 @@ use serde::Deserialize;
|
|||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{NostrRegistry, upload};
|
use state::{NostrRegistry, upload};
|
||||||
use ui::{Avatar, RenderedText, WelcomeMessage, rendered_text};
|
use ui::Avatar;
|
||||||
use util::{attachment_name, display_name, opens_run, pin_label, report};
|
use util::{attachment_name, display_name, opens_run, pin_label, report};
|
||||||
|
|
||||||
mod details;
|
mod details;
|
||||||
@@ -985,10 +987,7 @@ impl GroupPanel {
|
|||||||
.or_insert_with(|| {
|
.or_insert_with(|| {
|
||||||
rendered_text(
|
rendered_text(
|
||||||
&message.content,
|
&message.content,
|
||||||
message
|
&message.mentions,
|
||||||
.mentions
|
|
||||||
.iter()
|
|
||||||
.map(|mention| (mention.range.clone(), mention.public_key)),
|
|
||||||
&message.emojis,
|
&message.emojis,
|
||||||
&directory,
|
&directory,
|
||||||
cx,
|
cx,
|
||||||
@@ -1066,6 +1065,7 @@ impl GroupPanel {
|
|||||||
.loading(self.uploading)
|
.loading(self.uploading)
|
||||||
.disabled(self.uploading)
|
.disabled(self.uploading)
|
||||||
.ghost()
|
.ghost()
|
||||||
|
.large()
|
||||||
.on_click(cx.listener(|this, _event, window, cx| {
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
this.upload(window, cx);
|
this.upload(window, cx);
|
||||||
})),
|
})),
|
||||||
@@ -1073,9 +1073,10 @@ impl GroupPanel {
|
|||||||
.child(Textarea::new(&self.input).appearance(false).flex_1())
|
.child(Textarea::new(&self.input).appearance(false).flex_1())
|
||||||
.child(
|
.child(
|
||||||
Button::new("send")
|
Button::new("send")
|
||||||
.icon(ui::IconName::PaperPlaneFill)
|
.icon(IconName::SendHorizontal)
|
||||||
.tooltip("Send")
|
.tooltip("Send")
|
||||||
.ghost()
|
.ghost()
|
||||||
|
.large()
|
||||||
.on_click(cx.listener(|this, _event, window, cx| {
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
this.send(window, cx);
|
this.send(window, cx);
|
||||||
})),
|
})),
|
||||||
@@ -1140,7 +1141,7 @@ impl GroupPanel {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("cancel-reply")
|
Button::new("cancel-reply")
|
||||||
.icon(ui::IconName::Close)
|
.icon(IconName::X)
|
||||||
.tooltip("Cancel reply")
|
.tooltip("Cancel reply")
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
@@ -1177,7 +1178,7 @@ impl GroupPanel {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new(SharedString::from(format!("remove-attachment-{ix}")))
|
Button::new(SharedString::from(format!("remove-attachment-{ix}")))
|
||||||
.icon(ui::IconName::Close)
|
.icon(IconName::X)
|
||||||
.tooltip("Remove")
|
.tooltip("Remove")
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use chat::Message;
|
use chat::Message;
|
||||||
|
use chat_ui::message::{MessageMedia, MessageRow, ReactionBar, ReplyPreview, group_reactions};
|
||||||
use common::TimestampExt;
|
use common::TimestampExt;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -7,20 +8,19 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use gpui_kit::assets::IconName;
|
use gpui_kit::assets::IconName;
|
||||||
use gpui_kit::base::StyledExt as _;
|
use gpui_kit::base::StyledExt as _;
|
||||||
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
|
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenu, PopupMenuItem};
|
||||||
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex};
|
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex};
|
||||||
use nip29::{Activity, Group, Reaction};
|
use nip29::{Activity, Group, Reaction};
|
||||||
use nostr_sdk::prelude::{EventId, Kind};
|
use nostr_sdk::prelude::{EventId, Kind};
|
||||||
use person::Directory;
|
use person::Directory;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use state::NostrRegistry;
|
use state::NostrRegistry;
|
||||||
use ui::{
|
use ui::Avatar;
|
||||||
Avatar, MessageMedia, MessageRow, REACTION_EMOJIS, ReactionBar, ReplyPreview, group_reactions,
|
|
||||||
react_menu,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::GroupPanel;
|
use crate::GroupPanel;
|
||||||
|
|
||||||
|
const REACTIONS: [&str; 6] = ["👍", "❤️", "😂", "🎉", "😮", "😢"];
|
||||||
|
|
||||||
pub(crate) struct Actions {
|
pub(crate) struct Actions {
|
||||||
pub group: WeakEntity<Group>,
|
pub group: WeakEntity<Group>,
|
||||||
pub panel: WeakEntity<GroupPanel>,
|
pub panel: WeakEntity<GroupPanel>,
|
||||||
@@ -89,22 +89,6 @@ pub(crate) fn render(
|
|||||||
let group = actions.group;
|
let group = actions.group;
|
||||||
let panel = actions.panel;
|
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()
|
let row = div()
|
||||||
.id(SharedString::from(format!("message-menu-{}", id.to_hex())))
|
.id(SharedString::from(format!("message-menu-{}", id.to_hex())))
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -123,20 +107,8 @@ pub(crate) fn render(
|
|||||||
.submenu("React", window, cx, {
|
.submenu("React", window, cx, {
|
||||||
let group = group.clone();
|
let group = group.clone();
|
||||||
let panel = panel.clone();
|
let panel = panel.clone();
|
||||||
let custom = custom.clone();
|
|
||||||
|
|
||||||
move |menu, _window, _cx| {
|
move |menu, _window, cx| react_menu(menu, group.clone(), panel.clone(), id, 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 {
|
if !admin {
|
||||||
@@ -171,6 +143,67 @@ pub(crate) fn render(
|
|||||||
row.into_any_element()
|
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(
|
pub(crate) fn render_activity(
|
||||||
ix: usize,
|
ix: usize,
|
||||||
activity: &Activity,
|
activity: &Activity,
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||||
use gpui_kit::component::dialog::DialogButtonProps;
|
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::input::InputState;
|
||||||
use gpui_kit::component::notification::Notification;
|
use gpui_kit::component::notification::Notification;
|
||||||
use gpui_kit::component::switch::Switch;
|
use gpui_kit::component::switch::Switch;
|
||||||
use gpui_kit::component::{ActiveTheme, Sizable, WindowExt, h_flex, v_flex};
|
use gpui_kit::component::{ActiveTheme, Sizable, WindowExt, h_flex, v_flex};
|
||||||
use nip29::{Group, GroupId, GroupMetadata};
|
use nip29::{Group, GroupId, GroupMetadata};
|
||||||
use nostr_sdk::prelude::RelayUrl;
|
use nostr_sdk::prelude::RelayUrl;
|
||||||
use ui::settings_row;
|
|
||||||
|
|
||||||
use crate::util::field;
|
use crate::util::field;
|
||||||
|
|
||||||
@@ -126,61 +126,73 @@ pub(crate) fn open(group: WeakEntity<Group>, window: &mut Window, cx: &mut App)
|
|||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(settings_row(
|
.child(
|
||||||
"Private",
|
form_field()
|
||||||
"Only members can read group messages",
|
.label("Private")
|
||||||
Switch::new("private")
|
.description("Only members can read group messages")
|
||||||
.checked(current.private)
|
.child(
|
||||||
.on_click(toggle(&flags, |flags| &mut flags.private)),
|
Switch::new("private")
|
||||||
cx,
|
.checked(current.private)
|
||||||
))
|
.on_click(toggle(&flags, |flags| &mut flags.private)),
|
||||||
.child(settings_row(
|
),
|
||||||
"Restricted",
|
)
|
||||||
"Only members can post messages",
|
.child(
|
||||||
Switch::new("restricted")
|
form_field()
|
||||||
.checked(current.restricted)
|
.label("Restricted")
|
||||||
.on_click(toggle(&flags, |flags| &mut flags.restricted)),
|
.description("Only members can post messages")
|
||||||
cx,
|
.child(
|
||||||
))
|
Switch::new("restricted")
|
||||||
.child(settings_row(
|
.checked(current.restricted)
|
||||||
"Hidden",
|
.on_click(toggle(&flags, |flags| {
|
||||||
"Hidden from non-members, not discoverable",
|
&mut flags.restricted
|
||||||
Switch::new("hidden")
|
})),
|
||||||
.checked(current.hidden)
|
),
|
||||||
.on_click(toggle(&flags, |flags| &mut flags.hidden)),
|
)
|
||||||
cx,
|
.child(
|
||||||
))
|
form_field()
|
||||||
.child(settings_row(
|
.label("Hidden")
|
||||||
"Closed",
|
.description("Hidden from non-members, not discoverable")
|
||||||
"Join requests are ignored (invite-only)",
|
.child(
|
||||||
Switch::new("closed")
|
Switch::new("hidden")
|
||||||
.checked(current.closed)
|
.checked(current.hidden)
|
||||||
.on_click(toggle(&flags, |flags| &mut flags.closed)),
|
.on_click(toggle(&flags, |flags| &mut flags.hidden)),
|
||||||
cx,
|
),
|
||||||
))
|
)
|
||||||
.child(settings_row(
|
.child(
|
||||||
"LiveKit",
|
form_field()
|
||||||
if supported {
|
.label("Closed")
|
||||||
"Announce audio and video support."
|
.description("Join requests are ignored (invite-only)")
|
||||||
} else {
|
.child(
|
||||||
"The relay doesn't announce LiveKit support."
|
Switch::new("closed")
|
||||||
},
|
.checked(current.closed)
|
||||||
Switch::new("livekit").checked(current.livekit).on_click({
|
.on_click(toggle(&flags, |flags| &mut flags.closed)),
|
||||||
let flags = flags.clone();
|
),
|
||||||
let livekit = livekit.clone();
|
)
|
||||||
|
.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();
|
||||||
|
|
||||||
move |next, _window, cx| {
|
move |next, _window, cx| {
|
||||||
if *next && *livekit.read(cx) == Some(false) {
|
if *next && *livekit.read(cx) == Some(false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
flags.update(cx, |flags, cx| {
|
flags.update(cx, |flags, cx| {
|
||||||
flags.livekit = *next;
|
flags.livekit = *next;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
cx,
|
),
|
||||||
)),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.on_ok({
|
.on_ok({
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ pub(crate) fn opens_run(rows: &[Row], index: usize) -> bool {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match (current, previous) {
|
match (current, previous) {
|
||||||
(Row::Message(current), Row::Message(previous)) => ui::opens_run(
|
(Row::Message(current), Row::Message(previous)) => chat_ui::message::opens_run(
|
||||||
Some((previous.author, previous.created_at.as_secs())),
|
Some((previous.author, previous.created_at.as_secs())),
|
||||||
(current.author, current.created_at.as_secs()),
|
(current.author, current.created_at.as_secs()),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use gpui::{Context, EventEmitter, Task};
|
|||||||
use instant::Duration;
|
use instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{BOOTSTRAP_RELAYS, NostrRegistry};
|
use state::NostrRegistry;
|
||||||
|
|
||||||
/// Disable scoring when the follow set exceeds this size
|
/// Disable scoring when the follow set exceeds this size
|
||||||
const MAX_FOLLOWS: usize = 2000;
|
const MAX_FOLLOWS: usize = 2000;
|
||||||
@@ -14,10 +14,6 @@ const MAX_TAGS_PER_LIST: usize = 5000;
|
|||||||
const SYNC_INTERVAL: u64 = 15 * 60;
|
const SYNC_INTERVAL: u64 = 15 * 60;
|
||||||
/// Longest wait between retries while the graph has no data yet
|
/// Longest wait between retries while the graph has no data yet
|
||||||
const MAX_RETRY_WAIT: u64 = 60;
|
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
|
/// The web of trust graph changed
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -81,36 +77,6 @@ impl Snapshot {
|
|||||||
|
|
||||||
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 {}
|
impl EventEmitter<WoTEvent> for WebOfTrust {}
|
||||||
@@ -122,10 +88,8 @@ impl EventEmitter<WoTEvent> for WebOfTrust {}
|
|||||||
pub struct WebOfTrust {
|
pub struct WebOfTrust {
|
||||||
/// Current user's public key
|
/// Current user's public key
|
||||||
self_key: Option<PublicKey>,
|
self_key: Option<PublicKey>,
|
||||||
/// Latest built snapshot, none until the first load completes
|
/// Latest built snapshot, none until the first refresh completes
|
||||||
snapshot: Option<Snapshot>,
|
snapshot: Option<Snapshot>,
|
||||||
/// Whether a reconciliation with the relays is in flight
|
|
||||||
syncing: bool,
|
|
||||||
/// In-flight refresh, replaced by newer ones
|
/// In-flight refresh, replaced by newer ones
|
||||||
refresh_task: Option<Task<()>>,
|
refresh_task: Option<Task<()>>,
|
||||||
/// Keeps the periodic reconciliation loop alive
|
/// Keeps the periodic reconciliation loop alive
|
||||||
@@ -146,9 +110,9 @@ impl WebOfTrust {
|
|||||||
|
|
||||||
let mut has_data = false;
|
let mut has_data = false;
|
||||||
|
|
||||||
// Ignore errors, the next sync will retry
|
// Ignore errors, the next refresh will retry
|
||||||
let _ = this.update(cx, |this, cx| {
|
let _ = this.update(cx, |this, cx| {
|
||||||
this.sync(cx);
|
this.refresh(cx);
|
||||||
has_data = this.has_data();
|
has_data = this.has_data();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,56 +133,34 @@ impl WebOfTrust {
|
|||||||
Self {
|
Self {
|
||||||
self_key: None,
|
self_key: None,
|
||||||
snapshot: None,
|
snapshot: None,
|
||||||
syncing: false,
|
|
||||||
refresh_task: None,
|
refresh_task: None,
|
||||||
_tasks: tasks,
|
_tasks: tasks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the graph for the current account from the local database
|
/// Load the graph for the current account
|
||||||
pub(crate) fn hydrate(&mut self, cx: &mut Context<Self>) {
|
pub(crate) fn hydrate(&mut self, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
|
|
||||||
let Some(self_key) = nostr.read(cx).current_user() else {
|
let Some(self_key) = nostr.read(cx).current_user() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.self_key = Some(self_key);
|
self.self_key = Some(self_key);
|
||||||
// Replacing the task below cancels any in-flight work
|
self.refresh(cx);
|
||||||
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
|
/// Clear the graph on sign out or account switch
|
||||||
pub(crate) fn reset(&mut self, cx: &mut Context<Self>) {
|
pub(crate) fn reset(&mut self, cx: &mut Context<Self>) {
|
||||||
self.self_key = None;
|
self.self_key = None;
|
||||||
self.snapshot = None;
|
self.snapshot = None;
|
||||||
self.syncing = false;
|
|
||||||
self.refresh_task = None;
|
self.refresh_task = None;
|
||||||
|
|
||||||
cx.emit(WoTEvent);
|
cx.emit(WoTEvent);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reconcile the follow set's contact lists with the bootstrap relays and rebuild.
|
/// Reconcile the follow set's contact lists with the relays and rebuild.
|
||||||
fn sync(&mut self, cx: &mut Context<Self>) {
|
fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.syncing {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
@@ -226,13 +168,6 @@ impl WebOfTrust {
|
|||||||
return;
|
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| {
|
self.refresh_task = Some(cx.spawn(async move |this, cx| {
|
||||||
// Scope the reconcile to the follow set known so far
|
// Scope the reconcile to the follow set known so far
|
||||||
let known: HashSet<PublicKey> = stored_contact_list(&client, self_key)
|
let known: HashSet<PublicKey> = stored_contact_list(&client, self_key)
|
||||||
@@ -243,36 +178,60 @@ impl WebOfTrust {
|
|||||||
let mut authors: Vec<PublicKey> = known.iter().copied().collect();
|
let mut authors: Vec<PublicKey> = known.iter().copied().collect();
|
||||||
authors.push(self_key);
|
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);
|
let filter = Filter::new().kind(Kind::ContactList).authors(authors);
|
||||||
match client.sync(filter).with(BOOTSTRAP_RELAYS).opts(opts).await {
|
if let Err(error) = client.sync(filter).await {
|
||||||
Ok(output) => {
|
log::warn!("Failed to reconcile contact lists: {error}");
|
||||||
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
|
// The follow set after the reconcile
|
||||||
let snapshot = Snapshot::load(&client, self_key).await;
|
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);
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.syncing = false;
|
this.snapshot = Some(snapshot);
|
||||||
|
cx.emit(WoTEvent);
|
||||||
if let Some(snapshot) = snapshot {
|
cx.notify();
|
||||||
// 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();
|
.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 anyhow::{Error, anyhow};
|
||||||
use common::config_dir;
|
use common::config_dir;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, ImageSource, Subscription, Task, px};
|
use gpui::{App, AppContext, Context, Entity, Global, ImageSource, Subscription, Task};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
@@ -31,33 +31,6 @@ 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) {
|
pub fn init(cx: &mut App) {
|
||||||
AppSettings::set_global(cx.new(AppSettings::new), cx)
|
AppSettings::set_global(cx.new(AppSettings::new), cx)
|
||||||
}
|
}
|
||||||
@@ -67,34 +40,17 @@ pub fn init(cx: &mut App) {
|
|||||||
/// The async load fills the global settings later; the native theme must be
|
/// 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
|
/// applied before the first window opens, so boot reads the mode straight
|
||||||
/// from the settings file instead.
|
/// from the settings file instead.
|
||||||
fn persisted_settings() -> Option<Settings> {
|
pub fn persisted_appearance_mode() -> AppearanceMode {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
let path = config_dir().join(".settings");
|
let path = config_dir().join(".settings");
|
||||||
if let Ok(content) = std::fs::read_to_string(path)
|
if let Ok(content) = std::fs::read_to_string(path)
|
||||||
&& let Ok(settings) = serde_json::from_str::<Settings>(&content)
|
&& let Ok(settings) = serde_json::from_str::<Settings>(&content)
|
||||||
{
|
{
|
||||||
return Some(settings);
|
return settings.appearance_mode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
AppearanceMode::default()
|
||||||
}
|
|
||||||
|
|
||||||
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/";
|
const DEFAULT_FILE_SERVER: &str = "https://nostr.download/";
|
||||||
@@ -153,7 +109,6 @@ setting_accessors! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setting_getters! {
|
setting_getters! {
|
||||||
pub theme: ThemeSettings,
|
|
||||||
pub pinned_rooms: Vec<u64>,
|
pub pinned_rooms: Vec<u64>,
|
||||||
pub pinned_groups: Vec<String>,
|
pub pinned_groups: Vec<String>,
|
||||||
}
|
}
|
||||||
@@ -224,9 +179,6 @@ pub struct Settings {
|
|||||||
/// Hide the notice that messages in public groups are not encrypted
|
/// Hide the notice that messages in public groups are not encrypted
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub hide_group_notice: bool,
|
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
|
/// Show an image behind chat and group panels
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub panel_background: bool,
|
pub panel_background: bool,
|
||||||
@@ -254,7 +206,6 @@ impl Default for Settings {
|
|||||||
wot_auto_trust: true,
|
wot_auto_trust: true,
|
||||||
nip4e: false,
|
nip4e: false,
|
||||||
hide_group_notice: false,
|
hide_group_notice: false,
|
||||||
theme: ThemeSettings::default(),
|
|
||||||
panel_background: true,
|
panel_background: true,
|
||||||
panel_background_path: None,
|
panel_background_path: None,
|
||||||
trusted_relays: vec![],
|
trusted_relays: vec![],
|
||||||
@@ -319,34 +270,6 @@ 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
|
/// Load settings
|
||||||
fn load(&mut self, cx: &mut Context<Self>) {
|
fn load(&mut self, cx: &mut Context<Self>) {
|
||||||
let task: Task<Result<Settings, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Settings, Error>> = cx.background_spawn(async move {
|
||||||
@@ -459,27 +382,3 @@ 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,12 +6,9 @@ publish.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
common = { path = "../common" }
|
|
||||||
person = { path = "../person" }
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui-base.workspace = true
|
gpui-base.workspace = true
|
||||||
gpui-kit.workspace = true
|
gpui-kit.workspace = true
|
||||||
nostr-sdk.workspace = true
|
|
||||||
web-time = "1.1"
|
web-time = "1.1"
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|||||||
@@ -167,16 +167,11 @@ impl RenderOnce for PixelAvatar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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()
|
div()
|
||||||
.grid()
|
.grid()
|
||||||
.grid_cols(GRID_SIZE as u16)
|
.grid_cols(GRID_SIZE as u16)
|
||||||
.grid_rows(GRID_SIZE as u16)
|
.grid_rows(GRID_SIZE as u16)
|
||||||
.size(self.side)
|
.size(self.side)
|
||||||
.p(inset)
|
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -9,27 +9,16 @@ pub enum IconName {
|
|||||||
Book,
|
Book,
|
||||||
CaretDown,
|
CaretDown,
|
||||||
CaretRight,
|
CaretRight,
|
||||||
CheckCircle,
|
|
||||||
Close,
|
|
||||||
Compass,
|
Compass,
|
||||||
Door,
|
Door,
|
||||||
Emoji,
|
|
||||||
Group,
|
Group,
|
||||||
Input,
|
|
||||||
Invite,
|
Invite,
|
||||||
Link,
|
|
||||||
Lock,
|
|
||||||
Message,
|
Message,
|
||||||
PaperPlaneFill,
|
|
||||||
Plus,
|
|
||||||
PlusChat,
|
PlusChat,
|
||||||
Profile,
|
Profile,
|
||||||
Relay,
|
Relay,
|
||||||
Search,
|
Search,
|
||||||
Settings,
|
Settings,
|
||||||
Settings2,
|
|
||||||
Upload,
|
|
||||||
Warning,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IconNamed for IconName {
|
impl IconNamed for IconName {
|
||||||
@@ -40,27 +29,16 @@ impl IconNamed for IconName {
|
|||||||
Self::Book => "icons/book.svg",
|
Self::Book => "icons/book.svg",
|
||||||
Self::CaretDown => "icons/caret-down.svg",
|
Self::CaretDown => "icons/caret-down.svg",
|
||||||
Self::CaretRight => "icons/caret-right.svg",
|
Self::CaretRight => "icons/caret-right.svg",
|
||||||
Self::CheckCircle => "icons/check-circle.svg",
|
|
||||||
Self::Close => "icons/close.svg",
|
|
||||||
Self::Compass => "icons/compass.svg",
|
Self::Compass => "icons/compass.svg",
|
||||||
Self::Door => "icons/door.svg",
|
Self::Door => "icons/door.svg",
|
||||||
Self::Emoji => "icons/emoji.svg",
|
|
||||||
Self::Group => "icons/group.svg",
|
Self::Group => "icons/group.svg",
|
||||||
Self::Input => "icons/input.svg",
|
|
||||||
Self::Invite => "icons/invite.svg",
|
Self::Invite => "icons/invite.svg",
|
||||||
Self::Link => "icons/link.svg",
|
|
||||||
Self::Lock => "icons/lock.svg",
|
|
||||||
Self::Message => "icons/message.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::PlusChat => "icons/plus-chat.svg",
|
||||||
Self::Profile => "icons/profile.svg",
|
Self::Profile => "icons/profile.svg",
|
||||||
Self::Relay => "icons/relay.svg",
|
Self::Relay => "icons/relay.svg",
|
||||||
Self::Search => "icons/search.svg",
|
Self::Search => "icons/search.svg",
|
||||||
Self::Settings => "icons/settings.svg",
|
Self::Settings => "icons/settings.svg",
|
||||||
Self::Settings2 => "icons/settings2.svg",
|
|
||||||
Self::Upload => "icons/upload.svg",
|
|
||||||
Self::Warning => "icons/warning.svg",
|
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
pub use avatar::Avatar;
|
pub use avatar::Avatar;
|
||||||
pub use form::*;
|
|
||||||
pub use icon::IconName;
|
pub use icon::IconName;
|
||||||
pub use message::*;
|
|
||||||
pub use text::*;
|
|
||||||
pub use window_drag::title_bar_drag_handlers;
|
pub use window_drag::title_bar_drag_handlers;
|
||||||
|
|
||||||
pub mod avatar;
|
pub mod avatar;
|
||||||
pub mod form;
|
|
||||||
pub mod icon;
|
pub mod icon;
|
||||||
pub mod message;
|
|
||||||
pub mod text;
|
|
||||||
pub mod window_drag;
|
pub mod window_drag;
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ impl NewChat {
|
|||||||
|
|
||||||
Nav::new(start + offset)
|
Nav::new(start + offset)
|
||||||
.label(profile.name())
|
.label(profile.name())
|
||||||
.text_sm()
|
.text_xs()
|
||||||
.when(!AppSettings::get_hide_avatar(cx), |this| {
|
.when(!AppSettings::get_hide_avatar(cx), |this| {
|
||||||
this.prefix(
|
this.prefix(
|
||||||
Avatar::new(profile.avatar())
|
Avatar::new(profile.avatar())
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ use gpui::prelude::FluentBuilder;
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Context, Div, Entity, FocusHandle, InteractiveElement as _, IntoElement,
|
App, AppContext, Context, Div, Entity, FocusHandle, InteractiveElement as _, IntoElement,
|
||||||
ParentElement, PathPromptOptions, Render, SharedString, StatefulInteractiveElement as _,
|
ParentElement, PathPromptOptions, Render, SharedString, StatefulInteractiveElement as _,
|
||||||
StyleRefinement, Styled, Subscription, Task, WeakEntity, Window, div, px, rems,
|
Styled, Subscription, Task, WeakEntity, Window, div, px, rems,
|
||||||
};
|
};
|
||||||
use gpui_base::{Popup, Select};
|
use gpui_base::{Popup, Select};
|
||||||
use gpui_kit::assets::IconName;
|
use gpui_kit::assets::IconName;
|
||||||
use gpui_kit::base::StyledExt as _;
|
use gpui_kit::base::StyledExt as _;
|
||||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||||
use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle};
|
use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle};
|
||||||
use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants};
|
use gpui_kit::component::form::field;
|
||||||
use gpui_kit::component::input::{Input, InputEvent, InputState, Textarea, TextareaState};
|
use gpui_kit::component::input::{Input, InputEvent, InputState, Textarea, TextareaState};
|
||||||
use gpui_kit::component::separator::Separator;
|
use gpui_kit::component::separator::Separator;
|
||||||
use gpui_kit::component::switch::Switch;
|
use gpui_kit::component::switch::Switch;
|
||||||
@@ -19,14 +19,14 @@ use nip29::{GroupCreateOptions, GroupKey, GroupsEvent, GroupsRegistry};
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use state::upload;
|
use state::upload;
|
||||||
use ui::{Avatar, settings_row};
|
use ui::Avatar;
|
||||||
|
|
||||||
const DEFAULT_RELAYS: [&str; 2] = ["wss://relay.ditto.net", "wss://chat.wisp.talk"];
|
const DEFAULT_RELAYS: [&str; 2] = ["wss://relay.ditto.net", "wss://chat.wisp.talk"];
|
||||||
const PRIVATE_DESC: &str = "Only members can read group messages";
|
const PRIVATE_DESC: &str = "Only members can read group messages";
|
||||||
const CLOSED_DESC: &str = "Join requests are ignored (invite-only)";
|
const CLOSED_DESC: &str = "Join requests are ignored (invite-only)";
|
||||||
const RESTRICTED_DESC: &str = "Only members can post messages";
|
const RESTRICTED_DESC: &str = "Only members can post messages";
|
||||||
const HIDDEN_DESC: &str = "Hidden from non-members, not discoverable";
|
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";
|
const LIST_PRIVATE_DESC: &str = "Kept encrypted in your group list so others can't see you joined";
|
||||||
|
|
||||||
pub fn open(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) {
|
pub fn open(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) {
|
||||||
open_with(dock, None, window, cx);
|
open_with(dock, None, window, cx);
|
||||||
@@ -462,72 +462,71 @@ impl NewGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_access(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render_access(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
GroupBox::new()
|
let private = field().label("Private").description(PRIVATE_DESC).child(
|
||||||
.fill()
|
Switch::new("private")
|
||||||
.title("Access settings")
|
.checked(self.private)
|
||||||
.title_style(group_box_title_style(cx))
|
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||||
.child(settings_row(
|
this.private = *checked;
|
||||||
"Private",
|
cx.notify();
|
||||||
PRIVATE_DESC,
|
})),
|
||||||
Switch::new("private")
|
);
|
||||||
.checked(self.private)
|
|
||||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
let closed = field().label("Closed").description(CLOSED_DESC).child(
|
||||||
this.private = *checked;
|
Switch::new("closed")
|
||||||
cx.notify();
|
.checked(self.closed)
|
||||||
})),
|
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||||
cx,
|
this.closed = *checked;
|
||||||
))
|
cx.notify();
|
||||||
.child(settings_row(
|
})),
|
||||||
"Closed",
|
);
|
||||||
CLOSED_DESC,
|
|
||||||
Switch::new("closed")
|
let restricted = field()
|
||||||
.checked(self.closed)
|
.label("Restricted")
|
||||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
.description(RESTRICTED_DESC)
|
||||||
this.closed = *checked;
|
.child(
|
||||||
cx.notify();
|
|
||||||
})),
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
.child(settings_row(
|
|
||||||
"Restricted",
|
|
||||||
RESTRICTED_DESC,
|
|
||||||
Switch::new("restricted")
|
Switch::new("restricted")
|
||||||
.checked(self.restricted)
|
.checked(self.restricted)
|
||||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||||
this.restricted = *checked;
|
this.restricted = *checked;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})),
|
})),
|
||||||
cx,
|
);
|
||||||
))
|
|
||||||
.child(settings_row(
|
let hidden = field().label("Hidden").description(HIDDEN_DESC).child(
|
||||||
"Hidden",
|
Switch::new("hidden")
|
||||||
HIDDEN_DESC,
|
.checked(self.hidden)
|
||||||
Switch::new("hidden")
|
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||||
.checked(self.hidden)
|
this.hidden = *checked;
|
||||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
cx.notify();
|
||||||
this.hidden = *checked;
|
})),
|
||||||
cx.notify();
|
);
|
||||||
})),
|
|
||||||
cx,
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_list_setting(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render_list_setting(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
GroupBox::new()
|
let list_private = field()
|
||||||
.fill()
|
.label("Private on my list")
|
||||||
.title("List")
|
.description(LIST_PRIVATE_DESC)
|
||||||
.title_style(group_box_title_style(cx))
|
.child(
|
||||||
.child(settings_row(
|
|
||||||
"Private on my list",
|
|
||||||
LIST_PRIVATE_DESC,
|
|
||||||
Switch::new("list-private")
|
Switch::new("list-private")
|
||||||
.checked(self.list_private)
|
.checked(self.list_private)
|
||||||
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
|
||||||
this.list_private = *checked;
|
this.list_private = *checked;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})),
|
})),
|
||||||
cx,
|
);
|
||||||
))
|
|
||||||
|
v_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(field_label("List", cx))
|
||||||
|
.child(switch_row(list_private, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_error(
|
fn render_error(
|
||||||
@@ -663,12 +662,13 @@ fn field_label(text: &'static str, cx: &App) -> Div {
|
|||||||
.child(text)
|
.child(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Match [`field_label`]'s design for GroupBox titles.
|
fn switch_row(switch: impl IntoElement, cx: &App) -> Div {
|
||||||
fn group_box_title_style(cx: &App) -> StyleRefinement {
|
div()
|
||||||
StyleRefinement::default()
|
.w_full()
|
||||||
.text_xs()
|
.p_2()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.rounded(cx.theme().radius)
|
||||||
.font_semibold()
|
.bg(cx.theme().secondary)
|
||||||
|
.child(switch)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn relay_label(relay: &RelayUrl) -> SharedString {
|
fn relay_label(relay: &RelayUrl) -> SharedString {
|
||||||
|
|||||||
@@ -424,19 +424,19 @@ impl Render for ProfileDialog {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.child(label("Name", cx))
|
.child(label("Name", cx))
|
||||||
.child(Input::new(&self.name_input)),
|
.child(Input::new(&self.name_input).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.child(label("About", cx))
|
.child(label("About", cx))
|
||||||
.child(Textarea::new(&self.bio_input)),
|
.child(Textarea::new(&self.bio_input).small()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.child(label("Website", cx))
|
.child(label("Website", cx))
|
||||||
.child(Input::new(&self.website_input)),
|
.child(Input::new(&self.website_input).small()),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(!self.editing, |this| {
|
.when(!self.editing, |this| {
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
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 std::collections::HashSet;
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
@@ -7,14 +15,6 @@ use gpui::{
|
|||||||
ParentElement, Render, SharedString, Stateful, Styled, Subscription, Task, TextAlign,
|
ParentElement, Render, SharedString, Stateful, Styled, Subscription, Task, TextAlign,
|
||||||
WeakEntity, Window, div, rems,
|
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 instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
@@ -119,7 +119,7 @@ impl RelayManager {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(Input::new(&self.gossip.input).cleanable(true))
|
.child(Input::new(&self.gossip.input).small().cleanable(true))
|
||||||
.child(
|
.child(
|
||||||
Button::new("metadata")
|
Button::new("metadata")
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
@@ -208,7 +208,7 @@ impl RelayManager {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(Input::new(&self.inbox.input).cleanable(true))
|
.child(Input::new(&self.inbox.input).small().cleanable(true))
|
||||||
.child(
|
.child(
|
||||||
Button::new("add-inbox")
|
Button::new("add-inbox")
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
use gpui::http_client::Url;
|
use gpui::http_client::Url;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Context, Entity, IntoElement, ParentElement, PathPromptOptions, Render,
|
App, AppContext, Context, Entity, IntoElement, ParentElement, PathPromptOptions, Render,
|
||||||
Styled, Subscription, Window, div,
|
SharedString, Styled, Window, div,
|
||||||
};
|
};
|
||||||
use gpui_kit::assets::IconName;
|
use gpui_kit::assets::IconName;
|
||||||
|
use gpui_kit::base::StyledExt as _;
|
||||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||||
use gpui_kit::component::input::{Input, InputEvent, InputState, NumberInput};
|
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::menu::{DropdownMenu, PopupMenuItem};
|
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
|
||||||
use gpui_kit::component::notification::Notification;
|
use gpui_kit::component::notification::Notification;
|
||||||
use gpui_kit::component::separator::Separator;
|
|
||||||
use gpui_kit::component::switch::Switch;
|
use gpui_kit::component::switch::Switch;
|
||||||
use gpui_kit::component::{Disableable, Sizable, WindowExt, h_flex, v_flex};
|
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
|
||||||
use settings::{AppSettings, AppearanceMode};
|
use settings::AppSettings;
|
||||||
use ui::{settings_block, settings_row};
|
use settings::AppearanceMode;
|
||||||
|
|
||||||
/// 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> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
||||||
cx.new(|cx| Preferences::new(window, cx))
|
cx.new(|cx| Preferences::new(window, cx))
|
||||||
@@ -24,11 +22,6 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
|||||||
|
|
||||||
pub struct Preferences {
|
pub struct Preferences {
|
||||||
file_input: Entity<InputState>,
|
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 {
|
impl Preferences {
|
||||||
@@ -40,91 +33,7 @@ impl Preferences {
|
|||||||
.placeholder("https://myblossom.com")
|
.placeholder("https://myblossom.com")
|
||||||
});
|
});
|
||||||
|
|
||||||
let theme = AppSettings::get_theme(cx);
|
Self { file_input }
|
||||||
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
|
/// Update the file server (blossom) URL
|
||||||
@@ -181,23 +90,15 @@ impl Render for Preferences {
|
|||||||
const SCREENING: &str = "Show an screening dialog to verify the unknown sender.";
|
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 WOT: &str = "Treat people followed by your contacts as known senders.";
|
||||||
const AVATAR: &str = "Hide all avatar pictures to improve performance.";
|
const AVATAR: &str = "Hide all avatar pictures to improve performance.";
|
||||||
const APPEARANCE: &str =
|
const MODE: &str = "Follow the OS, or always use light or dark.";
|
||||||
"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 NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages.";
|
||||||
const BACKGROUND: &str = "Show an image behind chat and group panels.";
|
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 screening = AppSettings::get_screening(cx);
|
||||||
let wot_auto_trust = AppSettings::get_wot_auto_trust(cx);
|
let wot_auto_trust = AppSettings::get_wot_auto_trust(cx);
|
||||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||||
let nip4e = AppSettings::get_nip4e(cx);
|
let nip4e = AppSettings::get_nip4e(cx);
|
||||||
let appearance_mode = AppSettings::get_appearance_mode(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 = AppSettings::get_panel_background(cx);
|
||||||
let panel_background_path = AppSettings::get_panel_background_path(cx);
|
let panel_background_path = AppSettings::get_panel_background_path(cx);
|
||||||
|
|
||||||
@@ -207,175 +108,185 @@ impl Render for Preferences {
|
|||||||
};
|
};
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.mt_2()
|
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.w_full()
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
GroupBox::new()
|
||||||
.w_full()
|
.id("general")
|
||||||
.gap_3()
|
.title("General")
|
||||||
.child(settings_row(
|
.fill()
|
||||||
"Screening",
|
.child(field().label("Screening").description(SCREENING).child(
|
||||||
SCREENING,
|
|
||||||
Switch::new("screening").checked(screening).on_click(
|
Switch::new("screening").checked(screening).on_click(
|
||||||
move |_, _window, cx| {
|
move |_, _window, cx| {
|
||||||
AppSettings::update_screening(!screening, cx);
|
AppSettings::update_screening(!screening, cx);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
cx,
|
|
||||||
))
|
))
|
||||||
.child(settings_row(
|
.child(
|
||||||
"Web of Trust",
|
field().label("Web of Trust").description(WOT).child(
|
||||||
WOT,
|
Switch::new("wot-auto-trust")
|
||||||
Switch::new("wot-auto-trust")
|
.checked(wot_auto_trust)
|
||||||
.checked(wot_auto_trust)
|
.on_click(move |_, _window, cx| {
|
||||||
.on_click(move |_, _window, cx| {
|
AppSettings::update_wot_auto_trust(!wot_auto_trust, cx);
|
||||||
AppSettings::update_wot_auto_trust(!wot_auto_trust, cx);
|
}),
|
||||||
}),
|
),
|
||||||
cx,
|
)
|
||||||
))
|
.child(field().label("Hide user avatar").description(AVATAR).child(
|
||||||
.child(settings_row(
|
|
||||||
"Hide user avatar",
|
|
||||||
AVATAR,
|
|
||||||
Switch::new("avatar").checked(hide_avatar).on_click(
|
Switch::new("avatar").checked(hide_avatar).on_click(
|
||||||
move |_, _window, cx| {
|
move |_, _window, cx| {
|
||||||
AppSettings::update_hide_avatar(!hide_avatar, 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(
|
.child(
|
||||||
v_flex()
|
GroupBox::new()
|
||||||
.w_full()
|
.id("appearance")
|
||||||
.gap_3()
|
.title("Appearance")
|
||||||
.child(settings_row(
|
.fill()
|
||||||
"Font Size",
|
.child(
|
||||||
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()
|
h_flex()
|
||||||
.justify_end()
|
.gap_3()
|
||||||
.gap_1()
|
.justify_between()
|
||||||
.child(
|
.child(
|
||||||
Button::new("choose-background")
|
v_flex()
|
||||||
.label("Choose image")
|
.child(div().text_xs().child(SharedString::from("Mode")))
|
||||||
.ghost()
|
.child(
|
||||||
.small()
|
div()
|
||||||
.disabled(!panel_background)
|
.text_xs()
|
||||||
.on_click(|_, _window, cx| {
|
.text_color(cx.theme().muted_foreground)
|
||||||
Self::choose_background(cx);
|
.child(SharedString::from(MODE)),
|
||||||
}),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("reset-background")
|
Button::new("appearance-mode")
|
||||||
.label("Reset")
|
.label(appearance_mode.label())
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.disabled(!panel_background || panel_background_path.is_none())
|
.dropdown_menu(|this, _window, _cx| {
|
||||||
.on_click(|_, _window, cx| {
|
AppearanceMode::ALL.iter().fold(this, |menu, mode| {
|
||||||
AppSettings::update_panel_background_path(None, cx);
|
menu.item(PopupMenuItem::new(mode.label()).on_click(
|
||||||
|
move |_, _window, cx| {
|
||||||
|
Self::set_appearance_mode(*mode, 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);
|
||||||
cx.refresh_windows();
|
cx.refresh_windows();
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
cx,
|
)
|
||||||
)),
|
|
||||||
)
|
|
||||||
.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(
|
.child(
|
||||||
Button::new("update-file-server")
|
h_flex()
|
||||||
.icon(IconName::Check)
|
.gap_3()
|
||||||
.ghost()
|
.justify_between()
|
||||||
.size_8()
|
.child(
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
v_flex()
|
||||||
this.update_file_server(window, cx)
|
.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(
|
||||||
|
field()
|
||||||
|
.label("Decoupling Encryption Key")
|
||||||
|
.description(NIP4E)
|
||||||
|
.child(Switch::new("nip4e").checked(nip4e).on_click(
|
||||||
|
move |_, _window, cx| {
|
||||||
|
AppSettings::update_nip4e(!nip4e, 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")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ impl CoopTabGroupSkin {
|
|||||||
this
|
this
|
||||||
} else {
|
} else {
|
||||||
this.text_color(cx.theme().tab_foreground)
|
this.text_color(cx.theme().tab_foreground)
|
||||||
.bg(cx.theme().list_hover)
|
.bg(cx.theme().tab)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.selected(active)
|
.selected(active)
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ impl Workspace {
|
|||||||
let view = settings::init(window, cx);
|
let view = settings::init(window, cx);
|
||||||
|
|
||||||
window.open_dialog(cx, move |this, _window, _cx| {
|
window.open_dialog(cx, move |this, _window, _cx| {
|
||||||
this.width(px(650.))
|
this.width(px(520.))
|
||||||
.close_button(true)
|
.close_button(true)
|
||||||
.pb_2()
|
.pb_2()
|
||||||
.title("Preferences")
|
.title("Preferences")
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pub struct Nav {
|
|||||||
suffix: Option<AnyElement>,
|
suffix: Option<AnyElement>,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
clickable: bool,
|
clickable: bool,
|
||||||
center_prefix: bool,
|
gap: bool,
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ impl Nav {
|
|||||||
suffix: None,
|
suffix: None,
|
||||||
selected: false,
|
selected: false,
|
||||||
clickable: false,
|
clickable: false,
|
||||||
center_prefix: false,
|
gap: true,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,9 +50,9 @@ impl Nav {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether to center the prefix in the avatar-width column.
|
/// Whether to space the row's contents apart.
|
||||||
pub fn center_prefix(mut self, center: bool) -> Self {
|
pub fn gap(mut self, gap: bool) -> Self {
|
||||||
self.center_prefix = center;
|
self.gap = gap;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,13 +115,14 @@ impl RenderOnce for Nav {
|
|||||||
.h_8()
|
.h_8()
|
||||||
.w_full()
|
.w_full()
|
||||||
.px_2()
|
.px_2()
|
||||||
.gap_2()
|
.when(self.gap, |this| this.gap_2())
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.when_some(self.prefix, |this, prefix| {
|
.when_some(self.prefix, |this, prefix| {
|
||||||
this.child(
|
this.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.w_6()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.when(self.center_prefix, |this| this.w_6().justify_center())
|
.justify_center()
|
||||||
.child(prefix),
|
.child(prefix),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ impl Render for BackupPanel {
|
|||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child(SharedString::from("Public Key:")),
|
.child(SharedString::from("Public Key:")),
|
||||||
)
|
)
|
||||||
.child(Input::new(&self.npub_input).disabled(true)),
|
.child(Input::new(&self.npub_input).small().disabled(true)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -171,7 +171,7 @@ impl Render for BackupPanel {
|
|||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child(SharedString::from("Secret Key:")),
|
.child(SharedString::from("Secret Key:")),
|
||||||
)
|
)
|
||||||
.child(Input::new(&self.nsec_input).disabled(true)),
|
.child(Input::new(&self.nsec_input).small().disabled(true)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("copy")
|
Button::new("copy")
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ impl Render for ContactListPanel {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(Input::new(&self.input).cleanable(true))
|
.child(Input::new(&self.input).small().cleanable(true))
|
||||||
.child(
|
.child(
|
||||||
Button::new("add")
|
Button::new("add")
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
|
|||||||
@@ -244,13 +244,13 @@ impl GreeterPanel {
|
|||||||
.seed(seed)
|
.seed(seed)
|
||||||
.small(),
|
.small(),
|
||||||
)
|
)
|
||||||
.child(div().min_w_0().truncate().text_sm().child(name))
|
.child(div().min_w_0().truncate().text_xs().child(name))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.truncate()
|
.truncate()
|
||||||
.text_sm()
|
.text_xs()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.italic()
|
.italic()
|
||||||
.child(preview),
|
.child(preview),
|
||||||
@@ -363,13 +363,13 @@ impl GreeterPanel {
|
|||||||
.seed(seed)
|
.seed(seed)
|
||||||
.small(),
|
.small(),
|
||||||
)
|
)
|
||||||
.child(div().min_w_0().truncate().text_sm().child(name))
|
.child(div().min_w_0().truncate().text_xs().child(name))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.truncate()
|
.truncate()
|
||||||
.text_sm()
|
.text_xs()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.italic()
|
.italic()
|
||||||
.child(preview),
|
.child(preview),
|
||||||
@@ -458,9 +458,13 @@ impl GreeterPanel {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
Nav::new("browse-group")
|
Nav::new("browse-group")
|
||||||
.prefix(Icon::new(IconName::Compass))
|
.prefix(
|
||||||
|
Icon::new(IconName::Compass)
|
||||||
|
.small()
|
||||||
|
.text_color(cx.theme().muted_foreground),
|
||||||
|
)
|
||||||
.label("Browse Public Groups")
|
.label("Browse Public Groups")
|
||||||
.text_sm()
|
.text_xs()
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
let panel = browse::init(this.dock.clone(), window, cx);
|
let panel = browse::init(this.dock.clone(), window, cx);
|
||||||
add_panel_to(
|
add_panel_to(
|
||||||
@@ -474,9 +478,9 @@ impl GreeterPanel {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Nav::new("browse-posts")
|
Nav::new("browse-posts")
|
||||||
.prefix(Icon::new(IconName::Link))
|
.prefix(Icon::new(IconName::Link).small())
|
||||||
.label("Browse Nostr via Jumble")
|
.label("Browse Nostr via Jumble")
|
||||||
.text_sm()
|
.text_xs()
|
||||||
.on_click(|_, _, cx| {
|
.on_click(|_, _, cx| {
|
||||||
cx.open_url(JUMBLE_URL);
|
cx.open_url(JUMBLE_URL);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::rc::Rc;
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, ClipboardItem, Context, Entity, InteractiveElement as _, IntoElement,
|
AnyElement, ClipboardItem, Context, Entity, InteractiveElement as _, IntoElement,
|
||||||
ParentElement, SharedString, Styled, WeakEntity, div, px, relative, uniform_list,
|
ParentElement, SharedString, Styled, WeakEntity, div, relative, uniform_list,
|
||||||
};
|
};
|
||||||
use gpui_kit::base::StyledExt as _;
|
use gpui_kit::base::StyledExt as _;
|
||||||
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
|
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
|
||||||
@@ -161,13 +161,13 @@ impl Sidebar {
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.truncate()
|
.truncate()
|
||||||
.line_height(relative(1.125))
|
.line_height(relative(1.2))
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child(name),
|
.child(name),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_size(px(8.0))
|
.text_xs()
|
||||||
.font_medium()
|
.font_medium()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child(total_members),
|
.child(total_members),
|
||||||
|
|||||||
@@ -87,10 +87,22 @@ impl Sidebar {
|
|||||||
.small()
|
.small()
|
||||||
.dropdown_menu(|menu, _window, _cx| {
|
.dropdown_menu(|menu, _window, _cx| {
|
||||||
menu.label("Public Group")
|
menu.label("Public Group")
|
||||||
.menu("New Group", Box::new(Command::NewGroup))
|
.menu_with_icon(
|
||||||
.menu("Join Group", Box::new(Command::JoinGroup))
|
"New Group",
|
||||||
|
IconName::Group,
|
||||||
|
Box::new(Command::NewGroup),
|
||||||
|
)
|
||||||
|
.menu_with_icon(
|
||||||
|
"Join Group",
|
||||||
|
IconName::Door,
|
||||||
|
Box::new(Command::JoinGroup),
|
||||||
|
)
|
||||||
.separator()
|
.separator()
|
||||||
.menu("New Chat", Box::new(Command::NewChat))
|
.menu_with_icon(
|
||||||
|
"New Chat",
|
||||||
|
IconName::Message,
|
||||||
|
Box::new(Command::NewChat),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.when(self.group_open, |this| {
|
.when(self.group_open, |this| {
|
||||||
@@ -119,7 +131,6 @@ impl Sidebar {
|
|||||||
.child(
|
.child(
|
||||||
Nav::new("sidebar-settings")
|
Nav::new("sidebar-settings")
|
||||||
.prefix(Icon::new(IconName::Settings).small())
|
.prefix(Icon::new(IconName::Settings).small())
|
||||||
.center_prefix(true)
|
|
||||||
.label("Settings")
|
.label("Settings")
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.on_click(|_, window, cx| {
|
.on_click(|_, window, cx| {
|
||||||
|
|||||||
@@ -193,8 +193,9 @@ impl Sidebar {
|
|||||||
.h_20()
|
.h_20()
|
||||||
.w_full()
|
.w_full()
|
||||||
.px_4()
|
.px_4()
|
||||||
.bg(cx.theme().sidebar)
|
.border_t_1()
|
||||||
.text_color(cx.theme().sidebar_foreground)
|
.border_color(cx.theme().border)
|
||||||
|
.bg(cx.theme().background)
|
||||||
.child(
|
.child(
|
||||||
Button::new("create-chat")
|
Button::new("create-chat")
|
||||||
.label(if self.selected_contacts.len() > 1 {
|
.label(if self.selected_contacts.len() > 1 {
|
||||||
@@ -212,9 +213,7 @@ impl Sidebar {
|
|||||||
.gap_1()
|
.gap_1()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.text_center()
|
.child("Selected")
|
||||||
.justify_center()
|
|
||||||
.child("Selected:")
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ impl Sidebar {
|
|||||||
let create_button = contacts_open && !self.selected_contacts.is_empty();
|
let create_button = contacts_open && !self.selected_contacts.is_empty();
|
||||||
|
|
||||||
let (list_id, title) = if contacts_open {
|
let (list_id, title) = if contacts_open {
|
||||||
("sidebar-contacts", Some("Contacts"))
|
("sidebar-contacts", Some(("Contacts", IconName::Book)))
|
||||||
} else if requests_open {
|
} else if requests_open {
|
||||||
("sidebar-requests", Some("Requests"))
|
("sidebar-requests", Some(("Requests", IconName::Invite)))
|
||||||
} else {
|
} else {
|
||||||
("sidebar-list", None)
|
("sidebar-list", None)
|
||||||
};
|
};
|
||||||
@@ -119,6 +119,7 @@ impl Sidebar {
|
|||||||
Nav::new("contacts")
|
Nav::new("contacts")
|
||||||
.prefix(Icon::new(IconName::Book).small())
|
.prefix(Icon::new(IconName::Book).small())
|
||||||
.label("Contacts")
|
.label("Contacts")
|
||||||
|
.gap(false)
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.when(contacts_open, |this| this.bg(cx.theme().list_active))
|
.when(contacts_open, |this| this.bg(cx.theme().list_active))
|
||||||
.on_click(cx.listener(|this, _event, window, cx| {
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
@@ -126,17 +127,21 @@ impl Sidebar {
|
|||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.when_some(title, |this, title| {
|
.when_some(title, |this, (title, title_icon)| {
|
||||||
this.child(
|
this.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.px_4()
|
|
||||||
.h_10()
|
.h_10()
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.items_center()
|
||||||
.text_xs()
|
.child(
|
||||||
.font_semibold()
|
h_flex()
|
||||||
.child(title),
|
.px_2()
|
||||||
|
.gap_2()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child(Icon::new(title_icon).small())
|
||||||
|
.child(div().text_xs().font_semibold().child(title)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -292,7 +292,6 @@ impl Render for Sidebar {
|
|||||||
|
|
||||||
let group = self.group.clone().and_then(|group| group.upgrade());
|
let group = self.group.clone().and_then(|group| group.upgrade());
|
||||||
let searching = self.search_open && !self.find_input.read(cx).value().is_empty();
|
let searching = self.search_open && !self.find_input.read(cx).value().is_empty();
|
||||||
let picking_contacts = self.contacts_open && !self.requests_open;
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.image_cache(retain_all("sidebar"))
|
.image_cache(retain_all("sidebar"))
|
||||||
@@ -342,9 +341,7 @@ impl Render for Sidebar {
|
|||||||
_ => this.child(self.render_list(cx)),
|
_ => this.child(self.render_list(cx)),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.when(logged_in && !picking_contacts, |this| {
|
.when(logged_in, |this| this.child(self.render_footer(cx)))
|
||||||
this.child(self.render_footer(cx))
|
|
||||||
})
|
|
||||||
.when(loading && logged_in, |this| {
|
.when(loading && logged_in, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ impl Sidebar {
|
|||||||
.map(|(offset, row)| {
|
.map(|(offset, row)| {
|
||||||
let index = range.start + offset;
|
let index = range.start + offset;
|
||||||
|
|
||||||
let element = match row {
|
match row {
|
||||||
SidebarRow::Section(section) => {
|
SidebarRow::Section(section) => {
|
||||||
let section = *section;
|
let section = *section;
|
||||||
let label = section.label();
|
let label = section.label();
|
||||||
@@ -394,9 +394,7 @@ impl Sidebar {
|
|||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
// Keep a small gap between rows so selected rows read as pills.
|
|
||||||
div().pb_0p5().child(element).into_any_element()
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ product-name = "Coop"
|
|||||||
description = "Chat Freely, Stay Private on Nostr"
|
description = "Chat Freely, Stay Private on Nostr"
|
||||||
identifier = "su.reya.coop"
|
identifier = "su.reya.coop"
|
||||||
category = "SocialNetworking"
|
category = "SocialNetworking"
|
||||||
version = "2.0.0-alpha2"
|
version = "2.0.0-alpha"
|
||||||
out-dir = "../dist"
|
out-dir = "../dist"
|
||||||
before-packaging-command = "cargo build --release"
|
before-packaging-command = "cargo build --release"
|
||||||
resources = ["Cargo.toml", "src"]
|
resources = ["Cargo.toml", "src"]
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ use workspace::TAB_BAR_HEIGHT;
|
|||||||
|
|
||||||
actions!(coop, [Quit]);
|
actions!(coop, [Quit]);
|
||||||
|
|
||||||
|
const BASE_FONT_SIZE: f32 = 16.0;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Initialize logging
|
// Initialize logging
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
@@ -64,13 +66,11 @@ fn main() {
|
|||||||
settings::init(cx);
|
settings::init(cx);
|
||||||
|
|
||||||
let appearance = settings::persisted_appearance_mode();
|
let appearance = settings::persisted_appearance_mode();
|
||||||
let font_size = settings::persisted_theme().font_size;
|
match apply_native_theme(appearance, cx) {
|
||||||
match apply_native_theme(appearance, font_size, cx) {
|
|
||||||
Ok(()) => start_theme_watcher(appearance, cx),
|
Ok(()) => start_theme_watcher(appearance, cx),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log::warn!("native theme unavailable ({error})");
|
log::warn!("native theme unavailable ({error})");
|
||||||
apply_appearance(appearance, cx);
|
apply_appearance(appearance, cx);
|
||||||
AppSettings::apply_theme_style(cx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,30 +171,22 @@ fn quit(_ev: &Quit, cx: &mut App) {
|
|||||||
cx.quit();
|
cx.quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply the native theme to the app, scaled to the user's font size,
|
/// Apply the native theme to the app, and then apply the persisted appearance mode
|
||||||
/// then layer the persisted theme tuning on top
|
fn apply_native_theme(appearance: AppearanceMode, cx: &mut App) -> native_theme::Result<()> {
|
||||||
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 mut system = native_theme_gpui::SystemTheme::from_system()?;
|
||||||
let native = system.light.defaults.font.size;
|
let native = system.light.defaults.font.size;
|
||||||
|
|
||||||
if native > 0.0 {
|
if native > 0.0 {
|
||||||
system.accessibility.text_scaling_factor *= font_size / native;
|
system.accessibility.text_scaling_factor *= BASE_FONT_SIZE / native;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_system_theme(&system, cx);
|
apply_system_theme(&system, cx);
|
||||||
apply_appearance(appearance, 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(())
|
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) {
|
fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
|
||||||
match appearance {
|
match appearance {
|
||||||
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
||||||
@@ -230,11 +222,10 @@ fn start_theme_watcher(initial_appearance: AppearanceMode, cx: &mut App) {
|
|||||||
log::info!("OS theme changed; re-applying");
|
log::info!("OS theme changed; re-applying");
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let appearance = AppSettings::get_appearance_mode(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>() {
|
if let Some(watcher) = cx.try_global::<ThemeWatcher>() {
|
||||||
watcher.appearance.set(appearance);
|
watcher.appearance.set(appearance);
|
||||||
}
|
}
|
||||||
if let Err(error) = apply_native_theme(appearance, font_size, cx) {
|
if let Err(error) = apply_native_theme(appearance, cx) {
|
||||||
log::warn!("failed to apply updated OS theme ({error})");
|
log::warn!("failed to apply updated OS theme ({error})");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||