6 Commits
Author SHA1 Message Date
reya 2a50804583 update tabbar 2026-09-24 13:54:04 +07:00
reya 610bf816b0 update 2026-09-24 08:39:02 +07:00
reya c61be4a139 update sidebar 2026-09-23 20:19:42 +07:00
reya 273ddabde5 clean up the sidebar 2026-09-23 14:54:34 +07:00
reya 7ba289440b refactor panel open flow 2026-09-23 08:53:20 +07:00
reya 6510e45e4e update deps 2026-09-23 08:21:37 +07:00
24 changed files with 1067 additions and 1563 deletions
Generated
+194 -105
View File
@@ -987,9 +987,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.4.6" version = "1.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@@ -1339,7 +1339,7 @@ dependencies = [
"hmac 0.12.1", "hmac 0.12.1",
"nostr", "nostr",
"nostr-sdk", "nostr-sdk",
"rand 0.10.2", "rand 0.10.3",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.10.9", "sha2 0.10.9",
@@ -2190,9 +2190,9 @@ dependencies = [
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.12" version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b"
[[package]] [[package]]
name = "fixedbitset" name = "fixedbitset"
@@ -2711,14 +2711,16 @@ dependencies = [
[[package]] [[package]]
name = "gpui-base" name = "gpui-base"
version = "0.6.1" version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d45dcaaeac889bf1e7757db1beb26c9043c8ea3156651facc11c6be56bb6722" checksum = "c9e904ee0d351f4e85b967e26ae986d3aa195a8d9d35fcd18184a2c40989694e"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"anyhow", "anyhow",
"ashpd",
"async-channel", "async-channel",
"chrono", "chrono",
"data-url",
"futures", "futures",
"gpui-pre", "gpui-pre",
"gpui-pre-macros", "gpui-pre-macros",
@@ -2742,13 +2744,14 @@ dependencies = [
"tracing", "tracing",
"unicode-segmentation", "unicode-segmentation",
"web-time", "web-time",
"windows 0.58.0",
] ]
[[package]] [[package]]
name = "gpui-pre" name = "gpui-pre"
version = "0.3.5" 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 = "2a9ca98fdcad06276de623c2f48ebd710f1e7724eebef2f2673c583e85dc8335" checksum = "a0437c0b83e636a92bd1a39fa1d05fb632ae671289537497b35871ffbe231b84"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"anyhow", "anyhow",
@@ -2774,7 +2777,6 @@ dependencies = [
"gpui-pre-sum-tree", "gpui-pre-sum-tree",
"gpui-pre-util", "gpui-pre-util",
"gpui-pre-util-macros", "gpui-pre-util-macros",
"gpui-pre-ztracing",
"heapless 0.9.3", "heapless 0.9.3",
"image", "image",
"inventory", "inventory",
@@ -2782,6 +2784,8 @@ dependencies = [
"log", "log",
"lyon", "lyon",
"num_cpus", "num_cpus",
"objc2-core-foundation",
"objc2-core-video",
"parking", "parking",
"parking_lot", "parking_lot",
"pin-project", "pin-project",
@@ -2802,7 +2806,6 @@ dependencies = [
"strum", "strum",
"taffy", "taffy",
"thiserror 2.0.20", "thiserror 2.0.20",
"tracing",
"ttf-parser", "ttf-parser",
"url", "url",
"usvg", "usvg",
@@ -2816,9 +2819,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-apple" name = "gpui-pre-apple"
version = "0.3.5" 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 = "d3fc4da82c5c2dbb25443655cfbc4305a2e2d1380bc0b815e80664fdfbf4cfdb" checksum = "3af534f0746eb9e014114bcc0ee00dc250c4b8d75df2e437180c403aa65649c1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"block", "block",
@@ -2830,7 +2833,6 @@ dependencies = [
"etagere", "etagere",
"foreign-types", "foreign-types",
"gpui-pre", "gpui-pre",
"gpui-pre-collections",
"image", "image",
"log", "log",
"metal", "metal",
@@ -2841,9 +2843,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-collections" name = "gpui-pre-collections"
version = "0.3.5" 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 = "626a09f683ff6f3b2f708341bdcea9d479af5b14a6a2fccec9cff935ec0751b7" checksum = "15e7a65204039187d1f4b3d4c2e0233eeac54d4acb0b13f9dc1a3b5adad26c1f"
dependencies = [ dependencies = [
"gpui-pre-util", "gpui-pre-util",
"indexmap", "indexmap",
@@ -2852,9 +2854,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-derive-refineable" name = "gpui-pre-derive-refineable"
version = "0.3.5" 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 = "56d06116a2b857588f6ea7bfb447b3554ba985e780b5995ae47780252da152f3" checksum = "79dfb4a05f3cd9893d7768dbf5dba1bbbe99cab7394e89b360ccb3b2bcb6b34e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2863,9 +2865,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-http-client" name = "gpui-pre-http-client"
version = "0.3.5" 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 = "b0d2071c582c1a0f71b8c7c00607a3b9cd18f81fb3510d41845db3447b2a11c9" checksum = "cfd5a7542c77887a5aff997d78c95884119c8463409569b63fcf060956da1682"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-compression", "async-compression",
@@ -2884,9 +2886,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-http-client-tls" name = "gpui-pre-http-client-tls"
version = "0.3.5" 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 = "6b6f3936802580259d5af9bcb2ab37fb0d7002caeed449eaabb007c62aea7341" checksum = "72aed510adb5e7723f8f57b9751007e3451d46a84edb59b92227178cf59cc19c"
dependencies = [ dependencies = [
"log", "log",
"rustls", "rustls",
@@ -2896,9 +2898,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-linux" name = "gpui-pre-linux"
version = "0.3.5" 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 = "f6b427589681ec1fed27951fd82edc70a7e288bb2487ede26dbbd07c2ce23d4f" checksum = "aab655cda1d6dcd549ac869e8678babc84cf573ab6794a1fe1a39c6c70c60b48"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"accesskit_unix", "accesskit_unix",
@@ -2943,15 +2945,14 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-macos" name = "gpui-pre-macos"
version = "0.3.5" 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 = "806eac0cb5c0eebc0b032c6450c11d283264f9ac05754dda83fc80d0b86b4ee8" checksum = "951d17e41a72067ad2d35c60e08a62e8ad7fa9400b41c802e26cfb459f6c05da"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"accesskit_macos", "accesskit_macos",
"anyhow", "anyhow",
"async-task", "async-task",
"block",
"block2 0.6.2", "block2 0.6.2",
"cocoa 0.26.1", "cocoa 0.26.1",
"core-foundation 0.10.1", "core-foundation 0.10.1",
@@ -2965,17 +2966,17 @@ dependencies = [
"gpui-pre", "gpui-pre",
"gpui-pre-apple", "gpui-pre-apple",
"gpui-pre-collections", "gpui-pre-collections",
"gpui-pre-media",
"gpui-pre-util", "gpui-pre-util",
"image", "image",
"itertools 0.14.0", "itertools 0.14.0",
"libc", "libc",
"log", "log",
"mach2", "mach2",
"metal",
"objc", "objc",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-app-kit 0.3.2", "objc2-app-kit 0.3.2",
"objc2-core-graphics",
"objc2-core-media",
"objc2-foundation 0.3.2", "objc2-foundation 0.3.2",
"objc2-screen-capture-kit", "objc2-screen-capture-kit",
"objc2-user-notifications", "objc2-user-notifications",
@@ -2991,9 +2992,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-macros" name = "gpui-pre-macros"
version = "0.3.5" 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 = "c06139051bf1db51949bfbb4ee338208bd64b4c35340d8c1a6a8200744e28fe1" checksum = "6324092b40ea8ec7c327a4a28427e366a489b4d5d23b5d1bd0243fc2f4ca7a9e"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro-crate", "proc-macro-crate",
@@ -3002,26 +3003,11 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "gpui-pre-media"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "500acb13f818ff234021c1b9eae8592b0cbe017a64e26908c736b7421b1c6213"
dependencies = [
"anyhow",
"bindgen",
"core-foundation 0.10.1",
"core-video",
"foreign-types",
"metal",
"objc",
]
[[package]] [[package]]
name = "gpui-pre-perf" name = "gpui-pre-perf"
version = "0.3.5" 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 = "b0973bbb02bf46940ac0861d9dce0fc03a3109634172b69108d1bc13fa29d76c" checksum = "b94bff26ccd8105f6206144f8dab16a2dc02e7a68f39dada02cdf492f0afd940"
dependencies = [ dependencies = [
"gpui-pre-collections", "gpui-pre-collections",
"serde", "serde",
@@ -3030,9 +3016,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-platform" name = "gpui-pre-platform"
version = "0.3.5" 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 = "b613512e29f1e7b825bfa7e50a447388ff1347044350a475e4c98a892a985c8d" checksum = "8423d1e0a693d2f9f16c0326bb01b5e81ec0531854d596a18476934ac6880478"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"gpui-pre", "gpui-pre",
@@ -3044,9 +3030,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-refineable" name = "gpui-pre-refineable"
version = "0.3.5" 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 = "428f23703ead5601618ffea73c137897b98a94ce8d30e257f22be947732dfbf9" checksum = "93b4b0a8d296a3ee5beb1136bc48f8167a6028eb1e243b56851dcd8c9fc6d7a1"
dependencies = [ dependencies = [
"gpui-pre-derive-refineable", "gpui-pre-derive-refineable",
] ]
@@ -3103,9 +3089,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-reqwest-client" name = "gpui-pre-reqwest-client"
version = "0.3.5" 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 = "d05d4e3d2eb2ab777597879e46047247447b9d93cef568e63d545c9deb1b3e57" checksum = "8f32ccea5d1a7c8d2868d7b346c2c1f76aa3f86ec9e0575c4e7340af3afd9e9d"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -3121,9 +3107,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-scheduler" name = "gpui-pre-scheduler"
version = "0.3.5" 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 = "6dd1b7ff2e7bf43302ae982cd786a22c16c52e16682e67b66cfbe2cb2bc3a72d" checksum = "ff7b10b3d2fc3fda24847a04436eca9531612c8414874a37ddc005f20e521e12"
dependencies = [ dependencies = [
"async-task", "async-task",
"backtrace", "backtrace",
@@ -3138,9 +3124,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-shared-string" name = "gpui-pre-shared-string"
version = "0.3.5" 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 = "424a4dd6a570473d57acf1df2bfb9f3028d4b568abcaeb9252e0f13047b50c03" checksum = "fe17f81bd98a0253140ea79f7f76442d7499bd8d17db302861af8e46c11d38df"
dependencies = [ dependencies = [
"schemars", "schemars",
"serde", "serde",
@@ -3149,9 +3135,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-sum-tree" name = "gpui-pre-sum-tree"
version = "0.3.5" 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 = "4855718e82630d29198b515d2b027361ef63dd19b6021cefe945789dcff82372" checksum = "0a6d35e998a51d24248dad731de1f09d386cbbf46a04edde3b67f469b2ce3f16"
dependencies = [ dependencies = [
"gpui-pre-ztracing", "gpui-pre-ztracing",
"heapless 0.9.3", "heapless 0.9.3",
@@ -3162,9 +3148,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-util" name = "gpui-pre-util"
version = "0.3.5" 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 = "e973148165346c0f9b3f5072b321d7c2f33807c93daea67fe298c005c0e0c2a8" checksum = "6a026a30c578930d20974fd8c3d77de36223ac7ba193bd4259e7238929bc8806"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"log", "log",
@@ -3173,9 +3159,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-util-macros" name = "gpui-pre-util-macros"
version = "0.3.5" 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 = "4f71f6212d3772a70988c7d64627c7284dec73c17992ac4f9f0cdfdd46cbb677" checksum = "5cbf79627a6afc626d4e887dad9d2d92bb069413e8cc692ba3e90bff219eb7a9"
dependencies = [ dependencies = [
"gpui-pre-perf", "gpui-pre-perf",
"quote", "quote",
@@ -3184,9 +3170,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-web" name = "gpui-pre-web"
version = "0.3.5" 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 = "4f0daada903f106be946d87e25f5bc85ad6cf60d6d82cb8f8aed945417a9c1a3" checksum = "34be433b9fcb370b1fe9a1c7fe911f0eb6058791ef8167dcf6c5a5deb624975c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"console_error_panic_hook", "console_error_panic_hook",
@@ -3213,9 +3199,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-wgpu" name = "gpui-pre-wgpu"
version = "0.3.5" 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 = "640b666a16ddf9504e2eb3e7a997acb2b2adfaa7859925bdd36fc3acfd8f30a7" checksum = "05515b32799b1767d69300921c1b562cd53104965211a0e8905acdc7aa18e693"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytemuck", "bytemuck",
@@ -3240,9 +3226,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-windows" name = "gpui-pre-windows"
version = "0.3.5" 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 = "c12c7d84ec422feb2f064117d572447998381e5c219fcad69fe19f10d0b7aaea" checksum = "27662e65bfcf4445d2cccfad91f1570ec07f4b1b5de98400a9f207fd19c464cf"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"accesskit_windows", "accesskit_windows",
@@ -3269,9 +3255,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-zlog" name = "gpui-pre-zlog"
version = "0.3.5" 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 = "98352858fac3d4d05b46d6014ad04bad313652a691d4e21dc55fa96fb4acb7cf" checksum = "f3a5af0b734a391a84872eb770b3ee9830091ec2487b79d06371fe14eefb2b5d"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -3281,9 +3267,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-ztracing" name = "gpui-pre-ztracing"
version = "0.3.5" 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 = "5d63a3c7276875b4d7e6d3961e92a138e9233e86a591bf318f2e54df4ad58102" checksum = "a24cb19f78c5bc3aab8ddf1e8e0bd6b402fbcb9c7f112f64ddbedef8f8768a59"
dependencies = [ dependencies = [
"gpui-pre-zlog", "gpui-pre-zlog",
"gpui-pre-ztracing-macro", "gpui-pre-ztracing-macro",
@@ -3293,9 +3279,9 @@ dependencies = [
[[package]] [[package]]
name = "gpui-pre-ztracing-macro" name = "gpui-pre-ztracing-macro"
version = "0.3.5" 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 = "84e2a5ff79002223edfcfa76c6f9f79f4f60f6ec545ccc27832f12bd46348b39" checksum = "a1bb25850ad31b88bbdef454afb93ec4cdb81f5e200ba5b3f1a0368111780d2c"
[[package]] [[package]]
name = "gpui-updater-core" name = "gpui-updater-core"
@@ -3621,9 +3607,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.27.9" version = "0.27.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53"
dependencies = [ dependencies = [
"http", "http",
"hyper", "hyper",
@@ -4131,9 +4117,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]] [[package]]
name = "libredox" name = "libredox"
version = "0.1.24" version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@@ -4617,8 +4603,8 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.45.4" version = "0.45.5"
source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336"
dependencies = [ dependencies = [
"aes", "aes",
"base64 0.22.1", "base64 0.22.1",
@@ -4630,7 +4616,7 @@ dependencies = [
"chacha20poly1305", "chacha20poly1305",
"faster-hex", "faster-hex",
"opaquerr 0.2.0", "opaquerr 0.2.0",
"rand 0.10.2", "rand 0.10.3",
"scrypt", "scrypt",
"secp256k1", "secp256k1",
"serde", "serde",
@@ -4643,8 +4629,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-blossom" name = "nostr-blossom"
version = "0.45.0" version = "0.45.1"
source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bitcoin_hashes 1.2.0", "bitcoin_hashes 1.2.0",
@@ -4656,8 +4642,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.45.1" version = "0.45.2"
source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"futures-core", "futures-core",
@@ -4670,8 +4656,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.45.1" version = "0.45.2"
source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336"
dependencies = [ dependencies = [
"nostr", "nostr",
"opaquerr 0.2.0", "opaquerr 0.2.0",
@@ -4679,8 +4665,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-gossip" name = "nostr-gossip"
version = "0.45.0" version = "0.45.1"
source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336"
dependencies = [ dependencies = [
"nostr", "nostr",
"opaquerr 0.2.0", "opaquerr 0.2.0",
@@ -4688,8 +4674,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-gossip-memory" name = "nostr-gossip-memory"
version = "0.45.0" version = "0.45.1"
source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"lru", "lru",
@@ -4700,8 +4686,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.45.2" version = "0.45.3"
source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"flatbuffers", "flatbuffers",
@@ -4715,8 +4701,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-memory" name = "nostr-memory"
version = "0.45.1" version = "0.45.2"
source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336"
dependencies = [ dependencies = [
"btreecap", "btreecap",
"nostr", "nostr",
@@ -4726,8 +4712,8 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.45.2" version = "0.45.4"
source = "git+https://github.com/rust-nostr/nostr#b230cecf9dbb38e0228e6fff4544ed9d261326fc" source = "git+https://github.com/rust-nostr/nostr#01f1950c671b35f3d1186ca907373dd86ee3b336"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -4739,7 +4725,7 @@ dependencies = [
"nostr-database", "nostr-database",
"nostr-gossip", "nostr-gossip",
"opaquerr 0.2.0", "opaquerr 0.2.0",
"rand 0.10.2", "rand 0.10.3",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tracing", "tracing",
@@ -4990,6 +4976,28 @@ dependencies = [
"objc2-foundation 0.3.2", "objc2-foundation 0.3.2",
] ]
[[package]]
name = "objc2-core-audio"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"
dependencies = [
"dispatch2",
"objc2 0.6.4",
"objc2-core-audio-types",
"objc2-core-foundation",
]
[[package]]
name = "objc2-core-audio-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
dependencies = [
"bitflags 2.13.2",
"objc2 0.6.4",
]
[[package]] [[package]]
name = "objc2-core-data" name = "objc2-core-data"
version = "0.2.2" version = "0.2.2"
@@ -5069,6 +5077,21 @@ dependencies = [
"objc2-foundation 0.3.2", "objc2-foundation 0.3.2",
] ]
[[package]]
name = "objc2-core-media"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d"
dependencies = [
"bitflags 2.13.2",
"dispatch2",
"objc2 0.6.4",
"objc2-core-audio",
"objc2-core-audio-types",
"objc2-core-foundation",
"objc2-core-video",
]
[[package]] [[package]]
name = "objc2-core-text" name = "objc2-core-text"
version = "0.3.2" version = "0.3.2"
@@ -5193,8 +5216,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74b7c5390f477482f001bc354d6571a70db7e4f8d5288e860c45521fbce11394" checksum = "74b7c5390f477482f001bc354d6571a70db7e4f8d5288e860c45521fbce11394"
dependencies = [ dependencies = [
"block2 0.6.2", "block2 0.6.2",
"dispatch2",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-core-graphics", "objc2-core-graphics",
"objc2-core-media",
"objc2-foundation 0.3.2", "objc2-foundation 0.3.2",
] ]
@@ -5902,7 +5927,7 @@ dependencies = [
"bytes", "bytes",
"getrandom 0.4.3", "getrandom 0.4.3",
"lru-slab", "lru-slab",
"rand 0.10.2", "rand 0.10.3",
"rand_pcg", "rand_pcg",
"ring", "ring",
"rustc-hash 2.1.3", "rustc-hash 2.1.3",
@@ -5973,9 +5998,9 @@ dependencies = [
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.10.2" version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
dependencies = [ dependencies = [
"chacha20 0.10.2", "chacha20 0.10.2",
"getrandom 0.4.3", "getrandom 0.4.3",
@@ -8004,9 +8029,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
[[package]] [[package]]
name = "unicode-id" name = "unicode-id"
version = "0.3.6" version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580" checksum = "65dd75ab2d9d379dbe86d4059e91d8b5a6e7054865eaba1ddbfc7a817ff96392"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
@@ -8790,6 +8815,16 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.61.3" version = "0.61.3"
@@ -8858,6 +8893,19 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.61.2" version = "0.61.2"
@@ -8917,6 +8965,17 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]] [[package]]
name = "windows-implement" name = "windows-implement"
version = "0.60.2" version = "0.60.2"
@@ -8939,6 +8998,17 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]] [[package]]
name = "windows-interface" name = "windows-interface"
version = "0.59.3" version = "0.59.3"
@@ -9013,6 +9083,15 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.3.4" version = "0.3.4"
@@ -9031,6 +9110,16 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result 0.2.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-strings" name = "windows-strings"
version = "0.3.1" version = "0.3.1"
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M2.75 12C2.75 17.1086 6.89137 21.25 12 21.25C17.1086 21.25 21.25 17.1086 21.25 12C21.25 6.89137 17.1086 2.75 12 2.75C6.89137 2.75 2.75 6.89137 2.75 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.25 13L12 16.25L8.75 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 7.75V15.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 571 B

+1 -1
View File
@@ -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 fill-rule="evenodd" clip-rule="evenodd" d="M9 5.5V18.5H19.25C19.9404 18.5 20.5 17.9404 20.5 17.25V6.75C20.5 6.05964 19.9404 5.5 19.25 5.5H9ZM2 6.75C2 5.23122 3.23122 4 4.75 4H19.25C20.7688 4 22 5.23122 22 6.75V17.25C22 18.7688 20.7688 20 19.25 20H4.75C3.23122 20 2 18.7688 2 17.25V6.75Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M4.75 4C3.23122 4 2 5.23122 2 6.75V17.25C2 18.7688 3.23122 20 4.75 20H19.25C20.7688 20 22 18.7688 22 17.25V6.75C22 5.23122 20.7688 4 19.25 4H4.75ZM3.5 6.75C3.5 6.05964 4.05964 5.5 4.75 5.5H10.5V18.5H4.75C4.05964 18.5 3.5 17.9404 3.5 17.25V6.75Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M7 9.5C6.44772 9.5 6 9.05228 6 8.5C6 7.94772 6.44772 7.5 7 7.5C7.55228 7.5 8 7.94772 8 8.5C8 9.05228 7.55228 9.5 7 9.5ZM7 13C6.44772 13 6 12.5523 6 12C6 11.4477 6.44772 11 7 11C7.55228 11 8 11.4477 8 12C8 12.5523 7.55228 13 7 13ZM7 16.5C6.44772 16.5 6 16.0523 6 15.5C6 14.9477 6.44772 14.5 7 14.5C7.55228 14.5 8 14.9477 8 15.5C8 16.0523 7.55228 16.5 7 16.5Z" fill="currentColor"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 396 B

After

Width:  |  Height:  |  Size: 826 B

+1 -1
View File
@@ -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="M2.75 6.75C2.75 5.64543 3.64543 4.75 4.75 4.75H19.25C20.3546 4.75 21.25 5.64543 21.25 6.75V17.25C21.25 18.3546 20.3546 19.25 19.25 19.25H4.75C3.64543 19.25 2.75 18.3546 2.75 17.25V6.75Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M8.25 5V12V19" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/> <path d="M11.25 4.75H4.75C3.64543 4.75 2.75 5.64543 2.75 6.75V17.25C2.75 18.3546 3.64543 19.25 4.75 19.25H11.25M11.25 4.75H19.25C20.3546 4.75 21.25 5.64543 21.25 6.75V17.25C21.25 18.3546 20.3546 19.25 19.25 19.25H11.25M11.25 4.75V19.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/><path d="M6.25 8.5C6.25 8.91421 6.58579 9.25 7 9.25C7.41421 9.25 7.75 8.91421 7.75 8.5C7.75 8.08579 7.41421 7.75 7 7.75C6.58579 7.75 6.25 8.08579 6.25 8.5ZM6.25 12C6.25 12.4142 6.58579 12.75 7 12.75C7.41421 12.75 7.75 12.4142 7.75 12C7.75 11.5858 7.41421 11.25 7 11.25C6.58579 11.25 6.25 11.5858 6.25 12ZM6.25 15.5C6.25 15.9142 6.58579 16.25 7 16.25C7.41421 16.25 7.75 15.9142 7.75 15.5C7.75 15.0858 7.41421 14.75 7 14.75C6.58579 14.75 6.25 15.0858 6.25 15.5Z" fill="currentColor" stroke="currentColor" stroke-width="0.5"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 931 B

+6 -1
View File
@@ -31,7 +31,7 @@ fn uses_managed_updates() -> bool {
// Allow opting out of in-app updates via an explicit environment variable. // Allow opting out of in-app updates via an explicit environment variable.
|| std::env::var(COOP_UPDATE_EXPLANATION).is_ok() || std::env::var(COOP_UPDATE_EXPLANATION).is_ok()
// The Snap package sets `COOP_BUNDLE_TYPE=snap` (see snapcraft.yaml.in). // The Snap package sets `COOP_BUNDLE_TYPE=snap` (see snapcraft.yaml.in).
|| std::env::var(COOP_BUNDLE_TYPE).is_ok_and(|value| value == "snap") || std::env::var(COOP_BUNDLE_TYPE).is_ok_and(|value | value == "snap")
} }
/// Initialize the auto-update system. /// Initialize the auto-update system.
@@ -129,6 +129,11 @@ impl AutoUpdater {
matches!(self.status, UpdateStatus::Idle) matches!(self.status, UpdateStatus::Idle)
} }
/// Whether the running version is the newest release, so the status line can be hidden.
pub fn up_to_date(&self) -> bool {
matches!(self.status, UpdateStatus::Idle | UpdateStatus::UpToDate)
}
/// Whether a verified update is installed and waiting for a restart. /// Whether a verified update is installed and waiting for a restart.
pub fn staged(&self) -> bool { pub fn staged(&self) -> bool {
matches!(self.status, UpdateStatus::Staged(_)) matches!(self.status, UpdateStatus::Staged(_))
+5 -30
View File
@@ -9,7 +9,7 @@ use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::skim::SkimMatcherV2;
use gpui::{ use gpui::{
App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task, App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task,
WeakEntity, Window, WeakEntity,
}; };
use instant::Duration; use instant::Duration;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
@@ -37,15 +37,8 @@ impl Global for GlobalChatRegistry {}
/// Chat event. /// Chat event.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChatEvent { pub enum ChatEvent {
/// An event to open a room by its ID
OpenRoom(u64),
/// An event to close a room by its ID
CloseRoom(u64),
/// An event to notify UI about a new chat request
Ping, Ping,
/// No Inbox Relays found, the app is not ready to subscribe messages
InboxRelayNotFound, InboxRelayNotFound,
/// An error occurred
Error(String), Error(String),
} }
@@ -543,34 +536,16 @@ impl ChatRegistry {
cx.notify(); cx.notify();
} }
/// Emit an open room event. /// Track a room so it is listed and can be looked up by id.
/// pub fn track_room(&mut self, room: &Entity<Room>, cx: &mut Context<Self>) {
/// If the room is new, add it to the registry.
pub fn emit_room(&mut self, room: &Entity<Room>, window: &mut Window, cx: &mut Context<Self>) {
// Get the room's ID.
let id = room.read(cx).id; let id = room.read(cx).id;
// If the room is new, add it to the registry and index.
if let hash_map::Entry::Vacant(e) = self.room_index.entry(id) { if let hash_map::Entry::Vacant(e) = self.room_index.entry(id) {
let entity = room.to_owned(); let entity = room.to_owned();
e.insert(entity.clone()); e.insert(entity.clone());
self.rooms.insert(0, entity); self.rooms.insert(0, entity);
} cx.notify();
// Emit the open room event deferred to avoid re-entrant reads
cx.defer_in(window, move |_this, _window, cx| {
cx.emit(ChatEvent::OpenRoom(id));
});
}
/// Close a room.
pub fn close_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
if self.room_index.contains_key(&id) {
self.room_index.remove(&id);
self.rooms.retain(|r| r.read(cx).id != id);
cx.defer_in(window, move |_this, _window, cx| {
cx.emit(ChatEvent::CloseRoom(id));
});
} }
} }
+3 -2
View File
@@ -1983,12 +1983,13 @@ impl Panel for ChatPanel {
let seed = this.display_image_seed(cx); let seed = this.display_image_seed(cx);
h_flex() h_flex()
.gap_1p5() .gap_1()
.text_xs()
.child(Avatar::new(picture).seed(seed).xsmall()) .child(Avatar::new(picture).seed(seed).xsmall())
.child(label) .child(label)
.into_any_element() .into_any_element()
}) })
.unwrap_or(div().child("Unknown").into_any_element()) .unwrap_or(div().text_xs().child("Unknown").into_any_element())
} }
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> { fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
+6 -7
View File
@@ -11,7 +11,7 @@ use concord::cord06::RekeyScope;
use concord::derive::{channel_group_key, grant_locator}; use concord::derive::{channel_group_key, grant_locator};
use concord::state::{ChannelCursor, ChannelKeyRef, CommunityState, HeldKey, HeldRoot}; use concord::state::{ChannelCursor, ChannelKeyRef, CommunityState, HeldKey, HeldRoot};
use concord::{ChannelId, CommunityId, Epoch}; use concord::{ChannelId, CommunityId, Epoch};
use gpui::{App, AppContext, Context, EventEmitter, Task}; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
use state::NostrRegistry; use state::NostrRegistry;
@@ -107,8 +107,6 @@ impl SubscriptionKey {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum CommunityEvent { pub enum CommunityEvent {
Updated(CommunityId), Updated(CommunityId),
Open(CommunityId),
Close(CommunityId),
Channel(CommunityId, ChannelId), Channel(CommunityId, ChannelId),
/// History exists here that no held key can open. /// History exists here that no held key can open.
Unreadable(CommunityId), Unreadable(CommunityId),
@@ -180,15 +178,16 @@ impl Community {
&self.state &self.state
} }
pub fn name(&self) -> String { pub fn name(&self) -> SharedString {
if let Some(metadata) = &self.control.community { if let Some(metadata) = &self.control.community {
return metadata.name.clone(); return SharedString::from(&metadata.name);
} }
self.state self.state
.name .name
.clone() .as_ref()
.unwrap_or_else(|| self.state.id.to_hex()) .map(SharedString::from)
.unwrap_or_else(|| SharedString::from("Unknown"))
} }
pub fn control(&self) -> &ControlFold { pub fn control(&self) -> &ControlFold {
+1 -22
View File
@@ -7,7 +7,7 @@ pub use concord::cord03::{ChatMessage, ReplyRef};
use concord::state::CommunityState; use concord::state::CommunityState;
pub use concord::{ChannelId, CommunityId, Epoch}; pub use concord::{ChannelId, CommunityId, Epoch};
use futures::future::{Either, select}; use futures::future::{Either, select};
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, Window}; use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
use state::NostrRegistry; use state::NostrRegistry;
@@ -225,27 +225,6 @@ impl CommunityRegistry {
self.index.get(id).cloned() self.index.get(id).cloned()
} }
/// Ask the workspace to open a community's panel.
pub fn emit_community(
&mut self,
community: &Entity<Community>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let id = community.read(cx).id();
cx.defer_in(window, move |_this, _window, cx| {
cx.emit(CommunityEvent::Open(id));
});
}
/// Ask the workspace to close a community's panel.
pub fn emit_close(&mut self, id: CommunityId, window: &mut Window, cx: &mut Context<Self>) {
cx.defer_in(window, move |_this, _window, cx| {
cx.emit(CommunityEvent::Close(id));
});
}
/// Create a community owned by the current account and begin tracking it. /// Create a community owned by the current account and begin tracking it.
pub fn create(&mut self, metadata: CommunityMetadata, cx: &mut Context<Self>) { pub fn create(&mut self, metadata: CommunityMetadata, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
+13 -14
View File
@@ -145,21 +145,19 @@ impl CommunityPanel {
window, window,
|_this, _community, event, window, cx| { |_this, _community, event, window, cx| {
match event { match event {
CommunityEvent::Updated(_)
| CommunityEvent::Unreadable(_)
| CommunityEvent::Failed(_) => {
cx.defer_in(window, |this, window, cx| this.reload(window, cx));
}
CommunityEvent::Channel(..) => { CommunityEvent::Channel(..) => {
cx.defer_in(window, |this, window, cx| this.load(window, cx)); cx.defer_in(window, |this, window, cx| {
this.load(window, cx);
});
} }
CommunityEvent::Error(error) => { CommunityEvent::Error(error) => {
window.push_notification( window.push_notification(Notification::error(error.clone()), cx);
Notification::error(error.clone()).autohide(false), }
cx, _ => {
); cx.defer_in(window, |this, window, cx| {
this.reload(window, cx);
});
} }
CommunityEvent::Open(_) | CommunityEvent::Close(_) => {}
}; };
}, },
)); ));
@@ -723,12 +721,13 @@ impl Panel for CommunityPanel {
}; };
h_flex() h_flex()
.gap_1p5() .gap_1()
.text_xs()
.child(avatar) .child(avatar)
.child(SharedString::from(community.name())) .child(community.name())
.into_any_element() .into_any_element()
}) })
.unwrap_or_else(|_| div().child("Unknown").into_any_element()) .unwrap_or_else(|_| div().text_xs().child("Unknown").into_any_element())
} }
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> { fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
+1 -1
View File
@@ -27,7 +27,7 @@ pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
Size::Large => px(64.).into(), Size::Large => px(64.).into(),
Size::Medium => px(32.).into(), Size::Medium => px(32.).into(),
Size::Small => px(24.).into(), Size::Small => px(24.).into(),
Size::XSmall => px(20.).into(), Size::XSmall => px(18.).into(),
Size::Size(size) => size.into(), Size::Size(size) => size.into(),
} }
} }
+38 -200
View File
@@ -5,22 +5,21 @@ use std::sync::Arc;
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
Anchor, AnyElement, AnyView, App, AppContext as _, Bounds, Context, Div, Empty, Entity, AnyElement, AnyView, App, AppContext as _, Bounds, Context, Div, Empty, Entity,
GlobalElementId, InspectorElementId, InteractiveElement as _, IntoElement, LayoutId, GlobalElementId, InspectorElementId, InteractiveElement as _, IntoElement, LayoutId,
MouseButton, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, ScrollHandle, MouseButton, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, ScrollHandle,
SharedString, Stateful, StatefulInteractiveElement as _, Style, StyleRefinement, Styled as _, SharedString, Stateful, StatefulInteractiveElement as _, Style, StyleRefinement, Styled as _,
WeakEntity, Window, actions, div, px, rems, WeakEntity, Window, actions, div, px,
}; };
pub use gpui_base::dock::{DockArea, DockLayout, DockPlacement}; pub use gpui_base::dock::{DockArea, DockLayout, DockPlacement};
use gpui_base::dock::{ use gpui_base::dock::{
DockAreaRenderer, DockContext, DragPanel, DropIndicator, InsertTarget, NodeId, PaneNode, DockAreaRenderer, DockContext, DragPanel, DropIndicator, InsertTarget, NodeId, PaneNode,
PaneRef, PanelId, TabGroupContext, TabGroupRenderer, TileContext, TilesRenderer, PaneRef, PanelId, TabGroupContext, TabGroupRenderer,
}; };
use gpui_base::{Placement, ResizeHandleContext, Side}; use gpui_base::{Placement, ResizeHandleContext, Side};
use theme::{ActiveTheme, TABBAR_HEIGHT}; use theme::{ActiveTheme, TABBAR_HEIGHT};
use crate::button::{Button, ButtonVariants as _}; use crate::button::{Button, ButtonVariants as _};
use crate::menu::DropdownMenu as _;
use crate::resizable::{resize_handle, resize_handle_appearance}; use crate::resizable::{resize_handle, resize_handle_appearance};
use crate::tab::Tab; use crate::tab::Tab;
use crate::tab::tab_bar::TabBar; use crate::tab::tab_bar::TabBar;
@@ -95,6 +94,19 @@ pub fn add_panel(
area.add_panel_view(Arc::new(panel), placement, None, window, cx); area.add_panel_view(Arc::new(panel), placement, None, window, cx);
} }
/// Add a panel to a dock reached through a weak handle.
pub fn add_panel_to(
dock: &WeakEntity<DockArea>,
panel: PanelHandle,
placement: DockPlacement,
window: &mut Window,
cx: &mut App,
) {
let _ = dock.update(cx, |area, cx| {
add_panel(area, panel, placement, window, cx);
});
}
/// The panel in any region of `area` whose logical id is `key`. /// The panel in any region of `area` whose logical id is `key`.
fn find_panel(area: &DockArea, key: &SharedString, cx: &App) -> Option<(PanelId, NodeId, usize)> { fn find_panel(area: &DockArea, key: &SharedString, cx: &App) -> Option<(PanelId, NodeId, usize)> {
let placements = [ let placements = [
@@ -167,7 +179,6 @@ fn left_top_group(node: &PaneNode) -> Option<NodeId> {
match node.kind() { match node.kind() {
PaneRef::Tabs { .. } => Some(node.id()), PaneRef::Tabs { .. } => Some(node.id()),
PaneRef::Split { children, .. } => children.first().and_then(left_top_group), PaneRef::Split { children, .. } => children.first().and_then(left_top_group),
PaneRef::Tiles { .. } => None,
} }
} }
@@ -182,7 +193,6 @@ fn right_top_group(node: &PaneNode) -> Option<NodeId> {
}; };
child.and_then(right_top_group) child.and_then(right_top_group)
} }
PaneRef::Tiles { .. } => None,
} }
} }
@@ -263,18 +273,6 @@ impl DockAreaRenderer for DockSkin {
fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> { fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
Rc::new(TabGroupSkin::new(self.shared.clone())) Rc::new(TabGroupSkin::new(self.shared.clone()))
} }
fn tiles_renderer(&self) -> Rc<dyn TilesRenderer> {
Rc::new(NoTiles)
}
}
struct NoTiles;
impl TilesRenderer for NoTiles {
fn render_drag_bar(&self, _: &TileContext, _: &mut Window, _: &mut App) -> AnyElement {
Empty.into_any_element()
}
} }
/// The payload a dock's resize handle drags; the handle is the affordance. /// The payload a dock's resize handle drags; the handle is the affordance.
@@ -430,9 +428,6 @@ impl TabGroupSkin {
} }
/// Whether this group is the left dock's root with a single panel. /// Whether this group is the left dock's root with a single panel.
///
/// Such a group draws no tab bar, so its panel owns the window's top-left
/// corner — including the space the macOS traffic lights overlay.
fn is_plain_left_group(&self, group: &TabGroupContext, cx: &App) -> bool { fn is_plain_left_group(&self, group: &TabGroupContext, cx: &App) -> bool {
let Some(area) = self.shared.area() else { let Some(area) = self.shared.area() else {
return false; return false;
@@ -445,9 +440,6 @@ impl TabGroupSkin {
&& group.panels().len() == 1 && group.panels().len() == 1
} }
/// Whether this group is the topmost-left group on screen, which sits under
/// the native macOS traffic lights. The left dock's group is leftmost while
/// it is open and holds a panel; the center's is leftmost otherwise.
fn is_leftmost_top_group(&self, group: &TabGroupContext, cx: &App) -> bool { fn is_leftmost_top_group(&self, group: &TabGroupContext, cx: &App) -> bool {
let Some(area) = self.shared.area() else { let Some(area) = self.shared.area() else {
return false; return false;
@@ -465,172 +457,6 @@ impl TabGroupSkin {
tree.and_then(|tree| left_top_group(tree.root())) == Some(group.node()) tree.and_then(|tree| left_top_group(tree.root())) == Some(group.node())
} }
fn render_toolbar(
&self,
group: &TabGroupContext,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let zoomed = group.is_zoomed();
let closable = group.is_closable();
let zoomable = group.active_panel().is_some_and(|panel| panel.zoomable(cx));
let zoom_label = if zoomed { "Zoom Out" } else { "Zoom In" };
let buttons = group
.active_panel()
.and_then(PanelHandle::of)
.map(|handle| handle.panel().toolbar_buttons(window, cx))
.unwrap_or_default();
let menu_panel = group
.active_panel()
.and_then(PanelHandle::of)
.map(|handle| handle.panel().clone());
h_flex()
.p_0p5()
.gap_1p5()
.occlude()
.rounded_full()
.children(buttons.into_iter().map(|button| button.small().ghost()))
.when(zoomed, |this| {
this.child(
Button::new("zoom")
.icon(IconName::Zoom)
.small()
.ghost()
.tooltip("Zoom Out")
.on_click({
let group = TabGroupContext::clone(group);
move |_, window, cx| group.toggle_zoom(window, cx)
}),
)
})
.child(
Button::new("menu")
.icon(IconName::Ellipsis)
.small()
.ghost()
.dropdown_menu({
move |menu, _, cx| {
let menu = match menu_panel.clone() {
Some(panel) => panel.popup_menu(menu, cx),
None => menu,
};
menu.when(zoomable, |this| {
this.separator().menu(zoom_label, Box::new(ToggleZoom))
})
.when(closable, |this| {
this.separator().menu("Close", Box::new(ClosePanel))
})
}
})
.anchor(Anchor::TopRight),
)
}
fn render_title(
&self,
group: &TabGroupContext,
ix: usize,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let panel = group.panels()[ix].clone();
let left_button = self.dock_toggle_button(DockPlacement::Left, group, cx);
let bottom_button = self.dock_toggle_button(DockPlacement::Bottom, group, cx);
let right_button = self.dock_toggle_button(DockPlacement::Right, group, cx);
let has_leading = left_button.is_some() || bottom_button.is_some();
let drag = tab_drag(group, ix, cx);
let is_title_bar = self.is_title_bar_group(group, cx);
let needs_traffic_light_padding =
cfg!(target_os = "macos") && self.is_leftmost_top_group(group, cx);
let trailing_chrome = is_title_bar
.then(|| self.shared.chrome.trailing(window, cx))
.flatten();
let bar = h_flex()
.id("tab-title-bar")
.justify_between()
.items_center()
.line_height(rems(1.0))
.h(TABBAR_HEIGHT)
.bg(cx.theme().panel_background)
.when(left_button.is_some(), |this| this.pl_2())
.when(right_button.is_some(), |this| this.pr_2())
.when(has_leading, |this| {
this.child(
h_flex()
.flex_shrink_0()
.mr_1()
.gap_1()
.children(left_button)
.children(bottom_button),
)
})
.when(needs_traffic_light_padding, |this| {
this.pl(px(TRAFFIC_LIGHT_PADDING))
})
.child(
div()
.id("tab")
.flex_initial()
.min_w_0()
.px_2()
.overflow_hidden()
.whitespace_nowrap()
.child(
div()
.w_full()
.text_ellipsis()
.text_sm()
.child(panel_title(&panel, cx)),
)
.when_some(drag, |this, drag| {
this.on_drag(drag, {
let panel = panel.clone();
move |drag, offset, _, cx| {
cx.stop_propagation();
drag.set_drag_offset(offset);
cx.new(|_| DragPreview {
panel: panel.clone(),
})
}
})
}),
)
.child({
let space = div().id("tab-title-space").flex_1().h_full();
if is_title_bar {
title_bar_drag_handlers(space, window, cx).into_any_element()
} else {
space.into_any_element()
}
})
.child(
h_flex()
.flex_shrink_0()
.ml_1()
.gap_1()
.child(self.render_toolbar(group, window, cx))
.children(right_button),
)
.when_some(trailing_chrome, |this, chrome| this.child(chrome));
if is_title_bar {
h_flex()
.h(TABBAR_HEIGHT)
.bg(cx.theme().panel_background)
.child(bar.flex_1())
.child(window_controls())
.into_any_element()
} else {
bar.into_any_element()
}
}
fn render_tabs( fn render_tabs(
&self, &self,
group: &TabGroupContext, group: &TabGroupContext,
@@ -642,9 +468,12 @@ impl TabGroupSkin {
let bottom_button = self.dock_toggle_button(DockPlacement::Bottom, group, cx); let bottom_button = self.dock_toggle_button(DockPlacement::Bottom, group, cx);
let right_button = self.dock_toggle_button(DockPlacement::Right, group, cx); let right_button = self.dock_toggle_button(DockPlacement::Right, group, cx);
let has_leading = left_button.is_some() || bottom_button.is_some(); let has_leading = left_button.is_some() || bottom_button.is_some();
let collapsed = group.is_collapsed(); let collapsed = group.is_collapsed();
let droppable = group.is_droppable(); let droppable = group.is_droppable();
let tabs_count = group.panels().len(); let tabs_count = group.panels().len();
let is_title_bar = self.is_title_bar_group(group, cx);
let displayed = group.active_panel().map(|panel| panel.panel_id(cx)); let displayed = group.active_panel().map(|panel| panel.panel_id(cx));
let displayed_ix = displayed.and_then(|displayed| { let displayed_ix = displayed.and_then(|displayed| {
group group
@@ -652,12 +481,14 @@ impl TabGroupSkin {
.iter() .iter()
.position(|panel| panel.panel_id(cx) == displayed) .position(|panel| panel.panel_id(cx) == displayed)
}); });
let is_title_bar = self.is_title_bar_group(group, cx);
let needs_traffic_light_padding = let needs_traffic_light_padding =
cfg!(target_os = "macos") && self.is_leftmost_top_group(group, cx); cfg!(target_os = "macos") && self.is_leftmost_top_group(group, cx);
let trailing_chrome = is_title_bar let trailing_chrome = is_title_bar
.then(|| self.shared.chrome.trailing(window, cx)) .then(|| self.shared.chrome.trailing(window, cx))
.flatten(); .flatten();
let empty_space = div() let empty_space = div()
.id("tab-bar-empty-space") .id("tab-bar-empty-space")
.h_full() .h_full()
@@ -673,6 +504,7 @@ impl TabGroupSkin {
} }
}) })
}); });
let empty_space = if is_title_bar { let empty_space = if is_title_bar {
title_bar_drag_handlers(empty_space, window, cx).into_any_element() title_bar_drag_handlers(empty_space, window, cx).into_any_element()
} else { } else {
@@ -689,7 +521,9 @@ impl TabGroupSkin {
.when(is_title_bar || has_leading, |this| { .when(is_title_bar || has_leading, |this| {
this.prefix( this.prefix(
h_flex() h_flex()
.items_center() .size(TABBAR_HEIGHT)
.flex_shrink_0()
.justify_center()
.top_0() .top_0()
.right(-px(1.)) .right(-px(1.))
.pl_0p5() .pl_0p5()
@@ -767,13 +601,13 @@ impl TabGroupSkin {
.when(!collapsed, |this| { .when(!collapsed, |this| {
this.suffix( this.suffix(
h_flex() h_flex()
.flex_shrink_0()
.items_center() .items_center()
.top_0() .top_0()
.right_0() .right_0()
.h_full() .h_full()
.px_0p5() .px_0p5()
.gap_1() .gap_1()
.child(self.render_toolbar(group, window, cx))
.children(right_button) .children(right_button)
.children(trailing_chrome), .children(trailing_chrome),
) )
@@ -846,7 +680,13 @@ impl TabGroupSkin {
.small() .small()
.ghost() .ghost()
.tab_stop(false) .tab_stop(false)
.tooltip(if is_open { "Collapse" } else { "Expand" }) .map(|this| {
if is_open {
this.tooltip("Collapse")
} else {
this.tooltip("Expand")
}
})
.on_click(move |_, window, cx| { .on_click(move |_, window, cx| {
area.update(cx, |area, cx| area.toggle_dock(placement, window, cx)); area.update(cx, |area, cx| area.toggle_dock(placement, window, cx));
}), }),
@@ -901,13 +741,11 @@ impl TabGroupRenderer for TabGroupSkin {
self.scroll_handle.scroll_to_item(visible_ix); self.scroll_handle.scroll_to_item(visible_ix);
} }
match visible.as_slice() { if visible.is_empty() {
[] => Empty.into_any_element(), return Empty.into_any_element();
// One panel in a group that is not asking for tabs gets the title
// instead of a tab bar.
[ix] => self.render_title(group, *ix, window, cx),
_ => self.render_tabs(group, visible.as_slice(), window, cx),
} }
self.render_tabs(group, visible.as_slice(), window, cx)
} }
fn render_active_panel( fn render_active_panel(
-3
View File
@@ -19,9 +19,6 @@ pub enum PanelEvent {
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable { pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
/// The name of the panel used to serialize, deserialize and identify the panel. /// The name of the panel used to serialize, deserialize and identify the panel.
///
/// This is used to identify the panel when deserializing the panel.
/// Once you have defined a panel id, this must not be changed.
fn panel_id(&self) -> SharedString; fn panel_id(&self) -> SharedString;
/// The title of the panel /// The title of the panel
+2
View File
@@ -22,6 +22,7 @@ impl<T: IconNamed> From<T> for Icon {
pub enum IconName { pub enum IconName {
ArrowLeft, ArrowLeft,
ArrowRight, ArrowRight,
ArrowDownCircle,
Boom, Boom,
Book, Book,
ChevronDown, ChevronDown,
@@ -99,6 +100,7 @@ impl IconNamed for IconName {
match self { match self {
Self::ArrowLeft => "icons/arrow-left.svg", Self::ArrowLeft => "icons/arrow-left.svg",
Self::ArrowRight => "icons/arrow-right.svg", Self::ArrowRight => "icons/arrow-right.svg",
Self::ArrowDownCircle => "icons/arrow-down-circle.svg",
Self::Boom => "icons/boom.svg", Self::Boom => "icons/boom.svg",
Self::Book => "icons/book.svg", Self::Book => "icons/book.svg",
Self::ChevronDown => "icons/chevron-down.svg", Self::ChevronDown => "icons/chevron-down.svg",
+91 -54
View File
@@ -3,9 +3,9 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
AnyElement, App, ClickEvent, Div, InteractiveElement, IntoElement, MouseButton, ParentElement, AnyElement, App, ClickEvent, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px, relative, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px,
}; };
use theme::{ActiveTheme, TABBAR_HEIGHT}; use theme::ActiveTheme;
use crate::{Icon, IconName, Selectable, h_flex}; use crate::{Icon, IconName, Selectable, h_flex};
@@ -25,6 +25,7 @@ pub struct Tab {
children: Vec<AnyElement>, children: Vec<AnyElement>,
pub(super) disabled: bool, pub(super) disabled: bool,
pub(super) selected: bool, pub(super) selected: bool,
pub(super) segmented: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
} }
@@ -69,6 +70,7 @@ impl Default for Tab {
children: Vec::new(), children: Vec::new(),
disabled: false, disabled: false,
selected: false, selected: false,
segmented: false,
prefix: None, prefix: None,
suffix: None, suffix: None,
on_click: None, on_click: None,
@@ -132,6 +134,12 @@ impl Tab {
self.tab_bar_prefix = Some(tab_bar_prefix); self.tab_bar_prefix = Some(tab_bar_prefix);
self self
} }
/// Render the tab as a segment inside a segmented control.
pub(crate) fn segmented(mut self, segmented: bool) -> Self {
self.segmented = segmented;
self
}
} }
impl ParentElement for Tab { impl ParentElement for Tab {
@@ -167,74 +175,103 @@ impl Styled for Tab {
impl RenderOnce for Tab { impl RenderOnce for Tab {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let fg = if self.disabled { let Self {
ix,
base,
label,
icon,
prefix,
suffix,
children,
disabled,
selected,
segmented,
on_click,
..
} = self;
let foreground = if disabled {
cx.theme().text_muted cx.theme().text_muted
} else if self.selected { } else if selected {
cx.theme().tab_active_foreground cx.theme().tab_active_foreground
} else { } else {
cx.theme().tab_foreground cx.theme().tab_foreground
}; };
self.base let content = h_flex()
.id(self.ix) .flex_1()
.flex() .h_6()
.flex_wrap() .whitespace_nowrap()
.gap_1()
.items_center() .items_center()
.flex_shrink_0()
.h(TABBAR_HEIGHT)
.relative()
.overflow_hidden() .overflow_hidden()
.text_color(fg) .when(segmented, |this| this.justify_center().px_1())
.text_sm() .when(!segmented, |this| this.justify_start())
.when_some(self.prefix, |this, prefix| this.child(prefix)) .map(|this| match icon {
.child( Some(icon) => this.w(px(38.)).child(icon.size_4()),
h_flex() None => this
.map(|this| match label {
Some(label) => this.child(label),
None => this,
})
.children(children),
});
base.id(ix)
.group("tab")
.flex()
.items_center()
.text_color(foreground)
.when(segmented, |this| {
this.text_xs()
.flex_1() .flex_1()
.h(px(30.)) .h_6()
.line_height(relative(1.)) .rounded(cx.theme().radius)
.whitespace_nowrap() .when(selected && !disabled, |this| {
.items_center() this.bg(cx.theme().tab_active_background)
.justify_center() .when(cx.theme().shadow, |this| this.shadow_sm())
})
.when(!selected && !disabled, |this| {
this.hover(|this| this.bg(cx.theme().tab_hover_background))
})
})
.when(!segmented, |this| {
this.flex_shrink_0()
.min_w_32()
.h_7()
.gap_1()
.px_1p5()
.text_sm()
.rounded(cx.theme().radius)
.overflow_hidden() .overflow_hidden()
.flex_shrink_0() .when(selected && !disabled, |this| {
.px_3() this.bg(cx.theme().tab_background)
.map(|this| match self.icon { })
Some(icon) => this.w(px(38.)).child(icon.size_4()), .when(!selected && !disabled, |this| {
None => this this.hover(|this| {
.map(|this| match self.label { this.bg(cx.theme().tab_hover_background)
Some(label) => this.child(label), .text_color(cx.theme().tab_active_foreground)
None => this, })
}) })
.children(self.children), })
}), .when_some(prefix, |this, prefix| this.child(prefix))
) .child(content)
.when_some(self.suffix, |this, suffix| { .when_some(suffix, |this, suffix| {
this.child(div().pr_2().child(suffix)) this.child(
div()
.flex_shrink_0()
.when(!selected, |this| {
this.invisible().group_hover("tab", |this| this.visible())
})
.child(suffix),
)
}) })
.on_mouse_down(MouseButton::Left, |_ev, _window, cx| { .on_mouse_down(MouseButton::Left, |_ev, _window, cx| {
cx.stop_propagation(); cx.stop_propagation();
}) })
.when(!self.disabled, |this| { .when(!disabled, |this| {
this.when_some(self.on_click.clone(), |this, on_click| { this.when_some(on_click, |this, on_click| {
this.on_click(move |event, window, cx| on_click(event, window, cx)) this.on_click(move |event, window, cx| on_click(event, window, cx))
}) })
}) })
.child(
div()
.absolute()
.bottom_0()
.left_0()
.right_0()
.h_0p5()
.when(self.selected && !self.disabled, |this| {
this.bg(cx.theme().element_active)
})
.when(!self.selected && !self.disabled, |this| {
this.invisible().group_hover("", |this| {
this.visible().bg(cx.theme().secondary_background)
})
}),
)
} }
} }
+20 -3
View File
@@ -7,6 +7,7 @@ use gpui::{
Window, div, px, Window, div, px,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use theme::ActiveTheme;
use super::Tab; use super::Tab;
use crate::button::{Button, ButtonVariants as _}; use crate::button::{Button, ButtonVariants as _};
@@ -25,6 +26,7 @@ pub struct TabBar {
last_empty_space: AnyElement, last_empty_space: AnyElement,
selected_index: Option<usize>, selected_index: Option<usize>,
menu: bool, menu: bool,
segmented: bool,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>, on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
} }
@@ -43,9 +45,16 @@ impl TabBar {
selected_index: None, selected_index: None,
on_click: None, on_click: None,
menu: false, menu: false,
segmented: false,
} }
} }
/// Render the tabs as a segmented control inside a pill-shaped track.
pub fn segmented(mut self, segmented: bool) -> Self {
self.segmented = segmented;
self
}
/// Set whether to show the menu button when tabs overflow, default is false. /// Set whether to show the menu button when tabs overflow, default is false.
pub fn menu(mut self, menu: bool) -> Self { pub fn menu(mut self, menu: bool) -> Self {
self.menu = menu; self.menu = menu;
@@ -113,33 +122,41 @@ impl Styled for TabBar {
} }
impl RenderOnce for TabBar { impl RenderOnce for TabBar {
fn render(self, _: &mut Window, _cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let mut item_labels = Vec::new(); let mut item_labels = Vec::new();
let selected_index = self.selected_index; let selected_index = self.selected_index;
let on_click = self.on_click.clone(); let on_click = self.on_click.clone();
let segmented = self.segmented;
self.base self.base
.group("tab-bar") .group("tab-bar")
.relative() .relative()
.flex() .flex()
.items_center() .items_center()
.min_w_0()
.refine_style(&self.style) .refine_style(&self.style)
.when(segmented, |this| {
this.bg(cx.theme().tab_background)
.p_0p5()
.rounded(cx.theme().radius)
})
.when_some(self.prefix, |this, prefix| this.child(prefix)) .when_some(self.prefix, |this, prefix| this.child(prefix))
.child( .child(
h_flex() h_flex()
.id("tabs") .id("tabs")
.flex_1() .flex_1()
.overflow_x_scroll() .when(!segmented, |this| this.overflow_x_scroll())
.when_some(self.scroll_handle, |this, scroll_handle| { .when_some(self.scroll_handle, |this, scroll_handle| {
this.track_scroll(&scroll_handle) this.track_scroll(&scroll_handle)
}) })
.gap(px(0.)) .gap_1()
.children(self.children.into_iter().enumerate().map(|(ix, child)| { .children(self.children.into_iter().enumerate().map(|(ix, child)| {
item_labels.push((child.label.clone(), child.disabled)); item_labels.push((child.label.clone(), child.disabled));
let tab_bar_prefix = child.tab_bar_prefix.unwrap_or(true); let tab_bar_prefix = child.tab_bar_prefix.unwrap_or(true);
child child
.ix(ix) .ix(ix)
.tab_bar_prefix(tab_bar_prefix) .tab_bar_prefix(tab_bar_prefix)
.segmented(segmented)
.when_some(self.selected_index, |this, selected_ix| { .when_some(self.selected_index, |this, selected_ix| {
this.selected(selected_ix == ix) this.selected(selected_ix == ix)
}) })
+22 -7
View File
@@ -2,17 +2,18 @@ use chat::{ChatRegistry, Room, RoomKind};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Window, div, px, Subscription, WeakEntity, Window, div, px,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use state::NostrRegistry; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock::{DockArea, DockPlacement, PanelHandle};
use ui::input::{Input, InputEvent, InputState}; use ui::input::{Input, InputEvent, InputState};
use ui::{StyledExt, WindowExtension, v_flex}; use ui::{StyledExt, WindowExtension, v_flex};
pub fn open(window: &mut Window, cx: &mut App) { pub fn open(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) {
let view = cx.new(|cx| NewChat::new(window, cx)); let view = cx.new(|cx| NewChat::new(dock, window, cx));
window.open_modal(cx, move |this, _window, _cx| { window.open_modal(cx, move |this, _window, _cx| {
this.width(px(420.)).title("New chat").child(view.clone()) this.width(px(420.)).title("New chat").child(view.clone())
@@ -20,6 +21,9 @@ pub fn open(window: &mut Window, cx: &mut App) {
} }
pub struct NewChat { pub struct NewChat {
/// The dock a started chat opens in
dock: WeakEntity<DockArea>,
/// Public key input /// Public key input
input: Entity<InputState>, input: Entity<InputState>,
@@ -31,7 +35,7 @@ pub struct NewChat {
} }
impl NewChat { impl NewChat {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let input = cx.new(|cx| InputState::new(window, cx).placeholder("npub")); let input = cx.new(|cx| InputState::new(window, cx).placeholder("npub"));
let subscription = cx.subscribe_in(&input, window, |this, _input, event, window, cx| { let subscription = cx.subscribe_in(&input, window, |this, _input, event, window, cx| {
@@ -41,6 +45,7 @@ impl NewChat {
}); });
Self { Self {
dock,
input, input,
error: None, error: None,
_subscription: Some(subscription), _subscription: Some(subscription),
@@ -56,6 +61,8 @@ impl NewChat {
}; };
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let chat = ChatRegistry::global(cx);
let Some(current_user) = nostr.read(cx).current_user() else { let Some(current_user) = nostr.read(cx).current_user() else {
self.set_error("You are not signed in", cx); self.set_error("You are not signed in", cx);
return; return;
@@ -70,12 +77,20 @@ impl NewChat {
.organize(&current_user) .organize(&current_user)
.kind(RoomKind::Ongoing); .kind(RoomKind::Ongoing);
let chat = ChatRegistry::global(cx); let room = chat.update(cx, |chat, cx| {
chat.update(cx, |chat, cx| {
let room = cx.new(|_| room); let room = cx.new(|_| room);
chat.emit_room(&room, window, cx); chat.track_room(&room, cx);
room
}); });
ui::dock::add_panel_to(
&self.dock,
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
DockPlacement::Center,
window,
cx,
);
window.close_modal(cx); window.close_modal(cx);
} }
+18 -268
View File
@@ -1,4 +1,3 @@
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use ::settings::AppSettings; use ::settings::AppSettings;
@@ -6,25 +5,20 @@ use anyhow::Error;
use auto_update::AutoUpdater; use auto_update::AutoUpdater;
use chat::{ChatEvent, ChatRegistry}; use chat::{ChatEvent, ChatRegistry};
use common::download_dir; use common::download_dir;
use community::{CommunityEvent, CommunityRegistry};
use community_ui::CommunityPanel;
use device::{DeviceEvent, DeviceRegistry}; use device::{DeviceEvent, DeviceRegistry};
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
Action, AnyElement, App, AppContext, Context, Entity, InteractiveElement, IntoElement, Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div, px, Render, SharedString, Styled, Subscription, Task, Window, div, px,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::{PersonRegistry, shorten_pubkey};
use serde::Deserialize; use serde::Deserialize;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
use state::{NostrRegistry, StateEvent}; use state::{NostrRegistry, StateEvent};
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry}; use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock::{self, ClosePanel, DockArea, DockLayout, DockPlacement, Panel, PanelHandle}; use ui::dock::{self, DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::{Notification, NotificationKind}; use ui::notification::{Notification, NotificationKind};
use ui::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
use crate::dialogs::restore::RestoreEncryption; use crate::dialogs::restore::RestoreEncryption;
use crate::dialogs::{new_chat, new_community, settings}; use crate::dialogs::{new_chat, new_community, settings};
@@ -71,9 +65,6 @@ enum Command {
pub struct Workspace { pub struct Workspace {
dock: Entity<DockArea>, dock: Entity<DockArea>,
title_bar_chrome: Rc<dock::TitleBarChrome>,
/// The community panel currently docked, if any
community_panel: Option<WeakEntity<CommunityPanel>>,
/// Async tasks /// Async tasks
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<Result<(), Error>>>,
/// Event subscriptions /// Event subscriptions
@@ -83,12 +74,11 @@ pub struct Workspace {
impl Workspace { impl Workspace {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let communities = CommunityRegistry::global(cx);
let device = DeviceRegistry::global(cx); let device = DeviceRegistry::global(cx);
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let sidebar = cx.new(|cx| Sidebar::new(window, cx)); let (dock, _) = dock::dock_area("coop", window, cx);
let (dock, title_bar_chrome) = dock::dock_area("coop", window, cx); let sidebar = cx.new(|cx| Sidebar::new(window, dock.downgrade(), cx));
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
@@ -164,7 +154,7 @@ impl Workspace {
subscriptions.push( subscriptions.push(
// Observe all events emitted by the chat registry // Observe all events emitted by the chat registry
cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| { cx.subscribe_in(&chat, window, move |_this, _chat, ev, window, cx| {
match ev { match ev {
ChatEvent::InboxRelayNotFound => { ChatEvent::InboxRelayNotFound => {
const MSG: &str = "Messaging Relays not found. Cannot receive messages."; const MSG: &str = "Messaging Relays not found. Cannot receive messages.";
@@ -187,28 +177,6 @@ impl Workspace {
cx, cx,
); );
} }
ChatEvent::OpenRoom(id) => {
if let Some(room) = chat.read(cx).room(id, cx) {
this.add_panel_to_dock(
chat_ui::init(room, window, cx),
DockPlacement::Center,
window,
cx,
);
}
}
ChatEvent::CloseRoom(..) => {
this.dock.update(cx, |area, cx| {
// Force focus to the tab panel
ui::dock::focus_tab_panel(area, window, cx);
// Dispatch the close panel action
cx.defer_in(window, |_, window, cx| {
window.dispatch_action(Box::new(ClosePanel), cx);
window.close_all_modals(cx);
});
});
}
ChatEvent::Error(error) => { ChatEvent::Error(error) => {
window.push_notification(Notification::error(error).autohide(false), cx); window.push_notification(Notification::error(error).autohide(false), cx);
} }
@@ -217,71 +185,23 @@ impl Workspace {
}), }),
); );
subscriptions.push(
// Observe all events emitted by the community registry
cx.subscribe_in(
&communities,
window,
move |this, communities, event, window, cx| match event {
CommunityEvent::Open(id) => {
if let Some(community) = communities.read(cx).community(id) {
let panel = community_ui::init(community, window, cx);
this.community_panel = Some(panel.downgrade());
this.add_panel_to_dock(panel, DockPlacement::Center, window, cx);
}
}
CommunityEvent::Close(_) => {
let Some(panel) = this
.community_panel
.take()
.and_then(|panel| panel.upgrade())
else {
return;
};
this.dock.update(cx, |area, cx| {
ui::dock::add_panel(
area,
PanelHandle::new(panel),
DockPlacement::Center,
window,
cx,
);
ui::dock::focus_tab_panel(area, window, cx);
cx.defer_in(window, |_, window, cx| {
window.dispatch_action(Box::new(ClosePanel), cx);
});
});
}
_ => {}
},
),
);
cx.defer_in(window, move |this, window, cx| { cx.defer_in(window, move |this, window, cx| {
let sidebar = PanelHandle::new(sidebar); let sidebar = PanelHandle::new(sidebar);
this.dock.update(cx, |area, cx| {
let left = DockLayout::tabs().panel_view(Arc::new(sidebar), cx);
area.set_dock(DockPlacement::Left, left, window, cx);
area.set_dock_size(DockPlacement::Left, SIDEBAR_WIDTH, window, cx);
});
let greeter = PanelHandle::new(greeter::init(window, cx)); let greeter = PanelHandle::new(greeter::init(window, cx));
let center = DockLayout::v_split()
.child(DockLayout::tabs().panel_view(Arc::new(greeter), cx), None);
this.dock.update(cx, |area, cx| { this.dock.update(cx, |this, cx| {
area.set_center(center, window, cx); let left = DockLayout::tabs().panel_view(Arc::new(sidebar), cx);
let center = DockLayout::v_split()
.child(DockLayout::tabs().panel_view(Arc::new(greeter), cx), None);
this.set_dock(DockPlacement::Left, left, window, cx);
this.set_dock_size(DockPlacement::Left, SIDEBAR_WIDTH, window, cx);
this.set_center(center, window, cx);
}); });
}); });
Self { Self {
dock, dock,
title_bar_chrome,
community_panel: None,
tasks: vec![], tasks: vec![],
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
@@ -365,10 +285,11 @@ impl Workspace {
self.add_panel_to_dock(browse::init(window, cx), DockPlacement::Center, window, cx); self.add_panel_to_dock(browse::init(window, cx), DockPlacement::Center, window, cx);
} }
Command::ShowSearch => { Command::ShowSearch => {
self.add_panel_to_dock(search::init(window, cx), DockPlacement::Center, window, cx); let panel = search::init(self.dock.downgrade(), window, cx);
self.add_panel_to_dock(panel, DockPlacement::Center, window, cx);
} }
Command::NewChat => { Command::NewChat => {
new_chat::open(window, cx); new_chat::open(self.dock.downgrade(), window, cx);
} }
Command::NewCommunity => { Command::NewCommunity => {
new_community::open(window, cx); new_community::open(window, cx);
@@ -583,174 +504,6 @@ impl Workspace {
})) }))
}); });
} }
fn titlebar_right(_window: &mut Window, cx: &mut App) -> AnyElement {
let auto_updater = AutoUpdater::try_global(cx);
let chat = ChatRegistry::global(cx);
let nip4e_enabled = AppSettings::get_nip4e(cx);
let nostr = NostrRegistry::global(cx);
let Some(public_key) = nostr.read(cx).current_user() else {
return div().into_any_element();
};
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
let announcement = profile.announcement();
let updater_status = auto_updater.as_ref().and_then(|updater| {
let updater = updater.read(cx);
(!updater.idle()).then(|| updater.status())
});
let staged_update = auto_updater
.as_ref()
.is_some_and(|updater| updater.read(cx).staged());
h_flex()
.when(!cx.theme().platform.is_mac(), |this| this.pr_2())
.gap_2()
.when_some(updater_status, |this, status| {
this.child(div().text_xs().italic().child(status))
})
.when(staged_update, |this| {
this.child(
Button::new("restart-to-update")
.label("Restart to Update")
.tooltip("Quit and relaunch into the installed update")
.small()
.ghost()
.on_click(|_event, _window, cx| {
if let Some(auto_updater) = AutoUpdater::try_global(cx) {
auto_updater.update(cx, |this, cx| this.restart(cx));
}
}),
)
})
.when(nip4e_enabled, |this| {
this.child(
Button::new("key")
.icon(IconName::UserKey)
.tooltip("Decoupled encryption key")
.small()
.ghost()
.dropdown_menu(move |this, _window, _cx| {
this.min_w(px(260.))
.label("Encryption Key")
.when_some(announcement.as_ref(), |this, announcement| {
let name = announcement.client_name();
let pkey = shorten_pubkey(announcement.public_key(), 8);
this.item(PopupMenuItem::element(move |_window, cx| {
h_flex()
.gap_1()
.text_sm()
.child(
Icon::new(IconName::Device)
.small()
.text_color(cx.theme().icon_muted),
)
.child(name.clone())
}))
.item(
PopupMenuItem::element(move |_window, cx| {
h_flex()
.gap_1()
.text_sm()
.child(
Icon::new(IconName::UserKey)
.small()
.text_color(cx.theme().icon_muted),
)
.child(SharedString::from(pkey.clone()))
}),
)
})
.separator()
.menu_with_icon(
"Backup",
IconName::Shield,
Box::new(Command::BackupEncryption),
)
.menu_with_icon(
"Restore from secret key",
IconName::Usb,
Box::new(Command::ImportEncryption),
)
.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshEncryption),
)
.menu_with_icon(
"Reset",
IconName::Warning,
Box::new(Command::ResetEncryption),
)
}),
)
})
.child(
Button::new("inbox")
.icon(IconName::Inbox)
.small()
.ghost()
.dropdown_menu(move |this, _window, cx| {
let urls: Vec<(SharedString, SharedString)> = profile
.messaging_relays()
.iter()
.map(|url| {
(
SharedString::from(url.to_string()),
chat.read(cx).count_messages(url).to_string().into(),
)
})
.collect();
// Header
let menu = this.min_w(px(260.)).label("Messaging Relays");
// Content
let menu = urls.into_iter().fold(menu, |this, (url, count)| {
this.item(PopupMenuItem::element(move |_window, cx| {
h_flex()
.px_1()
.w_full()
.text_sm()
.justify_between()
.child(url.clone())
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(count.clone()),
)
}))
});
// Footer
menu.separator()
.menu_with_icon(
"Manage gossip relays",
IconName::Relay,
Box::new(Command::ShowRelayList),
)
.menu_with_icon(
"Manage messaging relays",
IconName::Relay,
Box::new(Command::ShowMessaging),
)
.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshMessagingRelays),
)
}),
)
.into_any_element()
}
} }
impl Render for Workspace { impl Render for Workspace {
@@ -758,9 +511,6 @@ impl Render for Workspace {
let modal_layer = Root::render_modal_layer(window, cx); let modal_layer = Root::render_modal_layer(window, cx);
let notification_layer = Root::render_notification_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx);
// Render the title bar chrome
self.title_bar_chrome.set_trailing(Self::titlebar_right);
div() div()
.id("workspace") .id("workspace")
.on_action(cx.listener(Self::on_command)) .on_action(cx.listener(Self::on_command))
+4 -1
View File
@@ -58,13 +58,16 @@ impl Panel for GreeterPanel {
} }
fn title(&self, cx: &App) -> AnyElement { fn title(&self, cx: &App) -> AnyElement {
div() h_flex()
.gap_1()
.text_xs()
.child( .child(
svg() svg()
.path("brand/coop.svg") .path("brand/coop.svg")
.size_4() .size_4()
.text_color(cx.theme().text_muted), .text_color(cx.theme().text_muted),
) )
.child("Welcome")
.into_any_element() .into_any_element()
} }
} }
+25 -9
View File
@@ -7,8 +7,8 @@ use common::DebouncedDelay;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
AnyElement, App, AppContext, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, AnyElement, App, AppContext, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity,
uniform_list, Window, div, uniform_list,
}; };
use instant::Duration; use instant::Duration;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
@@ -17,7 +17,7 @@ use smallvec::{SmallVec, smallvec};
use state::{FIND_DELAY, NostrRegistry}; use state::{FIND_DELAY, NostrRegistry};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock::{Panel, PanelEvent}; use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
use ui::input::{Input, InputEvent, InputState}; use ui::input::{Input, InputEvent, InputState};
use ui::notification::Notification; use ui::notification::Notification;
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
@@ -26,13 +26,15 @@ use crate::sidebar::{TreeRow, TreeRowKind};
const INPUT_PLACEHOLDER: &str = "Find or start a conversation"; const INPUT_PLACEHOLDER: &str = "Find or start a conversation";
pub fn init(window: &mut Window, cx: &mut App) -> Entity<SearchPanel> { pub fn init(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) -> Entity<SearchPanel> {
cx.new(|cx| SearchPanel::new(window, cx)) cx.new(|cx| SearchPanel::new(dock, window, cx))
} }
pub struct SearchPanel { pub struct SearchPanel {
name: SharedString, name: SharedString,
focus_handle: FocusHandle, focus_handle: FocusHandle,
/// The dock a started chat opens in
dock: WeakEntity<DockArea>,
/// Find input state /// Find input state
find_input: Entity<InputState>, find_input: Entity<InputState>,
@@ -63,7 +65,7 @@ pub struct SearchPanel {
} }
impl SearchPanel { impl SearchPanel {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let contact_list = cx.new(|_| None); let contact_list = cx.new(|_| None);
let selected_pkeys = cx.new(|_| HashSet::new()); let selected_pkeys = cx.new(|_| HashSet::new());
let find_results = cx.new(|_| None); let find_results = cx.new(|_| None);
@@ -107,6 +109,7 @@ impl SearchPanel {
Self { Self {
name: "Search".into(), name: "Search".into(),
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
dock,
find_input, find_input,
find_debouncer: DebouncedDelay::new(), find_debouncer: DebouncedDelay::new(),
find_results, find_results,
@@ -285,6 +288,7 @@ impl SearchPanel {
fn create_room(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn create_room(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let async_chat = chat.downgrade(); let async_chat = chat.downgrade();
let dock = self.dock.clone();
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let Some(public_key) = nostr.read(cx).current_user() else { let Some(public_key) = nostr.read(cx).current_user() else {
@@ -295,14 +299,26 @@ impl SearchPanel {
let receivers = self.get_selected(cx); let receivers = self.get_selected(cx);
self.tasks.push(cx.spawn_in(window, async move |this, cx| { self.tasks.push(cx.spawn_in(window, async move |this, cx| {
// Create a new room and emit it // Create a new room and register it
async_chat.update_in(cx, |this, _window, cx| { let room = async_chat.update_in(cx, |chat, _window, cx| {
let room = cx.new(|_| { let room = cx.new(|_| {
Room::new(public_key, receivers) Room::new(public_key, receivers)
.organize(&public_key) .organize(&public_key)
.kind(RoomKind::Ongoing) .kind(RoomKind::Ongoing)
}); });
this.emit_room(&room, _window, cx); chat.track_room(&room, cx);
room
})?;
// Open it in the dock
cx.update(|window, cx| {
ui::dock::add_panel_to(
&dock,
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
DockPlacement::Center,
window,
cx,
);
})?; })?;
// Reset the find panel // Reset the find panel
File diff suppressed because it is too large Load Diff
@@ -1,74 +0,0 @@
use gpui::{App, InteractiveElement, IntoElement, ParentElement, Styled, Window, div, svg};
use theme::{ActiveTheme, TABBAR_HEIGHT};
use ui::button::{Button, ButtonVariants};
use ui::{StyledExt, h_flex, title_bar_drag_handlers, v_flex};
use crate::dialogs::import;
const TITLE: &str = "Welcome to Coop!";
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
pub(super) fn render(window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.size_full()
.relative()
.bg(cx.theme().surface_background)
.child(title_bar_drag_handlers(
div()
.id("onboarding-drag")
.absolute()
.top_0()
.left_0()
.h(TABBAR_HEIGHT)
.w_full(),
window,
cx,
))
.child(
v_flex()
.size_full()
.justify_end()
.gap_4()
.p_4()
.child(
h_flex()
.gap_2()
.child(
svg()
.path("brand/coop.svg")
.size_8()
.text_color(cx.theme().icon_muted),
)
.child(
v_flex().child(div().font_semibold().child(TITLE)).child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(DESCRIPTION),
),
),
)
.child(
v_flex()
.gap_2()
.w_full()
.child(
Button::new("join-now")
.label("Join now")
.primary()
.font_semibold()
.h_8()
.w_full(),
)
.child(
Button::new("import-identity")
.label("Import identity")
.secondary()
.font_semibold()
.h_8()
.w_full()
.on_click(|_event, window, cx| import::open(window, cx)),
),
),
)
}
+10 -93
View File
@@ -1,121 +1,38 @@
use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{App, InteractiveElement, IntoElement, ParentElement, RenderOnce, Styled, Window, div};
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::{IconName, Selectable, h_flex};
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SidebarTab { pub enum SidebarTab {
Recents, Inbox,
Chats,
Communities, Communities,
} }
impl SidebarTab { impl SidebarTab {
pub const ALL: [SidebarTab; 3] = [Self::Recents, Self::Chats, Self::Communities]; pub const ALL: [SidebarTab; 2] = [Self::Inbox, Self::Communities];
pub fn label(self) -> &'static str { pub fn label(self) -> &'static str {
match self { match self {
Self::Recents => "Recents", Self::Inbox => "Inbox",
Self::Chats => "Chats",
Self::Communities => "Communities", Self::Communities => "Communities",
} }
} }
pub fn icon(self) -> IconName { /// Heading for the tab's list of items.
pub fn list_title(self) -> &'static str {
match self { match self {
Self::Recents => IconName::History, Self::Inbox => "Direct Messages",
Self::Chats => IconName::Message, Self::Communities => "Communities",
Self::Communities => IconName::Group,
} }
} }
pub fn list_id(self) -> &'static str { pub fn list_id(self) -> &'static str {
match self { match self {
Self::Recents => "sidebar-recents", Self::Inbox => "sidebar-inbox",
Self::Chats => "sidebar-chats",
Self::Communities => "sidebar-communities", Self::Communities => "sidebar-communities",
} }
} }
pub fn index(self) -> usize { pub fn index(self) -> usize {
match self { match self {
Self::Recents => 0, Self::Inbox => 0,
Self::Chats => 1, Self::Communities => 1,
Self::Communities => 2,
} }
} }
pub fn chat(self) -> bool {
matches!(self, Self::Chats)
}
pub fn community(self) -> bool {
matches!(self, Self::Communities)
}
}
#[derive(IntoElement)]
#[allow(clippy::type_complexity)]
pub struct TabBar {
active: SidebarTab,
on_select: Option<Rc<dyn Fn(SidebarTab, &mut Window, &mut App)>>,
}
impl TabBar {
pub fn new(active: SidebarTab) -> Self {
Self {
active,
on_select: None,
}
}
pub fn on_select(
mut self,
handler: impl Fn(SidebarTab, &mut Window, &mut App) + 'static,
) -> Self {
self.on_select = Some(Rc::new(handler));
self
}
}
impl RenderOnce for TabBar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let Self { active, on_select } = self;
div()
.id("sidebar-tabs")
.absolute()
.bottom_3()
.left_0()
.w_full()
.px_4()
.child(
h_flex()
.w_full()
.p_1()
.gap_1()
.rounded_full()
.bg(cx.theme().background)
.when(cx.theme().shadow, |this| this.shadow_md())
.children(SidebarTab::ALL.into_iter().map(|tab| {
let on_select = on_select.clone();
Button::new(format!("tab-{}", tab.list_id()))
.icon(tab.icon())
.ghost()
.flex_1()
.rounded()
.selected(tab == active)
.tooltip(tab.label())
.on_click(move |_event, window, cx| {
if let Some(on_select) = on_select.as_ref() {
on_select(tab, window, cx);
}
})
})),
)
}
} }
+30 -44
View File
@@ -1,36 +1,43 @@
use std::rc::Rc; use std::rc::Rc;
use chat::Room; use chat::Room;
use community::Community; use community::{ChannelId, Community};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
App, ClickEvent, ElementId, Entity, ImageSource, InteractiveElement, IntoElement, App, ClickEvent, ElementId, Entity, ImageSource, InteractiveElement, IntoElement,
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px,
}; };
use nostr_sdk::prelude::PublicKey;
use settings::AppSettings; use settings::AppSettings;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::{Avatar, PixelAvatar}; use ui::avatar::{Avatar, PixelAvatar};
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, h_flex}; use ui::{Icon, IconName, Selectable, Sizable, StyledExt, h_flex};
use super::tab::SidebarTab;
pub enum SidebarRow { pub enum SidebarRow {
Section { Room { room: Entity<Room> },
label: SharedString, Community { community: Entity<Community> },
count: usize, }
/// A collapsible group of rows in the sidebar's community view.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CommunitySection {
Channels,
Admins,
Members,
}
/// A row in the sidebar's community view.
pub enum CommunityRow {
Section(CommunitySection),
Channel {
id: ChannelId,
name: SharedString,
private: bool,
selected: bool,
}, },
Room { Member {
room: Entity<Room>, prefix: &'static str,
}, public_key: PublicKey,
Community {
community: Entity<Community>,
},
Action {
label: SharedString,
tab: SidebarTab,
},
Hint {
text: SharedString,
}, },
} }
@@ -39,8 +46,6 @@ pub enum TreeRowKind {
Section, Section,
Room, Room,
Community, Community,
Action,
Hint,
} }
#[derive(IntoElement)] #[derive(IntoElement)]
@@ -51,7 +56,6 @@ pub struct TreeRow {
avatar: Option<SharedString>, avatar: Option<SharedString>,
picture: Option<ImageSource>, picture: Option<ImageSource>,
icon: Option<IconName>, icon: Option<IconName>,
count: Option<usize>,
created_at: Option<SharedString>, created_at: Option<SharedString>,
selected: bool, selected: bool,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
@@ -71,7 +75,6 @@ impl TreeRow {
avatar: None, avatar: None,
picture: None, picture: None,
icon: None, icon: None,
count: None,
created_at: None, created_at: None,
selected: false, selected: false,
on_click: None, on_click: None,
@@ -96,11 +99,6 @@ impl TreeRow {
self self
} }
pub fn count(mut self, count: usize) -> Self {
self.count = Some(count);
self
}
pub fn created_at(mut self, created_at: impl Into<SharedString>) -> Self { pub fn created_at(mut self, created_at: impl Into<SharedString>) -> Self {
self.created_at = Some(created_at.into()); self.created_at = Some(created_at.into());
self self
@@ -133,8 +131,6 @@ impl RenderOnce for TreeRow {
let is_section = self.kind == TreeRowKind::Section; let is_section = self.kind == TreeRowKind::Section;
let is_room = self.kind == TreeRowKind::Room; let is_room = self.kind == TreeRowKind::Room;
let is_community = self.kind == TreeRowKind::Community; let is_community = self.kind == TreeRowKind::Community;
let is_action = self.kind == TreeRowKind::Action;
let is_hint = self.kind == TreeRowKind::Hint;
let is_selected = self.selected; let is_selected = self.selected;
let avatar = if hide_avatar { let avatar = if hide_avatar {
@@ -145,13 +141,13 @@ impl RenderOnce for TreeRow {
(seed, Some(picture)) => Some( (seed, Some(picture)) => Some(
Avatar::from_source(picture) Avatar::from_source(picture)
.when_some(seed, |avatar, seed| avatar.seed(seed)) .when_some(seed, |avatar, seed| avatar.seed(seed))
.xsmall() .small()
.flex_shrink_0() .flex_shrink_0()
.into_any_element(), .into_any_element(),
), ),
(Some(seed), None) => Some( (Some(seed), None) => Some(
PixelAvatar::new(seed) PixelAvatar::new(seed)
.xsmall() .small()
.flex_shrink_0() .flex_shrink_0()
.into_any_element(), .into_any_element(),
), ),
@@ -172,8 +168,8 @@ impl RenderOnce for TreeRow {
h_flex() h_flex()
.id(self.id) .id(self.id)
.h_8()
.w_full() .w_full()
.h_10()
.px_2() .px_2()
.gap_2() .gap_2()
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
@@ -183,14 +179,6 @@ impl RenderOnce for TreeRow {
.font_semibold() .font_semibold()
}) })
.when(is_room || is_community, |this| this.text_sm()) .when(is_room || is_community, |this| this.text_sm())
.when(is_action, |this| {
this.text_sm().text_color(cx.theme().text_muted)
})
.when(is_hint, |this| {
this.text_xs()
.font_normal()
.text_color(cx.theme().text_placeholder)
})
.when_some(avatar, |this, avatar| this.child(avatar)) .when_some(avatar, |this, avatar| this.child(avatar))
.child( .child(
h_flex() h_flex()
@@ -203,6 +191,7 @@ impl RenderOnce for TreeRow {
.when(is_room, |this| this.font_medium()) .when(is_room, |this| this.font_medium())
.child(self.label), .child(self.label),
) )
.child(div().flex_1())
.when(is_selected, |this| { .when(is_selected, |this| {
this.child( this.child(
Icon::new(IconName::CheckCircle) Icon::new(IconName::CheckCircle)
@@ -211,11 +200,8 @@ impl RenderOnce for TreeRow {
.text_color(cx.theme().icon_accent), .text_color(cx.theme().icon_accent),
) )
}) })
.when_some(self.count, |this, count| {
this.child(div().flex_shrink_0().font_normal().child(count.to_string()))
})
.when_some(self.created_at, |this, created_at| { .when_some(self.created_at, |this, created_at| {
this.child(div().flex_1()).child( this.child(
div() div()
.flex_shrink_0() .flex_shrink_0()
.text_color(cx.theme().text_placeholder) .text_color(cx.theme().text_placeholder)