4 Commits

Author SHA1 Message Date
ccbcc644db chore: make the ui consistent (#19)
Reviewed-on: #19
Co-authored-by: Ren Amamiya <reya@lume.nu>
Co-committed-by: Ren Amamiya <reya@lume.nu>
2026-03-12 02:19:59 +00:00
15c5ce7677 chore: remove ai stuffs 2026-03-10 17:28:51 +07:00
40d726c986 feat: refactor to use gpui event instead of local state (#18)
Reviewed-on: #18
Co-authored-by: Ren Amamiya <reya@lume.nu>
Co-committed-by: Ren Amamiya <reya@lume.nu>
2026-03-10 08:19:02 +00:00
fe4eb7df74 chore: re-add missing actions (#17)
Added:

- [x] Chage subject
- [x] Copy public key
- [x] View user's messaging relays
- [x] View user profile on njump

Reviewed-on: #17
Co-authored-by: Ren Amamiya <reya@lume.nu>
Co-committed-by: Ren Amamiya <reya@lume.nu>
2026-03-06 08:25:31 +00:00
71 changed files with 3599 additions and 2857 deletions

278
Cargo.lock generated
View File

@@ -220,9 +220,9 @@ dependencies = [
[[package]]
name = "ashpd"
version = "0.13.3"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e21900ac91937e4d9a51391f3569cd92fc38caea1a2a671d56b39797f3ece61f"
checksum = "5df42e54d7015b7a00d2db9f5cb9975bc4b0cd46627e666c94c3534207503e71"
dependencies = [
"enumflags2",
"futures-channel",
@@ -1200,7 +1200,7 @@ dependencies = [
[[package]]
name = "collections"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"indexmap",
"rustc-hash 2.1.1",
@@ -1316,9 +1316,12 @@ dependencies = [
[[package]]
name = "convert_case"
version = "0.4.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "coop"
@@ -1633,9 +1636,18 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
[[package]]
name = "derive_more"
version = "0.99.20"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
"convert_case",
"proc-macro2",
@@ -1647,7 +1659,7 @@ dependencies = [
[[package]]
name = "derive_refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"proc-macro2",
"quote",
@@ -1937,7 +1949,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -2001,6 +2013,18 @@ dependencies = [
"zune-inflate",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "1.9.0"
@@ -2611,7 +2635,7 @@ dependencies = [
[[package]]
name = "gpui"
version = "0.2.2"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -2690,7 +2714,7 @@ dependencies = [
[[package]]
name = "gpui_linux"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"anyhow",
"as-raw-xcb-connection",
@@ -2738,7 +2762,7 @@ dependencies = [
[[package]]
name = "gpui_macos"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"anyhow",
"async-task",
@@ -2780,7 +2804,7 @@ dependencies = [
[[package]]
name = "gpui_macros"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -2791,7 +2815,7 @@ dependencies = [
[[package]]
name = "gpui_platform"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"console_error_panic_hook",
"gpui",
@@ -2804,7 +2828,7 @@ dependencies = [
[[package]]
name = "gpui_tokio"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"anyhow",
"gpui",
@@ -2815,7 +2839,7 @@ dependencies = [
[[package]]
name = "gpui_util"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"anyhow",
"log",
@@ -2824,7 +2848,7 @@ dependencies = [
[[package]]
name = "gpui_web"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"anyhow",
"console_error_panic_hook",
@@ -2848,7 +2872,7 @@ dependencies = [
[[package]]
name = "gpui_wgpu"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"anyhow",
"bytemuck",
@@ -2876,7 +2900,7 @@ dependencies = [
[[package]]
name = "gpui_windows"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"anyhow",
"collections",
@@ -2937,9 +2961,9 @@ dependencies = [
[[package]]
name = "harfrust"
version = "0.5.0"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f9f40651a03bc0f7316bd75267ff5767e93017ef3cfffe76c6aa7252cc5a31c"
checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9"
dependencies = [
"bitflags 2.11.0",
"bytemuck",
@@ -3119,7 +3143,7 @@ dependencies = [
[[package]]
name = "http_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"anyhow",
"async-compression",
@@ -3144,7 +3168,7 @@ dependencies = [
[[package]]
name = "http_client_tls"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"rustls",
"rustls-platform-verifier",
@@ -3690,6 +3714,17 @@ dependencies = [
"redox_syscall 0.7.3",
]
[[package]]
name = "libsqlite3-sys"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linebender_resource_handle"
version = "0.1.1"
@@ -3924,7 +3959,7 @@ dependencies = [
[[package]]
name = "media"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"anyhow",
"bindgen",
@@ -4065,7 +4100,7 @@ dependencies = [
[[package]]
name = "naga"
version = "28.0.1"
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
dependencies = [
"arrayvec",
"bit-set",
@@ -4238,6 +4273,18 @@ dependencies = [
"nostr",
]
[[package]]
name = "nostr-gossip-sqlite"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
dependencies = [
"async-utility",
"nostr",
"nostr-gossip",
"rusqlite",
"tokio",
]
[[package]]
name = "nostr-lmdb"
version = "0.44.0"
@@ -4252,6 +4299,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "nostr-memory"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
dependencies = [
"btreecap",
"nostr",
"nostr-database",
"tokio",
]
[[package]]
name = "nostr-sdk"
version = "0.44.1"
@@ -4287,7 +4345,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -4692,7 +4750,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "perf"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"collections",
"serde",
@@ -4712,6 +4770,7 @@ dependencies = [
"smallvec",
"smol",
"state",
"urlencoding",
]
[[package]]
@@ -4962,11 +5021,11 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit 0.23.10+spec-1.0.0",
"toml_edit 0.25.4+spec-1.1.0",
]
[[package]]
@@ -5089,9 +5148,9 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.38.4"
version = "0.39.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
dependencies = [
"memchr",
]
@@ -5148,7 +5207,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -5401,7 +5460,7 @@ dependencies = [
[[package]]
name = "refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"derive_refineable",
]
@@ -5500,7 +5559,7 @@ dependencies = [
[[package]]
name = "reqwest_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"anyhow",
"bytes",
@@ -5555,7 +5614,7 @@ dependencies = [
[[package]]
name = "rope"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"arrayvec",
"log",
@@ -5573,6 +5632,30 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "rsqlite-vfs"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d"
dependencies = [
"hashbrown 0.16.1",
"thiserror 2.0.18",
]
[[package]]
name = "rusqlite"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3"
dependencies = [
"bitflags 2.11.0",
"fallible-iterator",
"fallible-streaming-iterator",
"libsqlite3-sys",
"smallvec",
"sqlite-wasm-rs",
]
[[package]]
name = "rust-embed"
version = "8.11.0"
@@ -5668,7 +5751,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -5817,7 +5900,7 @@ dependencies = [
[[package]]
name = "scheduler"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"async-task",
"backtrace",
@@ -6295,6 +6378,18 @@ dependencies = [
"bitflags 2.11.0",
]
[[package]]
name = "sqlite-wasm-rs"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b"
dependencies = [
"cc",
"js-sys",
"rsqlite-vfs",
"wasm-bindgen",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -6349,7 +6444,9 @@ dependencies = [
"nostr",
"nostr-blossom",
"nostr-connect",
"nostr-gossip-sqlite",
"nostr-lmdb",
"nostr-memory",
"nostr-sdk",
"petname",
"rustls",
@@ -6411,7 +6508,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "sum_tree"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"arrayvec",
"log",
@@ -6649,7 +6746,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -6960,6 +7057,15 @@ dependencies = [
"serde_core",
]
[[package]]
name = "toml_datetime"
version = "1.0.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
@@ -6976,12 +7082,12 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.23.10+spec-1.0.0"
version = "0.25.4+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
dependencies = [
"indexmap",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_datetime 1.0.0+spec-1.1.0",
"toml_parser",
"winnow",
]
@@ -7162,13 +7268,13 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "uds_windows"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca"
dependencies = [
"memoffset",
"tempfile",
"winapi",
"windows-sys 0.59.0",
]
[[package]]
@@ -7310,6 +7416,12 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "usvg"
version = "0.45.1"
@@ -7358,7 +7470,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "util"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"anyhow",
"async-fs",
@@ -7397,7 +7509,7 @@ dependencies = [
[[package]]
name = "util_macros"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"perf",
"quote",
@@ -7406,9 +7518,9 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.21.0"
version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
dependencies = [
"getrandom 0.4.2",
"js-sys",
@@ -7470,6 +7582,12 @@ dependencies = [
"sval_serde",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
@@ -7671,9 +7789,9 @@ dependencies = [
[[package]]
name = "wayland-backend"
version = "0.3.12"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9"
checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406"
dependencies = [
"cc",
"downcast-rs",
@@ -7685,9 +7803,9 @@ dependencies = [
[[package]]
name = "wayland-client"
version = "0.31.12"
version = "0.31.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec"
checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3"
dependencies = [
"bitflags 2.11.0",
"rustix 1.1.4",
@@ -7697,9 +7815,9 @@ dependencies = [
[[package]]
name = "wayland-cursor"
version = "0.31.12"
version = "0.31.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078"
checksum = "4b3298683470fbdc6ca40151dfc48c8f2fd4c41a26e13042f801f85002384091"
dependencies = [
"rustix 1.1.4",
"wayland-client",
@@ -7708,9 +7826,9 @@ dependencies = [
[[package]]
name = "wayland-protocols"
version = "0.32.10"
version = "0.32.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3"
checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7"
dependencies = [
"bitflags 2.11.0",
"wayland-backend",
@@ -7720,9 +7838,9 @@ dependencies = [
[[package]]
name = "wayland-protocols-plasma"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b"
checksum = "d392fc283a87774afc9beefcd6f931582bb97fe0e6ced0b306a62cb1d026527c"
dependencies = [
"bitflags 2.11.0",
"wayland-backend",
@@ -7733,9 +7851,9 @@ dependencies = [
[[package]]
name = "wayland-protocols-wlr"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3"
checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235"
dependencies = [
"bitflags 2.11.0",
"wayland-backend",
@@ -7746,20 +7864,20 @@ dependencies = [
[[package]]
name = "wayland-scanner"
version = "0.31.8"
version = "0.31.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3"
checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3"
dependencies = [
"proc-macro2",
"quick-xml 0.38.4",
"quick-xml 0.39.2",
"quote",
]
[[package]]
name = "wayland-sys"
version = "0.31.8"
version = "0.31.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd"
checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17"
dependencies = [
"dlib",
"log",
@@ -7848,7 +7966,7 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "wgpu"
version = "28.0.1"
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
dependencies = [
"arrayvec",
"bitflags 2.11.0",
@@ -7877,7 +7995,7 @@ dependencies = [
[[package]]
name = "wgpu-core"
version = "28.0.1"
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
dependencies = [
"arrayvec",
"bit-set",
@@ -7908,7 +8026,7 @@ dependencies = [
[[package]]
name = "wgpu-core-deps-apple"
version = "28.0.1"
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
dependencies = [
"wgpu-hal",
]
@@ -7916,7 +8034,7 @@ dependencies = [
[[package]]
name = "wgpu-core-deps-emscripten"
version = "28.0.1"
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
dependencies = [
"wgpu-hal",
]
@@ -7924,7 +8042,7 @@ dependencies = [
[[package]]
name = "wgpu-core-deps-windows-linux-android"
version = "28.0.1"
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
dependencies = [
"wgpu-hal",
]
@@ -7932,7 +8050,7 @@ dependencies = [
[[package]]
name = "wgpu-hal"
version = "28.0.1"
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
dependencies = [
"android_system_properties",
"arrayvec",
@@ -7979,7 +8097,7 @@ dependencies = [
[[package]]
name = "wgpu-types"
version = "28.0.1"
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
dependencies = [
"bitflags 2.11.0",
"bytemuck",
@@ -8033,7 +8151,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.48.0",
]
[[package]]
@@ -8637,9 +8755,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.14"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
@@ -9193,7 +9311,7 @@ dependencies = [
[[package]]
name = "zlog"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"anyhow",
"chrono",
@@ -9210,7 +9328,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "ztracing"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
dependencies = [
"tracing",
"tracing-subscriber",
@@ -9221,7 +9339,7 @@ dependencies = [
[[package]]
name = "ztracing_macro"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
[[package]]
name = "zune-core"

View File

@@ -11,7 +11,7 @@ publish = false
[workspace.dependencies]
# GPUI
gpui = { git = "https://github.com/zed-industries/zed" }
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "x11", "wayland", "runtime_shaders"] }
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "x11", "wayland"] }
gpui_linux = { git = "https://github.com/zed-industries/zed" }
gpui_windows = { git = "https://github.com/zed-industries/zed" }
gpui_macos = { git = "https://github.com/zed-industries/zed" }
@@ -20,8 +20,10 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" }
# Nostr
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
nostr-memory = { git = "https://github.com/rust-nostr/nostr" }
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
nostr-blossom = { git = "https://github.com/rust-nostr/nostr" }
nostr-gossip-sqlite = { git = "https://github.com/rust-nostr/nostr" }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] }

3
assets/icons/input.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M11.75 6.75H19.25C20.3546 6.75 21.25 7.64543 21.25 8.75V15.25C21.25 16.3546 20.3546 17.25 19.25 17.25H11.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.75 6.75H4.75C3.64543 6.75 2.75 7.64543 2.75 8.75V15.25C2.75 16.3546 3.64543 17.25 4.75 17.25H5.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.75 3.75V20.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 604 B

144
assets/themes/aurora.json Normal file
View File

@@ -0,0 +1,144 @@
{
"id": "aurora",
"name": "Aurora",
"author": "Coop",
"url": "https://github.com/lumehq/coop",
"light": {
"background": "#fdfcfeff",
"surface_background": "#f8f8ffff",
"elevated_surface_background": "#f0f1feff",
"panel_background": "#fdfcfeff",
"overlay": "#211f4300",
"title_bar": "#f0f1feff",
"title_bar_inactive": "#fdfcfeff",
"window_border": "#dadcffff",
"border": "#dadcffff",
"border_variant": "#cbcdffff",
"border_focused": "#5b5bd6ff",
"border_selected": "#5b5bd6ff",
"border_transparent": "#00000000",
"border_disabled": "#e6e7ffff",
"ring": "#5151cdff",
"text": "#1f2d5cff",
"text_muted": "#5753c6ff",
"text_placeholder": "#9b9ef0ff",
"text_accent": "#5b5bd6ff",
"text_danger": "#e54d2eff",
"text_warning": "#f76b15ff",
"icon": "#5753c6ff",
"icon_muted": "#9b9ef0ff",
"icon_accent": "#5151cdff",
"element_foreground": "#ffffffff",
"element_background": "#5b5bd6ff",
"element_hover": "#5151cdff",
"element_active": "#6e56cfff",
"element_selected": "#654dc4ff",
"element_disabled": "#5b5bd64d",
"secondary_foreground": "#1f2d5cff",
"secondary_background": "#f0f1feff",
"secondary_hover": "#e6e7ffff",
"secondary_active": "#dadcffff",
"secondary_selected": "#dadcffff",
"secondary_disabled": "#5b5bd64d",
"danger_foreground": "#ffffffff",
"danger_background": "#feebe7ff",
"danger_hover": "#ffcdc2ff",
"danger_active": "#fdbdafff",
"danger_selected": "#fdbdafff",
"danger_disabled": "#e54d2e4d",
"warning_foreground": "#ffffffff",
"warning_background": "#fff7edff",
"warning_hover": "#ffd19aff",
"warning_active": "#ffc182ff",
"warning_selected": "#ffc182ff",
"warning_disabled": "#f76b154d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#f0f1feff",
"ghost_element_hover": "#211f430d",
"ghost_element_active": "#211f431a",
"ghost_element_selected": "#211f431a",
"ghost_element_disabled": "#211f4305",
"tab_background": "#f0f1feff",
"tab_foreground": "#5753c6ff",
"tab_hover_background": "#211f430d",
"tab_active_background": "#fdfcfeff",
"tab_active_foreground": "#1f2d5cff",
"scrollbar_thumb_background": "#211f431a",
"scrollbar_thumb_hover_background": "#211f4326",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#00000000",
"drop_target_background": "#5b5bd61a",
"cursor": "#5b5bd6ff",
"selection": "#5b5bd640"
},
"dark": {
"background": "#14121fff",
"surface_background": "#1b1525ff",
"elevated_surface_background": "#291f43ff",
"panel_background": "#14121fff",
"overlay": "#baa7ff1a",
"title_bar": "#291f43ff",
"title_bar_inactive": "#14121fff",
"window_border": "#473876ff",
"border": "#473876ff",
"border_variant": "#3c2e69ff",
"border_focused": "#7d66d9ff",
"border_selected": "#7d66d9ff",
"border_transparent": "#00000000",
"border_disabled": "#33255bff",
"ring": "#6e56cfff",
"text": "#e2ddfeff",
"text_muted": "#baa7ffff",
"text_placeholder": "#6958adff",
"text_accent": "#baa7ffff",
"text_danger": "#ff977dff",
"text_warning": "#ffa057ff",
"icon": "#baa7ffff",
"icon_muted": "#6958adff",
"icon_accent": "#6e56cfff",
"element_foreground": "#14121fff",
"element_background": "#7d66d9ff",
"element_hover": "#baa7ffff",
"element_active": "#6e56cfff",
"element_selected": "#654dc4ff",
"element_disabled": "#7d66d94d",
"secondary_foreground": "#e2ddfeff",
"secondary_background": "#291f43ff",
"secondary_hover": "#33255bff",
"secondary_active": "#3c2e69ff",
"secondary_selected": "#3c2e69ff",
"secondary_disabled": "#7d66d94d",
"danger_foreground": "#181111ff",
"danger_background": "#391714ff",
"danger_hover": "#5e1c16ff",
"danger_active": "#6e2920ff",
"danger_selected": "#6e2920ff",
"danger_disabled": "#ff977d4d",
"warning_foreground": "#17120eff",
"warning_background": "#331e0bff",
"warning_hover": "#562800ff",
"warning_active": "#66350cff",
"warning_selected": "#66350cff",
"warning_disabled": "#ffa0574d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#291f43ff",
"ghost_element_hover": "#baa7ff0d",
"ghost_element_active": "#baa7ff1a",
"ghost_element_selected": "#baa7ff1a",
"ghost_element_disabled": "#baa7ff05",
"tab_background": "#291f43ff",
"tab_foreground": "#baa7ffff",
"tab_hover_background": "#baa7ff0d",
"tab_active_background": "#14121fff",
"tab_active_foreground": "#e2ddfeff",
"scrollbar_thumb_background": "#baa7ff1a",
"scrollbar_thumb_hover_background": "#baa7ff26",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#00000000",
"drop_target_background": "#baa7ff1a",
"cursor": "#baa7ffff",
"selection": "#baa7ff40"
}
}

View File

@@ -1,74 +1,76 @@
{
"id": "catppuccin-frappe",
"name": "Catppuccin Frappé",
"author": "Catppuccin",
"url": "https://github.com/catppuccin/catppuccin",
"author": "Catppuccin Org (ported by Coop)",
"url": "https://catppuccin.com",
"light": {
"background": "#303446",
"surface_background": "#292c3c",
"elevated_surface_background": "#232634",
"panel_background": "#303446",
"overlay": "#c6d0f51a",
"title_bar": "#292c3c",
"title_bar_inactive": "#232634",
"window_border": "#737994",
"border": "#626880",
"border_variant": "#51576d",
"title_bar": "#232634",
"title_bar_inactive": "#303446",
"window_border": "#51576d",
"border": "#51576d",
"border_variant": "#414559",
"border_focused": "#8caaee",
"border_selected": "#8caaee",
"border_transparent": "#00000000",
"border_disabled": "#414559",
"ring": "#8caaee",
"border_transparent": "#c6d0f500",
"border_disabled": "#292c3c",
"ring": "#babbf1",
"text": "#c6d0f5",
"text_muted": "#b5bfe2",
"text_placeholder": "#a5adce",
"text_muted": "#a5adce",
"text_placeholder": "#838ba7",
"text_accent": "#8caaee",
"icon": "#b5bfe2",
"icon_muted": "#a5adce",
"icon_accent": "#8caaee",
"element_foreground": "#232634",
"text_danger": "#e78284",
"text_warning": "#ef9f76",
"icon": "#a5adce",
"icon_muted": "#838ba7",
"icon_accent": "#babbf1",
"element_foreground": "#303446",
"element_background": "#8caaee",
"element_hover": "#babbf1",
"element_active": "#7e99d6",
"element_selected": "#7088bf",
"element_active": "#99d1db",
"element_selected": "#85c1dc",
"element_disabled": "#8caaee4d",
"secondary_foreground": "#7088bf",
"secondary_background": "#292c3c",
"secondary_hover": "#8caaee33",
"secondary_active": "#232634",
"secondary_selected": "#232634",
"secondary_foreground": "#c6d0f5",
"secondary_background": "#414559",
"secondary_hover": "#51576d",
"secondary_active": "#626880",
"secondary_selected": "#626880",
"secondary_disabled": "#8caaee4d",
"danger_foreground": "#232634",
"danger_foreground": "#303446",
"danger_background": "#e78284",
"danger_hover": "#ea999c",
"danger_active": "#d07576",
"danger_selected": "#b96869",
"danger_active": "#ef9f76",
"danger_selected": "#e5c890",
"danger_disabled": "#e782844d",
"warning_foreground": "#232634",
"warning_background": "#e5c890",
"warning_hover": "#ef9f76",
"warning_active": "#ceb482",
"warning_selected": "#b7a074",
"warning_disabled": "#e5c8904d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#414559",
"ghost_element_hover": "#c6d0f533",
"ghost_element_active": "#51576d",
"ghost_element_selected": "#51576d",
"ghost_element_disabled": "#c6d0f50d",
"tab_inactive_background": "#292c3c",
"tab_inactive_foreground": "#b5bfe2",
"warning_foreground": "#303446",
"warning_background": "#ef9f76",
"warning_hover": "#e5c890",
"warning_active": "#a6d189",
"warning_selected": "#81c8be",
"warning_disabled": "#ef9f764d",
"ghost_element_background": "#c6d0f500",
"ghost_element_background_alt": "#292c3c",
"ghost_element_hover": "#c6d0f50d",
"ghost_element_active": "#c6d0f51a",
"ghost_element_selected": "#c6d0f51a",
"ghost_element_disabled": "#c6d0f505",
"tab_background": "#232634",
"tab_foreground": "#a5adce",
"tab_hover_background": "#c6d0f50d",
"tab_active_background": "#303446",
"tab_active_foreground": "#c6d0f5",
"tab_hover_foreground": "#babbf1",
"scrollbar_thumb_background": "#c6d0f533",
"scrollbar_thumb_hover_background": "#c6d0f580",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#51576d",
"scrollbar_thumb_background": "#c6d0f51a",
"scrollbar_thumb_hover_background": "#c6d0f526",
"scrollbar_thumb_border": "#c6d0f500",
"scrollbar_track_background": "#c6d0f500",
"scrollbar_track_border": "#c6d0f500",
"drop_target_background": "#8caaee1a",
"cursor": "#f2d5cf",
"selection": "#949cbb40"
"cursor": "#8caaee",
"selection": "#8caaee40"
},
"dark": {
"background": "#303446",
@@ -76,65 +78,67 @@
"elevated_surface_background": "#232634",
"panel_background": "#303446",
"overlay": "#c6d0f51a",
"title_bar": "#292c3c",
"title_bar_inactive": "#232634",
"window_border": "#737994",
"border": "#626880",
"border_variant": "#51576d",
"title_bar": "#232634",
"title_bar_inactive": "#303446",
"window_border": "#51576d",
"border": "#51576d",
"border_variant": "#414559",
"border_focused": "#8caaee",
"border_selected": "#8caaee",
"border_transparent": "#00000000",
"border_disabled": "#414559",
"ring": "#8caaee",
"border_transparent": "#c6d0f500",
"border_disabled": "#292c3c",
"ring": "#babbf1",
"text": "#c6d0f5",
"text_muted": "#b5bfe2",
"text_placeholder": "#a5adce",
"text_muted": "#a5adce",
"text_placeholder": "#838ba7",
"text_accent": "#8caaee",
"icon": "#b5bfe2",
"icon_muted": "#a5adce",
"icon_accent": "#8caaee",
"element_foreground": "#232634",
"text_danger": "#e78284",
"text_warning": "#ef9f76",
"icon": "#a5adce",
"icon_muted": "#838ba7",
"icon_accent": "#babbf1",
"element_foreground": "#303446",
"element_background": "#8caaee",
"element_hover": "#babbf1",
"element_active": "#7e99d6",
"element_selected": "#7088bf",
"element_active": "#99d1db",
"element_selected": "#85c1dc",
"element_disabled": "#8caaee4d",
"secondary_foreground": "#7088bf",
"secondary_background": "#292c3c",
"secondary_hover": "#8caaee33",
"secondary_active": "#232634",
"secondary_selected": "#232634",
"secondary_foreground": "#c6d0f5",
"secondary_background": "#414559",
"secondary_hover": "#51576d",
"secondary_active": "#626880",
"secondary_selected": "#626880",
"secondary_disabled": "#8caaee4d",
"danger_foreground": "#232634",
"danger_foreground": "#303446",
"danger_background": "#e78284",
"danger_hover": "#ea999c",
"danger_active": "#d07576",
"danger_selected": "#b96869",
"danger_active": "#ef9f76",
"danger_selected": "#e5c890",
"danger_disabled": "#e782844d",
"warning_foreground": "#232634",
"warning_background": "#e5c890",
"warning_hover": "#ef9f76",
"warning_active": "#ceb482",
"warning_selected": "#b7a074",
"warning_disabled": "#e5c8904d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#414559",
"ghost_element_hover": "#c6d0f533",
"ghost_element_active": "#51576d",
"ghost_element_selected": "#51576d",
"ghost_element_disabled": "#c6d0f50d",
"tab_inactive_background": "#292c3c",
"tab_inactive_foreground": "#b5bfe2",
"warning_foreground": "#303446",
"warning_background": "#ef9f76",
"warning_hover": "#e5c890",
"warning_active": "#a6d189",
"warning_selected": "#81c8be",
"warning_disabled": "#ef9f764d",
"ghost_element_background": "#c6d0f500",
"ghost_element_background_alt": "#292c3c",
"ghost_element_hover": "#c6d0f50d",
"ghost_element_active": "#c6d0f51a",
"ghost_element_selected": "#c6d0f51a",
"ghost_element_disabled": "#c6d0f505",
"tab_background": "#232634",
"tab_foreground": "#a5adce",
"tab_hover_background": "#c6d0f50d",
"tab_active_background": "#303446",
"tab_active_foreground": "#c6d0f5",
"tab_hover_foreground": "#babbf1",
"scrollbar_thumb_background": "#c6d0f533",
"scrollbar_thumb_hover_background": "#c6d0f580",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#51576d",
"scrollbar_thumb_background": "#c6d0f51a",
"scrollbar_thumb_hover_background": "#c6d0f526",
"scrollbar_thumb_border": "#c6d0f500",
"scrollbar_track_background": "#c6d0f500",
"scrollbar_track_border": "#c6d0f500",
"drop_target_background": "#8caaee1a",
"cursor": "#f2d5cf",
"selection": "#949cbb40"
"cursor": "#8caaee",
"selection": "#8caaee40"
}
}

View File

@@ -1,74 +1,76 @@
{
"id": "catppuccin-latte",
"name": "Catppuccin Latte",
"author": "Catppuccin",
"url": "https://github.com/catppuccin/catppuccin",
"author": "Catppuccin Org (ported by Coop)",
"url": "https://catppuccin.com",
"light": {
"background": "#eff1f5",
"surface_background": "#e6e9ef",
"elevated_surface_background": "#dce0e8",
"panel_background": "#eff1f5",
"overlay": "#4c4f691a",
"title_bar": "#e6e9ef",
"title_bar_inactive": "#dce0e8",
"window_border": "#9ca0b0",
"border": "#acb0be",
"border_variant": "#bcc0cc",
"title_bar": "#dce0e8",
"title_bar_inactive": "#eff1f5",
"window_border": "#bcc0cc",
"border": "#bcc0cc",
"border_variant": "#ccd0da",
"border_focused": "#1e66f5",
"border_selected": "#1e66f5",
"border_transparent": "#00000000",
"border_disabled": "#ccd0da",
"ring": "#1e66f5",
"border_transparent": "#4c4f6900",
"border_disabled": "#e6e9ef",
"ring": "#7287fd",
"text": "#4c4f69",
"text_muted": "#5c5f77",
"text_placeholder": "#6c6f85",
"text_muted": "#6c6f85",
"text_placeholder": "#8c8fa1",
"text_accent": "#1e66f5",
"icon": "#5c5f77",
"icon_muted": "#6c6f85",
"icon_accent": "#1e66f5",
"text_danger": "#d20f39",
"text_warning": "#fe640b",
"icon": "#6c6f85",
"icon_muted": "#8c8fa1",
"icon_accent": "#7287fd",
"element_foreground": "#eff1f5",
"element_background": "#1e66f5",
"element_hover": "#8839ef",
"element_active": "#1c5ce0",
"element_selected": "#1a52cc",
"element_hover": "#7287fd",
"element_active": "#04a5e5",
"element_selected": "#209fb5",
"element_disabled": "#1e66f54d",
"secondary_foreground": "#1a52cc",
"secondary_background": "#e6e9ef",
"secondary_hover": "#8839ef33",
"secondary_active": "#dce0e8",
"secondary_selected": "#dce0e8",
"secondary_foreground": "#4c4f69",
"secondary_background": "#ccd0da",
"secondary_hover": "#bcc0cc",
"secondary_active": "#acb0be",
"secondary_selected": "#acb0be",
"secondary_disabled": "#1e66f54d",
"danger_foreground": "#eff1f5",
"danger_background": "#d20f39",
"danger_hover": "#e64553",
"danger_active": "#bd0d33",
"danger_selected": "#a80b2d",
"danger_active": "#fe640b",
"danger_selected": "#df8e1d",
"danger_disabled": "#d20f394d",
"warning_foreground": "#4c4f69",
"warning_background": "#df8e1d",
"warning_hover": "#fe640b",
"warning_active": "#c9801a",
"warning_selected": "#b47217",
"warning_disabled": "#df8e1d4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#ccd0da",
"ghost_element_hover": "#4c4f6933",
"ghost_element_active": "#bcc0cc",
"ghost_element_selected": "#bcc0cc",
"ghost_element_disabled": "#4c4f690d",
"tab_inactive_background": "#e6e9ef",
"tab_inactive_foreground": "#5c5f77",
"warning_foreground": "#eff1f5",
"warning_background": "#fe640b",
"warning_hover": "#df8e1d",
"warning_active": "#40a02b",
"warning_selected": "#179299",
"warning_disabled": "#fe640b4d",
"ghost_element_background": "#4c4f6900",
"ghost_element_background_alt": "#e6e9ef",
"ghost_element_hover": "#4c4f690d",
"ghost_element_active": "#4c4f691a",
"ghost_element_selected": "#4c4f691a",
"ghost_element_disabled": "#4c4f6905",
"tab_background": "#e6e9ef",
"tab_foreground": "#6c6f85",
"tab_hover_background": "#4c4f690d",
"tab_active_background": "#eff1f5",
"tab_active_foreground": "#4c4f69",
"tab_hover_foreground": "#8839ef",
"scrollbar_thumb_background": "#4c4f6933",
"scrollbar_thumb_hover_background": "#4c4f6980",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#bcc0cc",
"scrollbar_thumb_background": "#4c4f691a",
"scrollbar_thumb_hover_background": "#4c4f6926",
"scrollbar_thumb_border": "#4c4f6900",
"scrollbar_track_background": "#4c4f6900",
"scrollbar_track_border": "#4c4f6900",
"drop_target_background": "#1e66f51a",
"cursor": "#dc8a78",
"selection": "#7c7f9340"
"cursor": "#1e66f5",
"selection": "#1e66f540"
},
"dark": {
"background": "#eff1f5",
@@ -76,65 +78,67 @@
"elevated_surface_background": "#dce0e8",
"panel_background": "#eff1f5",
"overlay": "#4c4f691a",
"title_bar": "#e6e9ef",
"title_bar_inactive": "#dce0e8",
"window_border": "#9ca0b0",
"border": "#acb0be",
"border_variant": "#bcc0cc",
"title_bar": "#dce0e8",
"title_bar_inactive": "#eff1f5",
"window_border": "#bcc0cc",
"border": "#bcc0cc",
"border_variant": "#ccd0da",
"border_focused": "#1e66f5",
"border_selected": "#1e66f5",
"border_transparent": "#00000000",
"border_disabled": "#ccd0da",
"ring": "#1e66f5",
"border_transparent": "#4c4f6900",
"border_disabled": "#e6e9ef",
"ring": "#7287fd",
"text": "#4c4f69",
"text_muted": "#5c5f77",
"text_placeholder": "#6c6f85",
"text_muted": "#6c6f85",
"text_placeholder": "#8c8fa1",
"text_accent": "#1e66f5",
"icon": "#5c5f77",
"icon_muted": "#6c6f85",
"icon_accent": "#1e66f5",
"text_danger": "#d20f39",
"text_warning": "#fe640b",
"icon": "#6c6f85",
"icon_muted": "#8c8fa1",
"icon_accent": "#7287fd",
"element_foreground": "#eff1f5",
"element_background": "#1e66f5",
"element_hover": "#8839ef",
"element_active": "#1c5ce0",
"element_selected": "#1a52cc",
"element_hover": "#7287fd",
"element_active": "#04a5e5",
"element_selected": "#209fb5",
"element_disabled": "#1e66f54d",
"secondary_foreground": "#1a52cc",
"secondary_background": "#e6e9ef",
"secondary_hover": "#8839ef33",
"secondary_active": "#dce0e8",
"secondary_selected": "#dce0e8",
"secondary_foreground": "#4c4f69",
"secondary_background": "#ccd0da",
"secondary_hover": "#bcc0cc",
"secondary_active": "#acb0be",
"secondary_selected": "#acb0be",
"secondary_disabled": "#1e66f54d",
"danger_foreground": "#eff1f5",
"danger_background": "#d20f39",
"danger_hover": "#e64553",
"danger_active": "#bd0d33",
"danger_selected": "#a80b2d",
"danger_active": "#fe640b",
"danger_selected": "#df8e1d",
"danger_disabled": "#d20f394d",
"warning_foreground": "#4c4f69",
"warning_background": "#df8e1d",
"warning_hover": "#fe640b",
"warning_active": "#c9801a",
"warning_selected": "#b47217",
"warning_disabled": "#df8e1d4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#ccd0da",
"ghost_element_hover": "#4c4f6933",
"ghost_element_active": "#bcc0cc",
"ghost_element_selected": "#bcc0cc",
"ghost_element_disabled": "#4c4f690d",
"tab_inactive_background": "#e6e9ef",
"tab_inactive_foreground": "#5c5f77",
"warning_foreground": "#eff1f5",
"warning_background": "#fe640b",
"warning_hover": "#df8e1d",
"warning_active": "#40a02b",
"warning_selected": "#179299",
"warning_disabled": "#fe640b4d",
"ghost_element_background": "#4c4f6900",
"ghost_element_background_alt": "#e6e9ef",
"ghost_element_hover": "#4c4f690d",
"ghost_element_active": "#4c4f691a",
"ghost_element_selected": "#4c4f691a",
"ghost_element_disabled": "#4c4f6905",
"tab_background": "#e6e9ef",
"tab_foreground": "#6c6f85",
"tab_hover_background": "#4c4f690d",
"tab_active_background": "#eff1f5",
"tab_active_foreground": "#4c4f69",
"tab_hover_foreground": "#8839ef",
"scrollbar_thumb_background": "#4c4f6933",
"scrollbar_thumb_hover_background": "#4c4f6980",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#bcc0cc",
"scrollbar_thumb_background": "#4c4f691a",
"scrollbar_thumb_hover_background": "#4c4f6926",
"scrollbar_thumb_border": "#4c4f6900",
"scrollbar_track_background": "#4c4f6900",
"scrollbar_track_border": "#4c4f6900",
"drop_target_background": "#1e66f51a",
"cursor": "#dc8a78",
"selection": "#7c7f9340"
"cursor": "#1e66f5",
"selection": "#1e66f540"
}
}

View File

@@ -1,74 +1,76 @@
{
"id": "catppuccin-macchiato",
"name": "Catppuccin Macchiato",
"author": "Catppuccin",
"url": "https://github.com/catppuccin/catppuccin",
"author": "Catppuccin Org (ported by Coop)",
"url": "https://catppuccin.com",
"light": {
"background": "#24273a",
"surface_background": "#1e2030",
"elevated_surface_background": "#181926",
"panel_background": "#24273a",
"overlay": "#cad3f51a",
"title_bar": "#1e2030",
"title_bar_inactive": "#181926",
"window_border": "#6e738d",
"border": "#5b6078",
"border_variant": "#494d64",
"title_bar": "#181926",
"title_bar_inactive": "#24273a",
"window_border": "#494d64",
"border": "#494d64",
"border_variant": "#363a4f",
"border_focused": "#8aadf4",
"border_selected": "#8aadf4",
"border_transparent": "#00000000",
"border_disabled": "#363a4f",
"ring": "#8aadf4",
"border_transparent": "#cad3f500",
"border_disabled": "#1e2030",
"ring": "#b7bdf8",
"text": "#cad3f5",
"text_muted": "#b8c0e0",
"text_placeholder": "#a5adcb",
"text_muted": "#a5adcb",
"text_placeholder": "#8087a2",
"text_accent": "#8aadf4",
"icon": "#b8c0e0",
"icon_muted": "#a5adcb",
"icon_accent": "#8aadf4",
"element_foreground": "#181926",
"text_danger": "#ed8796",
"text_warning": "#f5a97f",
"icon": "#a5adcb",
"icon_muted": "#8087a2",
"icon_accent": "#b7bdf8",
"element_foreground": "#24273a",
"element_background": "#8aadf4",
"element_hover": "#b7bdf8",
"element_active": "#7c9cdc",
"element_selected": "#6e8bc5",
"element_active": "#91d7e3",
"element_selected": "#7dc4e4",
"element_disabled": "#8aadf44d",
"secondary_foreground": "#6e8bc5",
"secondary_background": "#1e2030",
"secondary_hover": "#8aadf433",
"secondary_active": "#181926",
"secondary_selected": "#181926",
"secondary_foreground": "#cad3f5",
"secondary_background": "#363a4f",
"secondary_hover": "#494d64",
"secondary_active": "#5b6078",
"secondary_selected": "#5b6078",
"secondary_disabled": "#8aadf44d",
"danger_foreground": "#181926",
"danger_foreground": "#24273a",
"danger_background": "#ed8796",
"danger_hover": "#ee99a0",
"danger_active": "#d57a87",
"danger_selected": "#be6d78",
"danger_active": "#f5a97f",
"danger_selected": "#eed49f",
"danger_disabled": "#ed87964d",
"warning_foreground": "#181926",
"warning_background": "#eed49f",
"warning_hover": "#f5a97f",
"warning_active": "#d6bf8f",
"warning_selected": "#beaa7f",
"warning_disabled": "#eed49f4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#363a4f",
"ghost_element_hover": "#cad3f533",
"ghost_element_active": "#494d64",
"ghost_element_selected": "#494d64",
"ghost_element_disabled": "#cad3f50d",
"tab_inactive_background": "#1e2030",
"tab_inactive_foreground": "#b8c0e0",
"warning_foreground": "#24273a",
"warning_background": "#f5a97f",
"warning_hover": "#eed49f",
"warning_active": "#a6da95",
"warning_selected": "#8bd5ca",
"warning_disabled": "#f5a97f4d",
"ghost_element_background": "#cad3f500",
"ghost_element_background_alt": "#1e2030",
"ghost_element_hover": "#cad3f50d",
"ghost_element_active": "#cad3f51a",
"ghost_element_selected": "#cad3f51a",
"ghost_element_disabled": "#cad3f505",
"tab_background": "#181926",
"tab_foreground": "#a5adcb",
"tab_hover_background": "#cad3f50d",
"tab_active_background": "#24273a",
"tab_active_foreground": "#cad3f5",
"tab_hover_foreground": "#b7bdf8",
"scrollbar_thumb_background": "#cad3f533",
"scrollbar_thumb_hover_background": "#cad3f580",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#494d64",
"scrollbar_thumb_background": "#cad3f51a",
"scrollbar_thumb_hover_background": "#cad3f526",
"scrollbar_thumb_border": "#cad3f500",
"scrollbar_track_background": "#cad3f500",
"scrollbar_track_border": "#cad3f500",
"drop_target_background": "#8aadf41a",
"cursor": "#f4dbd6",
"selection": "#939ab740"
"cursor": "#8aadf4",
"selection": "#8aadf440"
},
"dark": {
"background": "#24273a",
@@ -76,65 +78,67 @@
"elevated_surface_background": "#181926",
"panel_background": "#24273a",
"overlay": "#cad3f51a",
"title_bar": "#1e2030",
"title_bar_inactive": "#181926",
"window_border": "#6e738d",
"border": "#5b6078",
"border_variant": "#494d64",
"title_bar": "#181926",
"title_bar_inactive": "#24273a",
"window_border": "#494d64",
"border": "#494d64",
"border_variant": "#363a4f",
"border_focused": "#8aadf4",
"border_selected": "#8aadf4",
"border_transparent": "#00000000",
"border_disabled": "#363a4f",
"ring": "#8aadf4",
"border_transparent": "#cad3f500",
"border_disabled": "#1e2030",
"ring": "#b7bdf8",
"text": "#cad3f5",
"text_muted": "#b8c0e0",
"text_placeholder": "#a5adcb",
"text_muted": "#a5adcb",
"text_placeholder": "#8087a2",
"text_accent": "#8aadf4",
"icon": "#b8c0e0",
"icon_muted": "#a5adcb",
"icon_accent": "#8aadf4",
"element_foreground": "#181926",
"text_danger": "#ed8796",
"text_warning": "#f5a97f",
"icon": "#a5adcb",
"icon_muted": "#8087a2",
"icon_accent": "#b7bdf8",
"element_foreground": "#24273a",
"element_background": "#8aadf4",
"element_hover": "#b7bdf8",
"element_active": "#7c9cdc",
"element_selected": "#6e8bc5",
"element_active": "#91d7e3",
"element_selected": "#7dc4e4",
"element_disabled": "#8aadf44d",
"secondary_foreground": "#6e8bc5",
"secondary_background": "#1e2030",
"secondary_hover": "#8aadf433",
"secondary_active": "#181926",
"secondary_selected": "#181926",
"secondary_foreground": "#cad3f5",
"secondary_background": "#363a4f",
"secondary_hover": "#494d64",
"secondary_active": "#5b6078",
"secondary_selected": "#5b6078",
"secondary_disabled": "#8aadf44d",
"danger_foreground": "#181926",
"danger_foreground": "#24273a",
"danger_background": "#ed8796",
"danger_hover": "#ee99a0",
"danger_active": "#d57a87",
"danger_selected": "#be6d78",
"danger_active": "#f5a97f",
"danger_selected": "#eed49f",
"danger_disabled": "#ed87964d",
"warning_foreground": "#181926",
"warning_background": "#eed49f",
"warning_hover": "#f5a97f",
"warning_active": "#d6bf8f",
"warning_selected": "#beaa7f",
"warning_disabled": "#eed49f4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#363a4f",
"ghost_element_hover": "#cad3f533",
"ghost_element_active": "#494d64",
"ghost_element_selected": "#494d64",
"ghost_element_disabled": "#cad3f50d",
"tab_inactive_background": "#1e2030",
"tab_inactive_foreground": "#b8c0e0",
"warning_foreground": "#24273a",
"warning_background": "#f5a97f",
"warning_hover": "#eed49f",
"warning_active": "#a6da95",
"warning_selected": "#8bd5ca",
"warning_disabled": "#f5a97f4d",
"ghost_element_background": "#cad3f500",
"ghost_element_background_alt": "#1e2030",
"ghost_element_hover": "#cad3f50d",
"ghost_element_active": "#cad3f51a",
"ghost_element_selected": "#cad3f51a",
"ghost_element_disabled": "#cad3f505",
"tab_background": "#181926",
"tab_foreground": "#a5adcb",
"tab_hover_background": "#cad3f50d",
"tab_active_background": "#24273a",
"tab_active_foreground": "#cad3f5",
"tab_hover_foreground": "#b7bdf8",
"scrollbar_thumb_background": "#cad3f533",
"scrollbar_thumb_hover_background": "#cad3f580",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#494d64",
"scrollbar_thumb_background": "#cad3f51a",
"scrollbar_thumb_hover_background": "#cad3f526",
"scrollbar_thumb_border": "#cad3f500",
"scrollbar_track_background": "#cad3f500",
"scrollbar_track_border": "#cad3f500",
"drop_target_background": "#8aadf41a",
"cursor": "#f4dbd6",
"selection": "#939ab740"
"cursor": "#8aadf4",
"selection": "#8aadf440"
}
}

View File

@@ -1,74 +1,76 @@
{
"id": "catppuccin-mocha",
"name": "Catppuccin Mocha",
"author": "Catppuccin",
"url": "https://github.com/catppuccin/catppuccin",
"author": "Catppuccin Org (ported by Coop)",
"url": "https://catppuccin.com",
"light": {
"background": "#1e1e2e",
"surface_background": "#181825",
"elevated_surface_background": "#11111b",
"panel_background": "#1e1e2e",
"overlay": "#cdd6f41a",
"title_bar": "#181825",
"title_bar_inactive": "#11111b",
"window_border": "#6c7086",
"border": "#585b70",
"border_variant": "#45475a",
"title_bar": "#11111b",
"title_bar_inactive": "#1e1e2e",
"window_border": "#45475a",
"border": "#45475a",
"border_variant": "#313244",
"border_focused": "#89b4fa",
"border_selected": "#89b4fa",
"border_transparent": "#00000000",
"border_disabled": "#313244",
"ring": "#89b4fa",
"border_transparent": "#cdd6f400",
"border_disabled": "#181825",
"ring": "#b4befe",
"text": "#cdd6f4",
"text_muted": "#bac2de",
"text_placeholder": "#a6adc8",
"text_muted": "#a6adc8",
"text_placeholder": "#7f849c",
"text_accent": "#89b4fa",
"icon": "#bac2de",
"icon_muted": "#a6adc8",
"icon_accent": "#89b4fa",
"element_foreground": "#11111b",
"text_danger": "#f38ba8",
"text_warning": "#fab387",
"icon": "#a6adc8",
"icon_muted": "#7f849c",
"icon_accent": "#b4befe",
"element_foreground": "#1e1e2e",
"element_background": "#89b4fa",
"element_hover": "#b4befe",
"element_active": "#7ba2e1",
"element_selected": "#6d90c9",
"element_active": "#89dceb",
"element_selected": "#74c7ec",
"element_disabled": "#89b4fa4d",
"secondary_foreground": "#6d90c9",
"secondary_background": "#181825",
"secondary_hover": "#89b4fa33",
"secondary_active": "#11111b",
"secondary_selected": "#11111b",
"secondary_foreground": "#cdd6f4",
"secondary_background": "#313244",
"secondary_hover": "#45475a",
"secondary_active": "#585b70",
"secondary_selected": "#585b70",
"secondary_disabled": "#89b4fa4d",
"danger_foreground": "#11111b",
"danger_foreground": "#1e1e2e",
"danger_background": "#f38ba8",
"danger_hover": "#eba0ac",
"danger_active": "#db7d98",
"danger_selected": "#c46f88",
"danger_active": "#fab387",
"danger_selected": "#f9e2af",
"danger_disabled": "#f38ba84d",
"warning_foreground": "#11111b",
"warning_background": "#f9e2af",
"warning_hover": "#fab387",
"warning_active": "#e0cb9e",
"warning_selected": "#c8b48d",
"warning_disabled": "#f9e2af4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#313244",
"ghost_element_hover": "#cdd6f433",
"ghost_element_active": "#45475a",
"ghost_element_selected": "#45475a",
"ghost_element_disabled": "#cdd6f40d",
"tab_inactive_background": "#181825",
"tab_inactive_foreground": "#bac2de",
"warning_foreground": "#1e1e2e",
"warning_background": "#fab387",
"warning_hover": "#f9e2af",
"warning_active": "#a6e3a1",
"warning_selected": "#94e2d5",
"warning_disabled": "#fab3874d",
"ghost_element_background": "#cdd6f400",
"ghost_element_background_alt": "#181825",
"ghost_element_hover": "#cdd6f40d",
"ghost_element_active": "#cdd6f41a",
"ghost_element_selected": "#cdd6f41a",
"ghost_element_disabled": "#cdd6f405",
"tab_background": "#11111b",
"tab_foreground": "#a6adc8",
"tab_hover_background": "#cdd6f40d",
"tab_active_background": "#1e1e2e",
"tab_active_foreground": "#cdd6f4",
"tab_hover_foreground": "#b4befe",
"scrollbar_thumb_background": "#cdd6f433",
"scrollbar_thumb_hover_background": "#cdd6f580",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#45475a",
"scrollbar_thumb_background": "#cdd6f41a",
"scrollbar_thumb_hover_background": "#cdd6f426",
"scrollbar_thumb_border": "#cdd6f400",
"scrollbar_track_background": "#cdd6f400",
"scrollbar_track_border": "#cdd6f400",
"drop_target_background": "#89b4fa1a",
"cursor": "#f5e0dc",
"selection": "#9399b240"
"cursor": "#89b4fa",
"selection": "#89b4fa40"
},
"dark": {
"background": "#1e1e2e",
@@ -76,65 +78,67 @@
"elevated_surface_background": "#11111b",
"panel_background": "#1e1e2e",
"overlay": "#cdd6f41a",
"title_bar": "#181825",
"title_bar_inactive": "#11111b",
"window_border": "#6c7086",
"border": "#585b70",
"border_variant": "#45475a",
"title_bar": "#11111b",
"title_bar_inactive": "#1e1e2e",
"window_border": "#45475a",
"border": "#45475a",
"border_variant": "#313244",
"border_focused": "#89b4fa",
"border_selected": "#89b4fa",
"border_transparent": "#00000000",
"border_disabled": "#313244",
"ring": "#89b4fa",
"border_transparent": "#cdd6f400",
"border_disabled": "#181825",
"ring": "#b4befe",
"text": "#cdd6f4",
"text_muted": "#bac2de",
"text_placeholder": "#a6adc8",
"text_muted": "#a6adc8",
"text_placeholder": "#7f849c",
"text_accent": "#89b4fa",
"icon": "#bac2de",
"icon_muted": "#a6adc8",
"icon_accent": "#89b4fa",
"element_foreground": "#11111b",
"text_danger": "#f38ba8",
"text_warning": "#fab387",
"icon": "#a6adc8",
"icon_muted": "#7f849c",
"icon_accent": "#b4befe",
"element_foreground": "#1e1e2e",
"element_background": "#89b4fa",
"element_hover": "#b4befe",
"element_active": "#7ba2e1",
"element_selected": "#6d90c9",
"element_active": "#89dceb",
"element_selected": "#74c7ec",
"element_disabled": "#89b4fa4d",
"secondary_foreground": "#6d90c9",
"secondary_background": "#181825",
"secondary_hover": "#89b4fa33",
"secondary_active": "#11111b",
"secondary_selected": "#11111b",
"secondary_foreground": "#cdd6f4",
"secondary_background": "#313244",
"secondary_hover": "#45475a",
"secondary_active": "#585b70",
"secondary_selected": "#585b70",
"secondary_disabled": "#89b4fa4d",
"danger_foreground": "#11111b",
"danger_foreground": "#1e1e2e",
"danger_background": "#f38ba8",
"danger_hover": "#eba0ac",
"danger_active": "#db7d98",
"danger_selected": "#c46f88",
"danger_active": "#fab387",
"danger_selected": "#f9e2af",
"danger_disabled": "#f38ba84d",
"warning_foreground": "#11111b",
"warning_background": "#f9e2af",
"warning_hover": "#fab387",
"warning_active": "#e0cb9e",
"warning_selected": "#c8b48d",
"warning_disabled": "#f9e2af4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#313244",
"ghost_element_hover": "#cdd6f433",
"ghost_element_active": "#45475a",
"ghost_element_selected": "#45475a",
"ghost_element_disabled": "#cdd6f40d",
"tab_inactive_background": "#181825",
"tab_inactive_foreground": "#bac2de",
"warning_foreground": "#1e1e2e",
"warning_background": "#fab387",
"warning_hover": "#f9e2af",
"warning_active": "#a6e3a1",
"warning_selected": "#94e2d5",
"warning_disabled": "#fab3874d",
"ghost_element_background": "#cdd6f400",
"ghost_element_background_alt": "#181825",
"ghost_element_hover": "#cdd6f40d",
"ghost_element_active": "#cdd6f41a",
"ghost_element_selected": "#cdd6f41a",
"ghost_element_disabled": "#cdd6f405",
"tab_background": "#11111b",
"tab_foreground": "#a6adc8",
"tab_hover_background": "#cdd6f40d",
"tab_active_background": "#1e1e2e",
"tab_active_foreground": "#cdd6f4",
"tab_hover_foreground": "#b4befe",
"scrollbar_thumb_background": "#cdd6f433",
"scrollbar_thumb_hover_background": "#cdd6f580",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#45475a",
"scrollbar_thumb_background": "#cdd6f41a",
"scrollbar_thumb_hover_background": "#cdd6f426",
"scrollbar_thumb_border": "#cdd6f400",
"scrollbar_track_background": "#cdd6f400",
"scrollbar_track_border": "#cdd6f400",
"drop_target_background": "#89b4fa1a",
"cursor": "#f5e0dc",
"selection": "#9399b240"
"cursor": "#89b4fa",
"selection": "#89b4fa40"
}
}

View File

@@ -1,140 +1,144 @@
{
"id": "flexoki",
"name": "Flexoki",
"author": "Stephan Ango",
"author": "Steph Ango (ported by Coop)",
"url": "https://stephango.com/flexoki",
"light": {
"background": "#FFFCF0",
"surface_background": "#F2F0E5",
"elevated_surface_background": "#E6E4D9",
"panel_background": "#FFFCF0",
"overlay": "#100F0F1a",
"title_bar": "#F2F0E5",
"title_bar_inactive": "#E6E4D9",
"window_border": "#B7B5AC",
"overlay": "#100F0F1A",
"title_bar": "#E6E4D9",
"title_bar_inactive": "#FFFCF0",
"window_border": "#CECDC3",
"border": "#CECDC3",
"border_variant": "#DAD8CE",
"border_focused": "#205EA6",
"border_selected": "#205EA6",
"border_transparent": "#00000000",
"border_focused": "#24837B",
"border_selected": "#24837B",
"border_transparent": "#100F0F00",
"border_disabled": "#E6E4D9",
"ring": "#205EA6",
"ring": "#3AA99F",
"text": "#100F0F",
"text_muted": "#6F6E69",
"text_placeholder": "#9F9D96",
"text_accent": "#205EA6",
"text_placeholder": "#B7B5AC",
"text_accent": "#24837B",
"text_danger": "#AF3029",
"text_warning": "#BC5215",
"icon": "#6F6E69",
"icon_muted": "#9F9D96",
"icon_accent": "#205EA6",
"icon_muted": "#B7B5AC",
"icon_accent": "#3AA99F",
"element_foreground": "#FFFCF0",
"element_background": "#205EA6",
"element_hover": "#1A4F8C",
"element_active": "#163B66",
"element_selected": "#133051",
"element_disabled": "#205EA64d",
"secondary_foreground": "#163B66",
"secondary_background": "#F2F0E5",
"secondary_hover": "#205EA61a",
"secondary_active": "#E6E4D9",
"secondary_selected": "#E6E4D9",
"secondary_disabled": "#205EA64d",
"element_background": "#24837B",
"element_hover": "#3AA99F",
"element_active": "#1C1B1A",
"element_selected": "#100F0F",
"element_disabled": "#24837B4D",
"secondary_foreground": "#100F0F",
"secondary_background": "#E6E4D9",
"secondary_hover": "#DAD8CE",
"secondary_active": "#CECDC3",
"secondary_selected": "#CECDC3",
"secondary_disabled": "#24837B4D",
"danger_foreground": "#FFFCF0",
"danger_background": "#D14D41",
"danger_hover": "#C03E35",
"danger_active": "#AF3029",
"danger_selected": "#942822",
"danger_disabled": "#D14D414d",
"warning_foreground": "#100F0F",
"warning_background": "#D0A215",
"warning_hover": "#BE9207",
"warning_active": "#AD8301",
"warning_selected": "#8E6B01",
"warning_disabled": "#D0A2154d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#E6E4D9",
"ghost_element_hover": "#100F0F1a",
"ghost_element_active": "#DAD8CE",
"ghost_element_selected": "#DAD8CE",
"ghost_element_disabled": "#100F0F0d",
"tab_inactive_background": "#F2F0E5",
"tab_inactive_foreground": "#6F6E69",
"danger_background": "#AF3029",
"danger_hover": "#D14D41",
"danger_active": "#1C1B1A",
"danger_selected": "#100F0F",
"danger_disabled": "#AF30294D",
"warning_foreground": "#FFFCF0",
"warning_background": "#BC5215",
"warning_hover": "#DA702C",
"warning_active": "#1C1B1A",
"warning_selected": "#100F0F",
"warning_disabled": "#BC52154D",
"ghost_element_background": "#100F0F00",
"ghost_element_background_alt": "#F2F0E5",
"ghost_element_hover": "#100F0F0D",
"ghost_element_active": "#100F0F1A",
"ghost_element_selected": "#100F0F1A",
"ghost_element_disabled": "#100F0F05",
"tab_background": "#E6E4D9",
"tab_foreground": "#6F6E69",
"tab_hover_background": "#100F0F0D",
"tab_active_background": "#FFFCF0",
"tab_active_foreground": "#100F0F",
"tab_hover_foreground": "#205EA6",
"scrollbar_thumb_background": "#100F0F33",
"scrollbar_thumb_hover_background": "#100F0F4d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#DAD8CE",
"drop_target_background": "#205EA61a",
"cursor": "#205EA6",
"selection": "#205EA640"
"scrollbar_thumb_background": "#100F0F1A",
"scrollbar_thumb_hover_background": "#100F0F26",
"scrollbar_thumb_border": "#100F0F00",
"scrollbar_track_background": "#100F0F00",
"scrollbar_track_border": "#100F0F00",
"drop_target_background": "#24837B1A",
"cursor": "#24837B",
"selection": "#24837B40"
},
"dark": {
"background": "#100F0F",
"surface_background": "#1C1B1A",
"elevated_surface_background": "#282726",
"panel_background": "#100F0F",
"overlay": "#FFFCF01a",
"title_bar": "#1C1B1A",
"title_bar_inactive": "#282726",
"window_border": "#575653",
"overlay": "#FFFCF01A",
"title_bar": "#282726",
"title_bar_inactive": "#100F0F",
"window_border": "#403E3C",
"border": "#403E3C",
"border_variant": "#343331",
"border_focused": "#4385BE",
"border_selected": "#4385BE",
"border_transparent": "#00000000",
"border_focused": "#3AA99F",
"border_selected": "#3AA99F",
"border_transparent": "#100F0F00",
"border_disabled": "#282726",
"ring": "#4385BE",
"text": "#FFFCF0",
"ring": "#24837B",
"text": "#CECDC3",
"text_muted": "#878580",
"text_placeholder": "#6F6E69",
"text_accent": "#4385BE",
"text_placeholder": "#575653",
"text_accent": "#3AA99F",
"text_danger": "#D14D41",
"text_warning": "#DA702C",
"icon": "#878580",
"icon_muted": "#6F6E69",
"icon_accent": "#4385BE",
"icon_muted": "#575653",
"icon_accent": "#24837B",
"element_foreground": "#100F0F",
"element_background": "#4385BE",
"element_hover": "#3171B2",
"element_active": "#205EA6",
"element_selected": "#1A4F8C",
"element_disabled": "#4385BE4d",
"secondary_foreground": "#205EA6",
"element_background": "#3AA99F",
"element_hover": "#24837B",
"element_active": "#CECDC3",
"element_selected": "#F2F0E5",
"element_disabled": "#3AA99F4D",
"secondary_foreground": "#CECDC3",
"secondary_background": "#1C1B1A",
"secondary_hover": "#4385BE1a",
"secondary_active": "#282726",
"secondary_selected": "#282726",
"secondary_disabled": "#4385BE4d",
"secondary_hover": "#282726",
"secondary_active": "#343331",
"secondary_selected": "#343331",
"secondary_disabled": "#3AA99F4D",
"danger_foreground": "#100F0F",
"danger_background": "#E8705F",
"danger_hover": "#D14D41",
"danger_active": "#C03E35",
"danger_selected": "#AF3029",
"danger_disabled": "#E8705F4d",
"danger_background": "#D14D41",
"danger_hover": "#AF3029",
"danger_active": "#CECDC3",
"danger_selected": "#F2F0E5",
"danger_disabled": "#D14D414D",
"warning_foreground": "#100F0F",
"warning_background": "#DFB431",
"warning_hover": "#D0A215",
"warning_active": "#BE9207",
"warning_selected": "#AD8301",
"warning_disabled": "#DFB4314d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#282726",
"ghost_element_hover": "#FFFCF01a",
"ghost_element_active": "#343331",
"ghost_element_selected": "#343331",
"ghost_element_disabled": "#FFFCF00d",
"tab_inactive_background": "#1C1B1A",
"tab_inactive_foreground": "#878580",
"warning_background": "#DA702C",
"warning_hover": "#BC5215",
"warning_active": "#CECDC3",
"warning_selected": "#F2F0E5",
"warning_disabled": "#DA702C4D",
"ghost_element_background": "#100F0F00",
"ghost_element_background_alt": "#1C1B1A",
"ghost_element_hover": "#FFFCF00D",
"ghost_element_active": "#FFFCF01A",
"ghost_element_selected": "#FFFCF01A",
"ghost_element_disabled": "#FFFCF005",
"tab_background": "#282726",
"tab_foreground": "#878580",
"tab_hover_background": "#FFFCF00D",
"tab_active_background": "#100F0F",
"tab_active_foreground": "#FFFCF0",
"tab_hover_foreground": "#4385BE",
"scrollbar_thumb_background": "#FFFCF033",
"scrollbar_thumb_hover_background": "#FFFCF04d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#343331",
"drop_target_background": "#4385BE1a",
"cursor": "#4385BE",
"selection": "#4385BE40"
"tab_active_foreground": "#CECDC3",
"scrollbar_thumb_background": "#FFFCF01A",
"scrollbar_thumb_hover_background": "#FFFCF026",
"scrollbar_thumb_border": "#100F0F00",
"scrollbar_track_background": "#100F0F00",
"scrollbar_track_border": "#100F0F00",
"drop_target_background": "#3AA99F1A",
"cursor": "#3AA99F",
"selection": "#3AA99F40"
}
}

144
assets/themes/forest.json Normal file
View File

@@ -0,0 +1,144 @@
{
"id": "forest",
"name": "Forest",
"author": "Coop",
"url": "https://github.com/lumehq/coop",
"light": {
"background": "#fbfefcff",
"surface_background": "#f4fbf6ff",
"elevated_surface_background": "#e9f6e9ff",
"panel_background": "#fbfefcff",
"overlay": "#193b2d1a",
"title_bar": "#e9f6e9ff",
"title_bar_inactive": "#fbfefcff",
"window_border": "#c4e8d1ff",
"border": "#c4e8d1ff",
"border_variant": "#b2ddb5ff",
"border_focused": "#30a46cff",
"border_selected": "#30a46cff",
"border_transparent": "#00000000",
"border_disabled": "#e0f3e6ff",
"ring": "#2b9a66ff",
"text": "#193b2dff",
"text_muted": "#2f7c57ff",
"text_placeholder": "#8eceaaff",
"text_accent": "#30a46cff",
"text_danger": "#e54d2eff",
"text_warning": "#f76b15ff",
"icon": "#2f7c57ff",
"icon_muted": "#8eceaaff",
"icon_accent": "#2b9a66ff",
"element_foreground": "#ffffffff",
"element_background": "#30a46cff",
"element_hover": "#2b9a66ff",
"element_active": "#2a7e3bff",
"element_selected": "#218358ff",
"element_disabled": "#30a46c4d",
"secondary_foreground": "#193b2dff",
"secondary_background": "#e9f6e9ff",
"secondary_hover": "#daf1dbff",
"secondary_active": "#c4e8d1ff",
"secondary_selected": "#c4e8d1ff",
"secondary_disabled": "#30a46c4d",
"danger_foreground": "#ffffffff",
"danger_background": "#feebe7ff",
"danger_hover": "#ffcdc2ff",
"danger_active": "#fdbdafff",
"danger_selected": "#fdbdafff",
"danger_disabled": "#e54d2e4d",
"warning_foreground": "#ffffffff",
"warning_background": "#fff7edff",
"warning_hover": "#ffd19aff",
"warning_active": "#ffc182ff",
"warning_selected": "#ffc182ff",
"warning_disabled": "#f76b154d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#e9f6e9ff",
"ghost_element_hover": "#193b2d0d",
"ghost_element_active": "#193b2d1a",
"ghost_element_selected": "#193b2d1a",
"ghost_element_disabled": "#193b2d05",
"tab_background": "#e9f6e9ff",
"tab_foreground": "#2f7c57ff",
"tab_hover_background": "#193b2d0d",
"tab_active_background": "#fbfefcff",
"tab_active_foreground": "#193b2dff",
"scrollbar_thumb_background": "#193b2d1a",
"scrollbar_thumb_hover_background": "#193b2d26",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#00000000",
"drop_target_background": "#30a46c1a",
"cursor": "#30a46cff",
"selection": "#30a46c40"
},
"dark": {
"background": "#0e1512ff",
"surface_background": "#121b17ff",
"elevated_surface_background": "#132d21ff",
"panel_background": "#0e1512ff",
"overlay": "#b1f1cb1a",
"title_bar": "#132d21ff",
"title_bar_inactive": "#0e1512ff",
"window_border": "#20573eff",
"border": "#20573eff",
"border_variant": "#174933ff",
"border_focused": "#33b074ff",
"border_selected": "#33b074ff",
"border_transparent": "#00000000",
"border_disabled": "#113b29ff",
"ring": "#33b074ff",
"text": "#b1f1cbff",
"text_muted": "#71d083ff",
"text_placeholder": "#2f7c57ff",
"text_accent": "#3dd68cff",
"text_danger": "#ff977dff",
"text_warning": "#ffa057ff",
"icon": "#71d083ff",
"icon_muted": "#2f7c57ff",
"icon_accent": "#33b074ff",
"element_foreground": "#0e1512ff",
"element_background": "#3dd68cff",
"element_hover": "#71d083ff",
"element_active": "#33b074ff",
"element_selected": "#30a46cff",
"element_disabled": "#3dd68c4d",
"secondary_foreground": "#b1f1cbff",
"secondary_background": "#132d21ff",
"secondary_hover": "#113b29ff",
"secondary_active": "#174933ff",
"secondary_selected": "#174933ff",
"secondary_disabled": "#3dd68c4d",
"danger_foreground": "#181111ff",
"danger_background": "#391714ff",
"danger_hover": "#5e1c16ff",
"danger_active": "#6e2920ff",
"danger_selected": "#6e2920ff",
"danger_disabled": "#ff977d4d",
"warning_foreground": "#17120eff",
"warning_background": "#331e0bff",
"warning_hover": "#562800ff",
"warning_active": "#66350cff",
"warning_selected": "#66350cff",
"warning_disabled": "#ffa0574d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#132d21ff",
"ghost_element_hover": "#b1f1cb0d",
"ghost_element_active": "#b1f1cb1a",
"ghost_element_selected": "#b1f1cb1a",
"ghost_element_disabled": "#b1f1cb05",
"tab_background": "#132d21ff",
"tab_foreground": "#71d083ff",
"tab_hover_background": "#b1f1cb0d",
"tab_active_background": "#0e1512ff",
"tab_active_foreground": "#b1f1cbff",
"scrollbar_thumb_background": "#b1f1cb1a",
"scrollbar_thumb_hover_background": "#b1f1cb26",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#00000000",
"drop_target_background": "#3dd68c1a",
"cursor": "#3dd68cff",
"selection": "#3dd68c40"
}
}

144
assets/themes/ocean.json Normal file
View File

@@ -0,0 +1,144 @@
{
"id": "ocean",
"name": "Ocean",
"author": "Coop",
"url": "https://github.com/lumehq/coop",
"light": {
"background": "#fafefeff",
"surface_background": "#f2fbfaff",
"elevated_surface_background": "#e6f7f7ff",
"panel_background": "#fafefeff",
"overlay": "#00333f1a",
"title_bar": "#e6f7f7ff",
"title_bar_inactive": "#fafefeff",
"window_border": "#cce5e9ff",
"border": "#cce5e9ff",
"border_variant": "#b8dde3ff",
"border_focused": "#00a2c7ff",
"border_selected": "#00a2c7ff",
"border_transparent": "#00000000",
"border_disabled": "#e0f0f2ff",
"ring": "#0797b9ff",
"text": "#0d3c48ff",
"text_muted": "#107d98ff",
"text_placeholder": "#60b3d7ff",
"text_accent": "#00a2c7ff",
"text_danger": "#e54d2eff",
"text_warning": "#f76b15ff",
"icon": "#107d98ff",
"icon_muted": "#60b3d7ff",
"icon_accent": "#0797b9ff",
"element_foreground": "#ffffffff",
"element_background": "#00a2c7ff",
"element_hover": "#0797b9ff",
"element_active": "#12667eff",
"element_selected": "#0d4a5cff",
"element_disabled": "#00a2c74d",
"secondary_foreground": "#0d4a5cff",
"secondary_background": "#ddf9f2ff",
"secondary_hover": "#c8f4e9ff",
"secondary_active": "#b3ecdeff",
"secondary_selected": "#b3ecdeff",
"secondary_disabled": "#00a2c74d",
"danger_foreground": "#ffffffff",
"danger_background": "#feebe7ff",
"danger_hover": "#ffcdc2ff",
"danger_active": "#fdbdafff",
"danger_selected": "#fdbdafff",
"danger_disabled": "#e54d2e4d",
"warning_foreground": "#ffffffff",
"warning_background": "#fff7edff",
"warning_hover": "#ffd19aff",
"warning_active": "#ffc182ff",
"warning_selected": "#ffc182ff",
"warning_disabled": "#f76b154d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#e6f7f7ff",
"ghost_element_hover": "#00333f0d",
"ghost_element_active": "#00333f1a",
"ghost_element_selected": "#00333f1a",
"ghost_element_disabled": "#00333f05",
"tab_background": "#e6f7f7ff",
"tab_foreground": "#107d98ff",
"tab_hover_background": "#00333f0d",
"tab_active_background": "#fafefeff",
"tab_active_foreground": "#0d3c48ff",
"scrollbar_thumb_background": "#00333f1a",
"scrollbar_thumb_hover_background": "#00333f26",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#00000000",
"drop_target_background": "#00a2c71a",
"cursor": "#00a2c7ff",
"selection": "#00a2c740"
},
"dark": {
"background": "#0b161aff",
"surface_background": "#101b20ff",
"elevated_surface_background": "#082c36ff",
"panel_background": "#0b161aff",
"overlay": "#c2f3ff1a",
"title_bar": "#082c36ff",
"title_bar_inactive": "#0b161aff",
"window_border": "#1b537bff",
"border": "#1b537bff",
"border_variant": "#154467ff",
"border_focused": "#00a2c7ff",
"border_selected": "#00a2c7ff",
"border_transparent": "#00000000",
"border_disabled": "#112840ff",
"ring": "#23afd0ff",
"text": "#b6ecf7ff",
"text_muted": "#4ccce6ff",
"text_placeholder": "#197caeff",
"text_accent": "#7ce2feff",
"text_danger": "#ff977dff",
"text_warning": "#ffa057ff",
"icon": "#4ccce6ff",
"icon_muted": "#197caeff",
"icon_accent": "#23afd0ff",
"element_foreground": "#0b161aff",
"element_background": "#7ce2feff",
"element_hover": "#a8eeffff",
"element_active": "#23afd0ff",
"element_selected": "#00a2c7ff",
"element_disabled": "#7ce2fe4d",
"secondary_foreground": "#adf0ddff",
"secondary_background": "#0d2d2aff",
"secondary_hover": "#023b37ff",
"secondary_active": "#084843ff",
"secondary_selected": "#084843ff",
"secondary_disabled": "#7ce2fe4d",
"danger_foreground": "#181111ff",
"danger_background": "#391714ff",
"danger_hover": "#5e1c16ff",
"danger_active": "#6e2920ff",
"danger_selected": "#6e2920ff",
"danger_disabled": "#ff977d4d",
"warning_foreground": "#17120eff",
"warning_background": "#331e0bff",
"warning_hover": "#562800ff",
"warning_active": "#66350cff",
"warning_selected": "#66350cff",
"warning_disabled": "#ffa0574d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#082c36ff",
"ghost_element_hover": "#c2f3ff0d",
"ghost_element_active": "#c2f3ff1a",
"ghost_element_selected": "#c2f3ff1a",
"ghost_element_disabled": "#c2f3ff05",
"tab_background": "#082c36ff",
"tab_foreground": "#4ccce6ff",
"tab_hover_background": "#c2f3ff0d",
"tab_active_background": "#0b161aff",
"tab_active_foreground": "#b6ecf7ff",
"scrollbar_thumb_background": "#c2f3ff1a",
"scrollbar_thumb_hover_background": "#c2f3ff26",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#00000000",
"drop_target_background": "#7ce2fe1a",
"cursor": "#7ce2feff",
"selection": "#7ce2fe40"
}
}

View File

@@ -1,140 +0,0 @@
{
"id": "rose-pine-dawn",
"name": "Rosé Pine Dawn",
"author": "Rosé Pine",
"url": "https://rosepinetheme.com/",
"light": {
"background": "#faf4ed",
"surface_background": "#fffaf3",
"elevated_surface_background": "#f2e9e1",
"panel_background": "#faf4ed",
"overlay": "#5752791a",
"title_bar": "#fffaf3",
"title_bar_inactive": "#f2e9e1",
"window_border": "#cecacd",
"border": "#dfdad9",
"border_variant": "#f4ede8",
"border_focused": "#907aa9",
"border_selected": "#907aa9",
"border_transparent": "#00000000",
"border_disabled": "#f2e9e1",
"ring": "#907aa9",
"text": "#575279",
"text_muted": "#797593",
"text_placeholder": "#9893a5",
"text_accent": "#907aa9",
"icon": "#797593",
"icon_muted": "#9893a5",
"icon_accent": "#907aa9",
"element_foreground": "#faf4ed",
"element_background": "#907aa9",
"element_hover": "#907aa9e6",
"element_active": "#826b95",
"element_selected": "#745c81",
"element_disabled": "#907aa94d",
"secondary_foreground": "#745c81",
"secondary_background": "#fffaf3",
"secondary_hover": "#907aa91a",
"secondary_active": "#f2e9e1",
"secondary_selected": "#f2e9e1",
"secondary_disabled": "#907aa94d",
"danger_foreground": "#faf4ed",
"danger_background": "#b4637a",
"danger_hover": "#a7586e",
"danger_active": "#9a4d62",
"danger_selected": "#8d4256",
"danger_disabled": "#b4637a4d",
"warning_foreground": "#575279",
"warning_background": "#ea9d34",
"warning_hover": "#d98e2f",
"warning_active": "#c87f2a",
"warning_selected": "#b77025",
"warning_disabled": "#ea9d344d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#f2e9e1",
"ghost_element_hover": "#5752791a",
"ghost_element_active": "#dfdad9",
"ghost_element_selected": "#dfdad9",
"ghost_element_disabled": "#5752790d",
"tab_inactive_background": "#fffaf3",
"tab_inactive_foreground": "#797593",
"tab_active_background": "#faf4ed",
"tab_active_foreground": "#575279",
"tab_hover_foreground": "#907aa9",
"scrollbar_thumb_background": "#57527933",
"scrollbar_thumb_hover_background": "#5752794d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#dfdad9",
"drop_target_background": "#907aa91a",
"cursor": "#907aa9",
"selection": "#907aa940"
},
"dark": {
"background": "#faf4ed",
"surface_background": "#fffaf3",
"elevated_surface_background": "#f2e9e1",
"panel_background": "#faf4ed",
"overlay": "#5752791a",
"title_bar": "#fffaf3",
"title_bar_inactive": "#f2e9e1",
"window_border": "#cecacd",
"border": "#dfdad9",
"border_variant": "#f4ede8",
"border_focused": "#907aa9",
"border_selected": "#907aa9",
"border_transparent": "#00000000",
"border_disabled": "#f2e9e1",
"ring": "#907aa9",
"text": "#575279",
"text_muted": "#797593",
"text_placeholder": "#9893a5",
"text_accent": "#907aa9",
"icon": "#797593",
"icon_muted": "#9893a5",
"icon_accent": "#907aa9",
"element_foreground": "#faf4ed",
"element_background": "#907aa9",
"element_hover": "#907aa9e6",
"element_active": "#826b95",
"element_selected": "#745c81",
"element_disabled": "#907aa94d",
"secondary_foreground": "#745c81",
"secondary_background": "#fffaf3",
"secondary_hover": "#907aa91a",
"secondary_active": "#f2e9e1",
"secondary_selected": "#f2e9e1",
"secondary_disabled": "#907aa94d",
"danger_foreground": "#faf4ed",
"danger_background": "#b4637a",
"danger_hover": "#a7586e",
"danger_active": "#9a4d62",
"danger_selected": "#8d4256",
"danger_disabled": "#b4637a4d",
"warning_foreground": "#575279",
"warning_background": "#ea9d34",
"warning_hover": "#d98e2f",
"warning_active": "#c87f2a",
"warning_selected": "#b77025",
"warning_disabled": "#ea9d344d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#f2e9e1",
"ghost_element_hover": "#5752791a",
"ghost_element_active": "#dfdad9",
"ghost_element_selected": "#dfdad9",
"ghost_element_disabled": "#5752790d",
"tab_inactive_background": "#fffaf3",
"tab_inactive_foreground": "#797593",
"tab_active_background": "#faf4ed",
"tab_active_foreground": "#575279",
"tab_hover_foreground": "#907aa9",
"scrollbar_thumb_background": "#57527933",
"scrollbar_thumb_hover_background": "#5752794d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#dfdad9",
"drop_target_background": "#907aa91a",
"cursor": "#907aa9",
"selection": "#907aa940"
}
}

View File

@@ -1,140 +0,0 @@
{
"id": "rose-pine-moon",
"name": "Rosé Pine Moon",
"author": "Rosé Pine",
"url": "https://rosepinetheme.com/",
"light": {
"background": "#232136",
"surface_background": "#2a273f",
"elevated_surface_background": "#393552",
"panel_background": "#232136",
"overlay": "#e0def41a",
"title_bar": "#2a273f",
"title_bar_inactive": "#393552",
"window_border": "#56526e",
"border": "#44415a",
"border_variant": "#393552",
"border_focused": "#c4a7e7",
"border_selected": "#c4a7e7",
"border_transparent": "#00000000",
"border_disabled": "#393552",
"ring": "#c4a7e7",
"text": "#e0def4",
"text_muted": "#908caa",
"text_placeholder": "#6e6a86",
"text_accent": "#c4a7e7",
"icon": "#908caa",
"icon_muted": "#6e6a86",
"icon_accent": "#c4a7e7",
"element_foreground": "#232136",
"element_background": "#c4a7e7",
"element_hover": "#c4a7e7e6",
"element_active": "#b296d6",
"element_selected": "#a085c5",
"element_disabled": "#c4a7e74d",
"secondary_foreground": "#a085c5",
"secondary_background": "#393552",
"secondary_hover": "#c4a7e71a",
"secondary_active": "#44415a",
"secondary_selected": "#44415a",
"secondary_disabled": "#c4a7e74d",
"danger_foreground": "#232136",
"danger_background": "#eb6f92",
"danger_hover": "#e55a82",
"danger_active": "#df4572",
"danger_selected": "#d93062",
"danger_disabled": "#eb6f924d",
"warning_foreground": "#232136",
"warning_background": "#f6c177",
"warning_hover": "#f4b35e",
"warning_active": "#f2a545",
"warning_selected": "#f0972c",
"warning_disabled": "#f6c1774d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#393552",
"ghost_element_hover": "#e0def41a",
"ghost_element_active": "#44415a",
"ghost_element_selected": "#44415a",
"ghost_element_disabled": "#e0def40d",
"tab_inactive_background": "#2a273f",
"tab_inactive_foreground": "#908caa",
"tab_active_background": "#232136",
"tab_active_foreground": "#e0def4",
"tab_hover_foreground": "#c4a7e7",
"scrollbar_thumb_background": "#e0def433",
"scrollbar_thumb_hover_background": "#e0def44d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#44415a",
"drop_target_background": "#c4a7e71a",
"cursor": "#c4a7e7",
"selection": "#c4a7e740"
},
"dark": {
"background": "#232136",
"surface_background": "#2a273f",
"elevated_surface_background": "#393552",
"panel_background": "#232136",
"overlay": "#e0def41a",
"title_bar": "#2a273f",
"title_bar_inactive": "#393552",
"window_border": "#56526e",
"border": "#44415a",
"border_variant": "#393552",
"border_focused": "#c4a7e7",
"border_selected": "#c4a7e7",
"border_transparent": "#00000000",
"border_disabled": "#393552",
"ring": "#c4a7e7",
"text": "#e0def4",
"text_muted": "#908caa",
"text_placeholder": "#6e6a86",
"text_accent": "#c4a7e7",
"icon": "#908caa",
"icon_muted": "#6e6a86",
"icon_accent": "#c4a7e7",
"element_foreground": "#232136",
"element_background": "#c4a7e7",
"element_hover": "#c4a7e7e6",
"element_active": "#b296d6",
"element_selected": "#a085c5",
"element_disabled": "#c4a7e74d",
"secondary_foreground": "#a085c5",
"secondary_background": "#393552",
"secondary_hover": "#c4a7e71a",
"secondary_active": "#44415a",
"secondary_selected": "#44415a",
"secondary_disabled": "#c4a7e74d",
"danger_foreground": "#232136",
"danger_background": "#eb6f92",
"danger_hover": "#e55a82",
"danger_active": "#df4572",
"danger_selected": "#d93062",
"danger_disabled": "#eb6f924d",
"warning_foreground": "#232136",
"warning_background": "#f6c177",
"warning_hover": "#f4b35e",
"warning_active": "#f2a545",
"warning_selected": "#f0972c",
"warning_disabled": "#f6c1774d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#393552",
"ghost_element_hover": "#e0def41a",
"ghost_element_active": "#44415a",
"ghost_element_selected": "#44415a",
"ghost_element_disabled": "#e0def40d",
"tab_inactive_background": "#2a273f",
"tab_inactive_foreground": "#908caa",
"tab_active_background": "#232136",
"tab_active_foreground": "#e0def4",
"tab_hover_foreground": "#c4a7e7",
"scrollbar_thumb_background": "#e0def433",
"scrollbar_thumb_hover_background": "#e0def44d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#44415a",
"drop_target_background": "#c4a7e71a",
"cursor": "#c4a7e7",
"selection": "#c4a7e740"
}
}

View File

@@ -1,140 +0,0 @@
{
"id": "rose-pine",
"name": "Rosé Pine",
"author": "Rosé Pine",
"url": "https://rosepinetheme.com/",
"light": {
"background": "#191724",
"surface_background": "#1f1d2e",
"elevated_surface_background": "#26233a",
"panel_background": "#191724",
"overlay": "#e0def41a",
"title_bar": "#1f1d2e",
"title_bar_inactive": "#26233a",
"window_border": "#524f67",
"border": "#403d52",
"border_variant": "#26233a",
"border_focused": "#c4a7e7",
"border_selected": "#c4a7e7",
"border_transparent": "#00000000",
"border_disabled": "#26233a",
"ring": "#c4a7e7",
"text": "#e0def4",
"text_muted": "#908caa",
"text_placeholder": "#6e6a86",
"text_accent": "#c4a7e7",
"icon": "#908caa",
"icon_muted": "#6e6a86",
"icon_accent": "#c4a7e7",
"element_foreground": "#191724",
"element_background": "#c4a7e7",
"element_hover": "#c4a7e7e6",
"element_active": "#b296d6",
"element_selected": "#a085c5",
"element_disabled": "#c4a7e74d",
"secondary_foreground": "#a085c5",
"secondary_background": "#26233a",
"secondary_hover": "#c4a7e71a",
"secondary_active": "#403d52",
"secondary_selected": "#403d52",
"secondary_disabled": "#c4a7e74d",
"danger_foreground": "#191724",
"danger_background": "#eb6f92",
"danger_hover": "#e55a82",
"danger_active": "#df4572",
"danger_selected": "#d93062",
"danger_disabled": "#eb6f924d",
"warning_foreground": "#191724",
"warning_background": "#f6c177",
"warning_hover": "#f4b35e",
"warning_active": "#f2a545",
"warning_selected": "#f0972c",
"warning_disabled": "#f6c1774d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#26233a",
"ghost_element_hover": "#e0def41a",
"ghost_element_active": "#403d52",
"ghost_element_selected": "#403d52",
"ghost_element_disabled": "#e0def40d",
"tab_inactive_background": "#1f1d2e",
"tab_inactive_foreground": "#908caa",
"tab_active_background": "#191724",
"tab_active_foreground": "#e0def4",
"tab_hover_foreground": "#c4a7e7",
"scrollbar_thumb_background": "#e0def433",
"scrollbar_thumb_hover_background": "#e0def44d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#403d52",
"drop_target_background": "#c4a7e71a",
"cursor": "#c4a7e7",
"selection": "#c4a7e740"
},
"dark": {
"background": "#191724",
"surface_background": "#1f1d2e",
"elevated_surface_background": "#26233a",
"panel_background": "#191724",
"overlay": "#e0def41a",
"title_bar": "#1f1d2e",
"title_bar_inactive": "#26233a",
"window_border": "#524f67",
"border": "#403d52",
"border_variant": "#26233a",
"border_focused": "#c4a7e7",
"border_selected": "#c4a7e7",
"border_transparent": "#00000000",
"border_disabled": "#26233a",
"ring": "#c4a7e7",
"text": "#e0def4",
"text_muted": "#908caa",
"text_placeholder": "#6e6a86",
"text_accent": "#c4a7e7",
"icon": "#908caa",
"icon_muted": "#6e6a86",
"icon_accent": "#c4a7e7",
"element_foreground": "#191724",
"element_background": "#c4a7e7",
"element_hover": "#c4a7e7e6",
"element_active": "#b296d6",
"element_selected": "#a085c5",
"element_disabled": "#c4a7e74d",
"secondary_foreground": "#a085c5",
"secondary_background": "#26233a",
"secondary_hover": "#c4a7e71a",
"secondary_active": "#403d52",
"secondary_selected": "#403d52",
"secondary_disabled": "#c4a7e74d",
"danger_foreground": "#191724",
"danger_background": "#eb6f92",
"danger_hover": "#e55a82",
"danger_active": "#df4572",
"danger_selected": "#d93062",
"danger_disabled": "#eb6f924d",
"warning_foreground": "#191724",
"warning_background": "#f6c177",
"warning_hover": "#f4b35e",
"warning_active": "#f2a545",
"warning_selected": "#f0972c",
"warning_disabled": "#f6c1774d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#26233a",
"ghost_element_hover": "#e0def41a",
"ghost_element_active": "#403d52",
"ghost_element_selected": "#403d52",
"ghost_element_disabled": "#e0def40d",
"tab_inactive_background": "#1f1d2e",
"tab_inactive_foreground": "#908caa",
"tab_active_background": "#191724",
"tab_active_foreground": "#e0def4",
"tab_hover_foreground": "#c4a7e7",
"scrollbar_thumb_background": "#e0def433",
"scrollbar_thumb_hover_background": "#e0def44d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#403d52",
"drop_target_background": "#c4a7e71a",
"cursor": "#c4a7e7",
"selection": "#c4a7e740"
}
}

View File

@@ -10,11 +10,12 @@ use common::EventUtils;
use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
use gpui::{
App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window,
App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task,
WeakEntity, Window,
};
use nostr_sdk::prelude::*;
use smallvec::{SmallVec, smallvec};
use state::{DEVICE_GIFTWRAP, NostrRegistry, RelayState, TIMEOUT, USER_GIFTWRAP};
use state::{DEVICE_GIFTWRAP, NostrRegistry, StateEvent, TIMEOUT, USER_GIFTWRAP};
mod message;
mod room;
@@ -39,6 +40,10 @@ pub enum ChatEvent {
CloseRoom(u64),
/// An event to notify UI about a new chat request
Ping,
/// An event to notify UI that the chat registry has subscribed to messaging relays
Subscribed,
/// An error occurred
Error(SharedString),
}
/// Channel signal.
@@ -48,41 +53,25 @@ enum Signal {
Message(NewMessage),
/// Eose received from relay pool
Eose,
}
/// Inbox state.
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum InboxState {
#[default]
Idle,
Checking,
RelayNotAvailable,
RelayConfigured(Box<Event>),
Subscribing,
}
impl InboxState {
pub fn not_configured(&self) -> bool {
matches!(self, InboxState::RelayNotAvailable)
}
pub fn subscribing(&self) -> bool {
matches!(self, InboxState::Subscribing)
}
/// An error occurred
Error(SharedString),
}
/// Chat Registry
#[derive(Debug)]
pub struct ChatRegistry {
/// Relay state for messaging relay list
state: Entity<InboxState>,
/// Collection of all chat rooms
rooms: Vec<Entity<Room>>,
/// Tracking the status of unwrapping gift wrap events.
tracking_flag: Arc<AtomicBool>,
/// Channel for sending signals to the UI.
signal_tx: flume::Sender<Signal>,
/// Channel for receiving signals from the UI.
signal_rx: flume::Receiver<Signal>,
/// Async tasks
tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
@@ -105,36 +94,18 @@ impl ChatRegistry {
/// Create a new chat registry instance
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let state = cx.new(|_| InboxState::default());
let nostr = NostrRegistry::global(cx);
let (tx, rx) = flume::unbounded::<Signal>();
let mut subscriptions = smallvec![];
subscriptions.push(
// Observe the nip65 state and load chat rooms on every state change
cx.observe(&nostr, |this, state, cx| {
match state.read(cx).relay_list_state {
RelayState::Idle => {
// Subscribe to the signer event
cx.subscribe(&nostr, |this, _state, event, cx| {
if let StateEvent::SignerSet = event {
this.reset(cx);
}
RelayState::Configured => {
this.get_contact_list(cx);
this.ensure_messaging_relays(cx);
}
_ => {}
}
// Load rooms on every state change
this.get_rooms(cx);
}),
);
subscriptions.push(
// Observe the nip17 state and load chat rooms on every state change
cx.observe(&state, |this, state, cx| {
if let InboxState::RelayConfigured(event) = state.read(cx) {
let relay_urls: Vec<_> = nip17::extract_relay_list(event).cloned().collect();
this.get_messages(relay_urls, cx);
this.get_contact_list(cx);
this.get_messages(cx)
}
}),
);
@@ -147,9 +118,10 @@ impl ChatRegistry {
});
Self {
state,
rooms: vec![],
tracking_flag: Arc::new(AtomicBool::new(false)),
signal_rx: rx,
signal_tx: tx,
tasks: smallvec![],
_subscriptions: subscriptions,
}
@@ -167,7 +139,8 @@ impl ChatRegistry {
let sub_id2 = SubscriptionId::new(USER_GIFTWRAP);
// Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Signal>(1024);
let tx = self.signal_tx.clone();
let rx = self.signal_rx.clone();
self.tasks.push(cx.background_spawn(async move {
let device_signer = signer.get_encryption_signer().await;
@@ -194,7 +167,14 @@ impl ChatRegistry {
// Extract the rumor from the gift wrap event
match extract_rumor(&client, &device_signer, event.as_ref()).await {
Ok(rumor) => match rumor.created_at >= initialized_at {
Ok(rumor) => {
if rumor.tags.is_empty() {
let error: SharedString =
"Message doesn't belong to any rooms".into();
tx.send_async(Signal::Error(error)).await?;
}
match rumor.created_at >= initialized_at {
true => {
let new_message = NewMessage::new(event.id, rumor);
let signal = Signal::Message(new_message);
@@ -204,9 +184,12 @@ impl ChatRegistry {
false => {
status.store(true, Ordering::Release);
}
},
}
}
Err(e) => {
log::warn!("Failed to unwrap the gift wrap event: {e}");
let error: SharedString =
format!("Failed to unwrap the gift wrap event: {e}").into();
tx.send_async(Signal::Error(error)).await?;
}
}
}
@@ -235,6 +218,11 @@ impl ChatRegistry {
this.get_rooms(cx);
})?;
}
Signal::Error(error) => {
this.update(cx, |_this, cx| {
cx.emit(ChatEvent::Error(error));
})?;
}
};
}
@@ -245,6 +233,7 @@ impl ChatRegistry {
/// Tracking the status of unwrapping gift wrap events.
fn tracking(&mut self, cx: &mut Context<Self>) {
let status = self.tracking_flag.clone();
let tx = self.signal_tx.clone();
self.tasks.push(cx.background_spawn(async move {
let loop_duration = Duration::from_secs(15);
@@ -252,6 +241,9 @@ impl ChatRegistry {
loop {
if status.load(Ordering::Acquire) {
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
_ = tx.send_async(Signal::Eose).await;
} else {
_ = tx.send_async(Signal::Eose).await;
}
smol::Timer::after(loop_duration).await;
}
@@ -268,29 +260,20 @@ impl ChatRegistry {
return;
};
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let id = SubscriptionId::new("contact-list");
let opts = SubscribeAutoCloseOptions::default()
.exit_policy(ReqExitPolicy::ExitOnEOSE)
.timeout(Some(Duration::from_secs(TIMEOUT)));
// Get user's write relays
let urls = write_relays.await;
// Construct filter for inbox relays
let filter = Filter::new()
.kind(Kind::ContactList)
.author(public_key)
.limit(1);
// Construct target for subscription
let target: HashMap<&RelayUrl, Filter> =
urls.iter().map(|relay| (relay, filter.clone())).collect();
// Subscribe
client.subscribe(target).close_on(opts).with_id(id).await?;
client.subscribe(filter).close_on(opts).with_id(id).await?;
Ok(())
});
@@ -298,39 +281,35 @@ impl ChatRegistry {
self.tasks.push(task);
}
/// Ensure messaging relays are set up for the current user.
pub fn ensure_messaging_relays(&mut self, cx: &mut Context<Self>) {
let task = self.verify_relays(cx);
// Set state to checking
self.set_state(InboxState::Checking, cx);
/// Get all messages for current user
pub fn get_messages(&mut self, cx: &mut Context<Self>) {
let task = self.subscribe(cx);
self.tasks.push(cx.spawn(async move |this, cx| {
let result = task.await?;
// Update state
this.update(cx, |this, cx| {
this.set_state(result, cx);
match task.await {
Ok(_) => {
this.update(cx, |_this, cx| {
cx.emit(ChatEvent::Subscribed);
})?;
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(ChatEvent::Error(SharedString::from(e.to_string())));
})?;
}
}
Ok(())
}));
}
// Verify messaging relay list for current user
fn verify_relays(&mut self, cx: &mut Context<Self>) -> Task<Result<InboxState, Error>> {
// Get messaging relay list for current user
fn get_messaging_relays(&self, cx: &App) -> Task<Result<Vec<RelayUrl>, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
return Task::ready(Err(anyhow!("User not found")));
};
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
cx.background_spawn(async move {
let urls = write_relays.await;
let public_key = signer.get_public_key().await?;
// Construct filter for inbox relays
let filter = Filter::new()
@@ -338,61 +317,32 @@ impl ChatRegistry {
.author(public_key)
.limit(1);
// Construct target for subscription
let target: HashMap<&RelayUrl, Filter> =
urls.iter().map(|relay| (relay, filter.clone())).collect();
// Stream events from user's write relays
let mut stream = client
.stream_events(target)
.stream_events(filter)
.timeout(Duration::from_secs(TIMEOUT))
.await?;
while let Some((_url, res)) = stream.next().await {
match res {
Ok(event) => {
return Ok(InboxState::RelayConfigured(Box::new(event)));
}
Err(e) => {
log::error!("Failed to receive relay list event: {e}");
}
if let Ok(event) = res {
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
return Ok(urls);
}
}
Ok(InboxState::RelayNotAvailable)
Err(anyhow!("Messaging Relays not found"))
})
}
/// Get all messages for current user
fn get_messages<I>(&mut self, relay_urls: I, cx: &mut Context<Self>)
where
I: IntoIterator<Item = RelayUrl>,
{
let task = self.subscribe(relay_urls, cx);
self.tasks.push(cx.spawn(async move |this, cx| {
task.await?;
// Update state
this.update(cx, |this, cx| {
this.set_state(InboxState::Subscribing, cx);
})?;
Ok(())
}));
}
/// Continuously get gift wrap events for the current user in their messaging relays
fn subscribe<I>(&mut self, urls: I, cx: &mut Context<Self>) -> Task<Result<(), Error>>
where
I: IntoIterator<Item = RelayUrl>,
{
fn subscribe(&self, cx: &App) -> Task<Result<(), Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let urls = urls.into_iter().collect::<Vec<_>>();
let urls = self.get_messaging_relays(cx);
cx.background_spawn(async move {
let urls = urls.await?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(USER_GIFTWRAP);
@@ -419,19 +369,6 @@ impl ChatRegistry {
})
}
/// Set the state of the inbox
fn set_state(&mut self, state: InboxState, cx: &mut Context<Self>) {
self.state.update(cx, |this, cx| {
*this = state;
cx.notify();
});
}
/// Get the relay state
pub fn state(&self, cx: &App) -> InboxState {
self.state.read(cx).clone()
}
/// Get the loading status of the chat registry
pub fn loading(&self) -> bool {
self.tracking_flag.load(Ordering::Acquire)

View File

@@ -1,5 +1,4 @@
use std::cmp::Ordering;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::time::Duration;
@@ -10,7 +9,7 @@ use itertools::Itertools;
use nostr_sdk::prelude::*;
use person::{Person, PersonRegistry};
use settings::{RoomConfig, SignerKind};
use state::{NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT};
use state::{NostrRegistry, TIMEOUT};
use crate::NewMessage;
@@ -333,9 +332,6 @@ impl Room {
let signer = nostr.read(cx).signer();
let sender = signer.public_key();
// Get room's id
let id = self.id;
// Get all members, excluding the sender
let members: Vec<PublicKey> = self
.members
@@ -345,30 +341,27 @@ impl Room {
.collect();
cx.background_spawn(async move {
let id = SubscriptionId::new(format!("room-{id}"));
let opts = SubscribeAutoCloseOptions::default()
.exit_policy(ReqExitPolicy::ExitOnEOSE)
.timeout(Some(Duration::from_secs(TIMEOUT)));
// Construct filters for each member
let filters: Vec<Filter> = members
.into_iter()
.map(|public_key| {
Filter::new()
for public_key in members.into_iter() {
let inbox = Filter::new()
.author(public_key)
.kind(Kind::RelayList)
.limit(1)
})
.collect();
.kind(Kind::InboxRelays)
.limit(1);
// Construct target for subscription
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
.into_iter()
.map(|relay| (relay, filters.clone()))
.collect();
let announcement = Filter::new()
.author(public_key)
.kind(Kind::Custom(10044))
.limit(1);
// Subscribe to the target
client.subscribe(target).close_on(opts).with_id(id).await?;
client
.subscribe(vec![inbox, announcement])
.close_on(opts)
.await?;
}
Ok(())
})
@@ -491,15 +484,9 @@ impl Room {
// Process each member
for member in members {
let relays = member.messaging_relays();
let announcement = member.announcement();
let public_key = member.public_key();
if relays.is_empty() {
reports.push(SendReport::new(public_key).error("No messaging relays"));
continue;
}
// Handle encryption signer requirements
if signer_kind.encryption() {
if announcement.is_none() {
@@ -535,8 +522,7 @@ impl Room {
SignerKind::User => (member.public_key(), user_signer.clone()),
};
match send_gift_wrap(&client, &signer, &receiver, &rumor, relays, public_key).await
{
match send_gift_wrap(&client, &signer, &receiver, &rumor, public_key).await {
Ok((report, _)) => {
reports.push(report);
sents += 1;
@@ -549,12 +535,10 @@ impl Room {
// Send backup to current user if needed
if backup && sents >= 1 {
let relays = sender.messaging_relays();
let public_key = sender.public_key();
let signer = encryption_signer.as_ref().unwrap_or(&user_signer);
match send_gift_wrap(&client, signer, &public_key, &rumor, relays, public_key).await
{
match send_gift_wrap(&client, signer, &public_key, &rumor, public_key).await {
Ok((report, _)) => reports.push(report),
Err(report) => reports.push(report),
}
@@ -571,22 +555,16 @@ async fn send_gift_wrap<T>(
signer: &T,
receiver: &PublicKey,
rumor: &UnsignedEvent,
relays: &[RelayUrl],
public_key: PublicKey,
) -> Result<(SendReport, bool), SendReport>
where
T: NostrSigner + 'static,
{
// Ensure relay connections
for url in relays {
client.add_relay(url).and_connect().await.ok();
}
match EventBuilder::gift_wrap(signer, receiver, rumor.clone(), []).await {
Ok(event) => {
match client
.send_event(&event)
.to(relays)
.to_nip17()
.ack_policy(AckPolicy::none())
.await
{

View File

@@ -7,21 +7,11 @@ use settings::SignerKind;
#[action(namespace = chat, no_json)]
pub enum Command {
Insert(&'static str),
ChangeSubject(&'static str),
ChangeSubject(String),
ChangeSigner(SignerKind),
ToggleBackup,
Subject,
Copy(PublicKey),
Relays(PublicKey),
Njump(PublicKey),
}
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = chat, no_json)]
pub struct SeenOn(pub EventId);
/// Define a open public key action
#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)]
#[action(namespace = pubkey, no_json)]
pub struct OpenPublicKey(pub PublicKey);
/// Define a copy inline public key action
#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)]
#[action(namespace = pubkey, no_json)]
pub struct CopyPublicKey(pub PublicKey);

View File

@@ -1,6 +1,5 @@
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::sync::Arc;
use std::time::Duration;
pub use actions::*;
use anyhow::{Context as AnyhowContext, Error};
@@ -24,10 +23,10 @@ use state::{NostrRegistry, upload};
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput};
use ui::menu::{ContextMenuExt, DropdownMenu};
use ui::menu::DropdownMenu;
use ui::notification::Notification;
use ui::scroll::Scrollbar;
use ui::{
@@ -42,11 +41,6 @@ mod text;
const ANNOUNCEMENT: &str =
"This conversation is private. Only members can see each other's messages.";
const NO_INBOX: &str = "has not set up messaging relays. \
They will not receive messages you send.";
const NO_ANNOUNCEMENT: &str = "has not set up an encryption key. \
You cannot send messages encrypted with an encryption key to them yet. \
Coop automatically uses your identity to encrypt messages.";
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
cx.new(|cx| ChatPanel::new(room, window, cx))
@@ -72,9 +66,15 @@ pub struct ChatPanel {
/// Mapping message (rumor event) ids to their reports
reports_by_id: Entity<BTreeMap<EventId, Vec<SendReport>>>,
/// Input state
/// Chat input state
input: Entity<InputState>,
/// Subject input state
subject_input: Entity<InputState>,
/// Subject bar visibility
subject_bar: Entity<bool>,
/// Sent message ids
sent_ids: Arc<RwLock<Vec<EventId>>>,
@@ -91,7 +91,7 @@ pub struct ChatPanel {
tasks: Vec<Task<Result<(), Error>>>,
/// Event subscriptions
subscriptions: SmallVec<[Subscription; 2]>,
subscriptions: SmallVec<[Subscription; 3]>,
}
impl ChatPanel {
@@ -124,19 +124,37 @@ impl ChatPanel {
.clean_on_escape()
});
// Define subject input state
let subject_input = cx.new(|cx| InputState::new(window, cx).placeholder("New subject..."));
let subject_bar = cx.new(|_cx| false);
// Define subscriptions
let subscriptions =
smallvec![
let mut subscriptions = smallvec![];
subscriptions.push(
// Subscribe the chat input event
cx.subscribe_in(&input, window, move |this, _input, event, window, cx| {
if let InputEvent::PressEnter { .. } = event {
this.send_text_message(window, cx);
};
})
];
}),
);
subscriptions.push(
// Subscribe the subject input event
cx.subscribe_in(
&subject_input,
window,
move |this, _input, event, window, cx| {
if let InputEvent::PressEnter { .. } = event {
this.change_subject(window, cx);
};
},
),
);
// Define all functions that will run after the current cycle
cx.defer_in(window, |this, window, cx| {
this.connect(window, cx);
this.handle_notifications(cx);
this.subscribe_room_events(window, cx);
this.get_messages(window, cx);
@@ -149,6 +167,8 @@ impl ChatPanel {
room,
list_state,
input,
subject_input,
subject_bar,
replies_to,
attachments,
rendered_texts_by_id: BTreeMap::new(),
@@ -160,49 +180,6 @@ impl ChatPanel {
}
}
/// Get all necessary data for each member
fn connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Ok((members, connect)) = self
.room
.read_with(cx, |this, cx| (this.members(), this.connect(cx)))
else {
return;
};
// Run the connect task in background
self.tasks.push(connect);
// Spawn another task to verify after 3 seconds
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(3)).await;
// Verify the connection
this.update_in(cx, |this, _window, cx| {
let persons = PersonRegistry::global(cx);
for member in members.into_iter() {
let profile = persons.read(cx).get(&member, cx);
if profile.announcement().is_none() {
let content = format!("{} {}", profile.name(), NO_ANNOUNCEMENT);
let message = Message::warning(content);
this.insert_message(message, true, cx);
}
if profile.messaging_relays().is_empty() {
let content = format!("{} {}", profile.name(), NO_INBOX);
let message = Message::warning(content);
this.insert_message(message, true, cx);
}
}
})?;
Ok(())
}));
}
/// Handle nostr notifications
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
@@ -254,10 +231,7 @@ impl ChatPanel {
}
fn subscribe_room_events(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(room) = self.room.upgrade() else {
return;
};
if let Some(room) = self.room.upgrade() {
self.subscriptions.push(
// Subscribe to room events
cx.subscribe_in(&room, window, move |this, _room, event, window, cx| {
@@ -272,6 +246,7 @@ impl ChatPanel {
}),
);
}
}
/// Load all messages belonging to this room
fn get_messages(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
@@ -316,6 +291,16 @@ impl ChatPanel {
content
}
fn change_subject(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
let subject = self.subject_input.read(cx).value();
self.room
.update(cx, |this, cx| {
this.set_subject(subject, cx);
})
.ok();
}
fn send_text_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// Get the message which includes all attachments
let content = self.get_input_value(cx);
@@ -505,10 +490,21 @@ impl ChatPanel {
}
}
fn copy_message(&self, id: &EventId, cx: &Context<Self>) {
if let Some(message) = self.message(id) {
cx.write_to_clipboard(ClipboardItem::new_string(message.content.to_string()));
fn copy_author(&self, public_key: &PublicKey, cx: &App) {
let content = public_key.to_bech32().unwrap();
let item = ClipboardItem::new_string(content);
cx.write_to_clipboard(item);
}
fn copy_message(&self, id: &EventId, cx: &App) {
let Some(message) = self.message(id) else {
return;
};
let content = message.content.to_string();
let item = ClipboardItem::new_string(content);
cx.write_to_clipboard(item);
}
fn reply_to(&mut self, id: &EventId, cx: &mut Context<Self>) {
@@ -558,7 +554,10 @@ impl ChatPanel {
Err(e) => {
this.update_in(cx, |this, window, cx| {
this.set_uploading(false, cx);
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
})?;
}
}
@@ -588,7 +587,7 @@ impl ChatPanel {
});
}
fn profile(&self, public_key: &PublicKey, cx: &Context<Self>) -> Person {
fn profile(&self, public_key: &PublicKey, cx: &App) -> Person {
let persons = PersonRegistry::global(cx);
persons.read(cx).get(public_key, cx)
}
@@ -602,11 +601,14 @@ impl ChatPanel {
if self
.room
.update(cx, |this, cx| {
this.set_subject(*subject, cx);
this.set_subject(subject, cx);
})
.is_err()
{
window.push_notification(Notification::error("Failed to change subject"), cx);
window.push_notification(
Notification::error("Failed to change subject").autohide(false),
cx,
);
}
}
Command::ChangeSigner(kind) => {
@@ -617,7 +619,10 @@ impl ChatPanel {
})
.is_err()
{
window.push_notification(Notification::error("Failed to change signer"), cx);
window.push_notification(
Notification::error("Failed to change signer").autohide(false),
cx,
);
}
}
Command::ToggleBackup => {
@@ -628,10 +633,101 @@ impl ChatPanel {
})
.is_err()
{
window.push_notification(Notification::error("Failed to toggle backup"), cx);
window.push_notification(
Notification::error("Failed to toggle backup").autohide(false),
cx,
);
}
}
Command::Subject => {
self.open_subject(window, cx);
}
Command::Copy(public_key) => {
self.copy_author(public_key, cx);
}
Command::Relays(public_key) => {
self.open_relays(public_key, window, cx);
}
Command::Njump(public_key) => {
self.open_njump(public_key, cx);
}
}
}
fn open_subject(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let subject_input = self.subject_input.clone();
window.open_modal(cx, move |this, _window, cx| {
let subject = subject_input.read(cx).value();
this.title("Change subject")
.show_close(true)
.confirm()
.child(
v_flex()
.gap_2()
.child(
v_flex()
.gap_1p5()
.child(
div()
.text_sm()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Subject:")),
)
.child(TextInput::new(&subject_input).small()),
)
.child(
div()
.italic()
.text_xs()
.text_color(cx.theme().text_placeholder)
.child(SharedString::from(
"Subject will be updated when you send a new message.",
)),
),
)
.on_ok(move |_ev, window, cx| {
window
.dispatch_action(Box::new(Command::ChangeSubject(subject.to_string())), cx);
true
})
});
}
fn open_relays(&mut self, public_key: &PublicKey, window: &mut Window, cx: &mut Context<Self>) {
let profile = self.profile(public_key, cx);
window.open_modal(cx, move |this, _window, cx| {
let relays = profile.messaging_relays();
this.title("Messaging Relays")
.show_close(true)
.child(v_flex().gap_1().children({
let mut items = vec![];
for url in relays.iter() {
items.push(
h_flex()
.h_7()
.px_2()
.gap_2()
.bg(cx.theme().elevated_surface_background)
.rounded(cx.theme().radius)
.text_sm()
.child(div().size_1p5().rounded_full().bg(gpui::green()))
.child(SharedString::from(url.to_string())),
);
}
items
}))
});
}
fn open_njump(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
let content = format!("https://njump.me/{}", public_key.to_bech32().unwrap());
cx.open_url(&content);
}
fn render_announcement(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
@@ -758,18 +854,14 @@ impl ChatPanel {
.flex()
.gap_3()
.when(!hide_avatar, |this| {
this.child(
div()
.id(SharedString::from(format!("{ix}-avatar")))
.child(Avatar::new(author.avatar()))
.context_menu(move |this, _window, _cx| {
let view = Box::new(OpenPublicKey(public_key));
let copy = Box::new(CopyPublicKey(public_key));
this.menu("View Profile", view)
.menu("Copy Public Key", copy)
}),
)
this.child(Avatar::new(author.avatar()).dropdown_menu(
move |this, _window, _cx| {
this.menu("Copy Public Key", Box::new(Command::Copy(public_key)))
.menu("View Relays", Box::new(Command::Relays(public_key)))
.separator()
.menu("View on njump.me", Box::new(Command::Njump(public_key)))
},
))
})
.child(
v_flex()
@@ -807,8 +899,17 @@ impl ChatPanel {
}),
),
)
.child(self.render_border(cx))
.child(self.render_actions(&id, cx))
.child(
div()
.group_hover("", |this| this.bg(cx.theme().element_active))
.absolute()
.left_0()
.top_0()
.w(px(2.))
.h_full()
.bg(cx.theme().border_transparent),
)
.child(self.render_actions(&id, &public_key, cx))
.on_mouse_down(
MouseButton::Middle,
cx.listener(move |this, _, _window, cx| {
@@ -896,11 +997,11 @@ impl ChatPanel {
fn render_message_reports(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
h_flex()
.id(SharedString::from(id.to_hex()))
.gap_0p5()
.text_color(cx.theme().danger_active)
.gap_1()
.text_color(cx.theme().text_danger)
.text_xs()
.italic()
.child(Icon::new(IconName::Info).xsmall())
.child(Icon::new(IconName::Info).small())
.child(SharedString::from(
"Failed to send message. Click to see details.",
))
@@ -911,7 +1012,7 @@ impl ChatPanel {
window.open_modal(cx, move |this, _window, cx| {
this.show_close(true)
.title(SharedString::from("Sent Reports"))
.child(v_flex().gap_4().pb_4().w_full().children({
.child(v_flex().gap_4().w_full().children({
let mut items = Vec::with_capacity(reports.len());
for report in reports.iter() {
@@ -1030,18 +1131,12 @@ impl ChatPanel {
})
}
fn render_border(&self, cx: &Context<Self>) -> impl IntoElement {
div()
.group_hover("", |this| this.bg(cx.theme().element_active))
.absolute()
.left_0()
.top_0()
.w(px(2.))
.h_full()
.bg(cx.theme().border_transparent)
}
fn render_actions(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
fn render_actions(
&self,
id: &EventId,
public_key: &PublicKey,
cx: &Context<Self>,
) -> impl IntoElement {
h_flex()
.p_0p5()
.gap_1()
@@ -1082,13 +1177,22 @@ impl ChatPanel {
)
.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
.child(
Button::new("seen-on")
Button::new("advance")
.icon(IconName::Ellipsis)
.small()
.ghost()
.dropdown_menu({
let id = id.to_owned();
move |this, _window, _cx| this.menu("Seen on", Box::new(SeenOn(id)))
let public_key = *public_key;
let _id = *id;
move |this, _window, _cx| {
this.menu("Copy author", Box::new(Command::Copy(public_key)))
/*
.menu(
"Trace",
Box::new(Command::Trace(id)),
)
*/
}
}),
)
.group_hover("", |this| this.visible())
@@ -1286,12 +1390,30 @@ impl Panel for ChatPanel {
h_flex()
.gap_1p5()
.child(Avatar::new(url).small())
.child(Avatar::new(url).xsmall())
.child(label)
.into_any_element()
})
.unwrap_or(div().child("Unknown").into_any_element())
}
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
let subject_bar = self.subject_bar.clone();
vec![
Button::new("subject")
.icon(IconName::Input)
.tooltip("Change subject")
.small()
.ghost()
.on_click(move |_ev, _window, cx| {
subject_bar.update(cx, |this, cx| {
*this = !*this;
cx.notify();
});
}),
]
}
}
impl EventEmitter<PanelEvent> for ChatPanel {}
@@ -1307,6 +1429,33 @@ impl Render for ChatPanel {
v_flex()
.on_action(cx.listener(Self::on_command))
.size_full()
.when(*self.subject_bar.read(cx), |this| {
this.child(
h_flex()
.h_12()
.w_full()
.px_2()
.gap_2()
.border_b_1()
.border_color(cx.theme().border)
.child(
TextInput::new(&self.subject_input)
.text_sm()
.small()
.bordered(false),
)
.child(
Button::new("change")
.icon(IconName::CheckCircle)
.label("Change")
.secondary()
.disabled(self.uploading)
.on_click(cx.listener(move |this, _ev, window, cx| {
this.change_subject(window, cx);
})),
),
)
})
.child(
v_flex()
.flex_1()

View File

@@ -1,60 +0,0 @@
use gpui::{
div, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString,
Styled, Window,
};
use theme::ActiveTheme;
use ui::input::{InputState, TextInput};
use ui::{v_flex, Sizable};
pub fn init(subject: Option<String>, window: &mut Window, cx: &mut App) -> Entity<Subject> {
cx.new(|cx| Subject::new(subject, window, cx))
}
pub struct Subject {
input: Entity<InputState>,
}
impl Subject {
pub fn new(subject: Option<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let input = cx.new(|cx| InputState::new(window, cx).placeholder("Plan for holiday"));
if let Some(value) = subject {
input.update(cx, |this, cx| {
this.set_value(value, window, cx);
});
};
Self { input }
}
pub fn new_subject(&self, cx: &App) -> SharedString {
self.input.read(cx).value()
}
}
impl Render for Subject {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.gap_2()
.child(
v_flex()
.gap_1()
.child(
div()
.text_sm()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Subject:")),
)
.child(TextInput::new(&self.input).small()),
)
.child(
div()
.text_xs()
.italic()
.text_color(cx.theme().text_placeholder)
.child(SharedString::from(
"Subject will be updated when you send a new message.",
)),
)
}
}

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use anyhow::{anyhow, Error};
use anyhow::{Error, anyhow};
use chrono::{Local, TimeZone};
use gpui::{Image, ImageFormat, SharedString};
use nostr_sdk::prelude::*;

View File

@@ -1,17 +1,17 @@
use anyhow::Error;
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window,
App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render,
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, div, px,
};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use state::{NostrRegistry, SignerEvent};
use state::{NostrRegistry, StateEvent};
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::indicator::Indicator;
use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, WindowExtension};
use ui::{Disableable, Icon, IconName, Sizable, WindowExtension, h_flex, v_flex};
use crate::dialogs::connect::ConnectSigner;
use crate::dialogs::import::ImportKey;
@@ -44,13 +44,14 @@ impl AccountSelector {
let nostr = NostrRegistry::global(cx);
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| {
match event {
SignerEvent::Set => {
StateEvent::SignerSet => {
window.close_all_modals(cx);
window.refresh();
}
SignerEvent::Error(e) => {
StateEvent::Error(e) => {
this.set_error(e.to_string(), cx);
}
_ => {}
};
});
@@ -161,7 +162,7 @@ impl Render for AccountSelector {
.italic()
.text_xs()
.text_center()
.text_color(cx.theme().danger_active)
.text_color(cx.theme().text_danger)
.child(error.clone()),
)
})

View File

@@ -4,13 +4,13 @@ use std::time::Duration;
use common::TextUtils;
use gpui::prelude::FluentBuilder;
use gpui::{
div, img, px, AppContext, Context, Entity, Image, IntoElement, ParentElement, Render,
SharedString, Styled, Subscription, Window,
AppContext, Context, Entity, Image, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Window, div, img, px,
};
use nostr_connect::prelude::*;
use state::{
CoopAuthUrlHandler, NostrRegistry, SignerEvent, CLIENT_NAME, NOSTR_CONNECT_RELAY,
NOSTR_CONNECT_TIMEOUT,
CLIENT_NAME, CoopAuthUrlHandler, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT, NostrRegistry,
StateEvent,
};
use theme::ActiveTheme;
use ui::v_flex;
@@ -31,7 +31,7 @@ impl ConnectSigner {
let error = cx.new(|_| None);
let nostr = NostrRegistry::global(cx);
let app_keys = nostr.read(cx).app_keys.clone();
let app_keys = nostr.read(cx).keys();
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
@@ -55,7 +55,7 @@ impl ConnectSigner {
// Subscribe to the signer event
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
if let SignerEvent::Error(e) = event {
if let StateEvent::Error(e) = event {
this.set_error(e, cx);
}
});
@@ -101,7 +101,7 @@ impl Render for ConnectSigner {
div()
.text_xs()
.text_center()
.text_color(cx.theme().danger_active)
.text_color(cx.theme().text_danger)
.child(error.clone()),
)
})

View File

@@ -1,18 +1,18 @@
use std::time::Duration;
use anyhow::{anyhow, Error};
use anyhow::{Error, anyhow};
use gpui::prelude::FluentBuilder;
use gpui::{
div, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Task, Window,
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Task, Window, div,
};
use nostr_connect::prelude::*;
use smallvec::{smallvec, SmallVec};
use state::{CoopAuthUrlHandler, NostrRegistry, SignerEvent};
use smallvec::{SmallVec, smallvec};
use state::{CoopAuthUrlHandler, NostrRegistry, StateEvent};
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::input::{InputEvent, InputState, TextInput};
use ui::{v_flex, Disableable};
use ui::{Disableable, v_flex};
#[derive(Debug)]
pub struct ImportKey {
@@ -60,7 +60,7 @@ impl ImportKey {
subscriptions.push(
// Subscribe to the nostr signer event
cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
if let SignerEvent::Error(e) = event {
if let StateEvent::Error(e) = event {
this.set_error(e, cx);
}
}),
@@ -117,7 +117,7 @@ impl ImportKey {
};
let nostr = NostrRegistry::global(cx);
let app_keys = nostr.read(cx).app_keys.clone();
let app_keys = nostr.read(cx).keys();
let timeout = Duration::from_secs(30);
// Construct the nostr connect signer
@@ -293,7 +293,7 @@ impl Render for ImportKey {
div()
.text_xs()
.text_center()
.text_color(cx.theme().danger_active)
.text_color(cx.theme().text_danger)
.child(error.clone()),
)
})

View File

@@ -1,7 +1,7 @@
use gpui::http_client::Url;
use gpui::{
div, px, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString,
Styled, Window,
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
Window, div, px,
};
use settings::{AppSettings, AuthMode};
use theme::{ActiveTheme, ThemeMode};
@@ -11,7 +11,7 @@ use ui::input::{InputState, TextInput};
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::Notification;
use ui::switch::Switch;
use ui::{h_flex, v_flex, IconName, Sizable, WindowExtension};
use ui::{IconName, Sizable, WindowExtension, h_flex, v_flex};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
cx.new(|cx| Preferences::new(window, cx))
@@ -41,7 +41,7 @@ impl Preferences {
AppSettings::update_file_server(url, cx);
}
Err(e) => {
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(Notification::error(e.to_string()).autohide(false), cx);
}
}
}

View File

@@ -2,9 +2,9 @@ use std::sync::{Arc, Mutex};
use assets::Assets;
use gpui::{
actions, point, px, size, App, AppContext, Bounds, KeyBinding, Menu, MenuItem, SharedString,
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
WindowOptions,
App, AppContext, Bounds, KeyBinding, Menu, MenuItem, SharedString, TitlebarOptions,
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
actions, point, px, size,
};
use gpui_platform::application;
use state::{APP_ID, CLIENT_NAME};
@@ -86,7 +86,7 @@ fn main() {
state::init(window, cx);
// Initialize person registry
person::init(cx);
person::init(window, cx);
// Initialize relay auth registry
relay_auth::init(window, cx);

View File

@@ -2,16 +2,16 @@ use std::time::Duration;
use anyhow::Error;
use gpui::{
div, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window,
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div,
};
use nostr_sdk::prelude::*;
use state::KEYRING;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::input::{InputState, TextInput};
use ui::{divider, v_flex, IconName, Sizable, StyledExt};
use ui::{IconName, Sizable, StyledExt, divider, v_flex};
const MSG: &str = "Store your account keys in a safe location. \
You can restore your account or move to another client anytime you want.";

View File

@@ -4,20 +4,20 @@ use std::time::Duration;
use anyhow::{Context as AnyhowContext, Error};
use gpui::prelude::FluentBuilder;
use gpui::{
div, rems, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, TextAlign, Window,
Task, TextAlign, Window, div, rems,
};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec};
use smallvec::{SmallVec, smallvec};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::input::{InputEvent, InputState, TextInput};
use ui::{h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ContactListPanel> {
cx.new(|cx| ContactListPanel::new(window, cx))
@@ -156,15 +156,6 @@ impl ContactListPanel {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
window.push_notification("Public Key not found", cx);
return;
};
// Get user's write relays
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
// Get contacts
let contacts: Vec<Contact> = self
@@ -177,14 +168,12 @@ impl ContactListPanel {
self.set_updating(true, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
// Construct contact list event builder
let builder = EventBuilder::contact_list(contacts);
let event = client.sign_event_builder(builder).await?;
// Set contact list
client.send_event(&event).to(urls).await?;
client.send_event(&event).to_nip65().await?;
Ok(())
});
@@ -333,7 +322,7 @@ impl Render for ContactListPanel {
div()
.italic()
.text_xs()
.text_color(cx.theme().danger_active)
.text_color(cx.theme().text_danger)
.child(error.clone()),
)
}),

View File

@@ -1,17 +1,14 @@
use chat::{ChatRegistry, InboxState};
use gpui::prelude::FluentBuilder;
use gpui::{
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Window,
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Window, div, svg,
};
use state::{NostrRegistry, RelayState};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::dock::DockPlacement;
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt};
use ui::dock::{DockPlacement, Panel, PanelEvent};
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
use crate::panels::{messaging_relays, profile, relay_list};
use crate::panels::profile;
use crate::workspace::Workspace;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
@@ -82,15 +79,6 @@ impl Render for GreeterPanel {
const TITLE: &str = "Welcome to Coop!";
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
let chat = ChatRegistry::global(cx);
let nip17 = chat.read(cx).state(cx);
let nostr = NostrRegistry::global(cx);
let nip65 = nostr.read(cx).relay_list_state.clone();
let required_actions =
nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable;
h_flex()
.size_full()
.items_center()
@@ -130,64 +118,6 @@ impl Render for GreeterPanel {
),
),
)
.when(required_actions, |this| {
this.child(
v_flex()
.gap_2()
.w_full()
.child(
h_flex()
.gap_2()
.w_full()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Required Actions"))
.child(div().flex_1().h_px().bg(cx.theme().border)),
)
.child(
v_flex()
.gap_2()
.w_full()
.when(nip65.not_configured(), |this| {
this.child(
Button::new("relaylist")
.icon(Icon::new(IconName::Relay))
.label("Set up relay list")
.ghost()
.small()
.justify_start()
.on_click(move |_ev, window, cx| {
Workspace::add_panel(
relay_list::init(window, cx),
DockPlacement::Center,
window,
cx,
);
}),
)
})
.when(nip17.not_configured(), |this| {
this.child(
Button::new("import")
.icon(Icon::new(IconName::Relay))
.label("Set up messaging relays")
.ghost()
.small()
.justify_start()
.on_click(move |_ev, window, cx| {
Workspace::add_panel(
messaging_relays::init(window, cx),
DockPlacement::Center,
window,
cx,
);
}),
)
}),
),
)
})
.child(
v_flex()
.gap_2()

View File

@@ -1,21 +1,21 @@
use std::collections::HashSet;
use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use anyhow::{Context as AnyhowContext, Error, anyhow};
use gpui::prelude::FluentBuilder;
use gpui::{
div, rems, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, TextAlign, Window,
Task, TextAlign, Window, div, rems,
};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use smallvec::{SmallVec, smallvec};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::input::{InputEvent, InputState, TextInput};
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
const MSG: &str = "Messaging Relays are relays that hosted all your messages. \
Other users will find your relays and send messages to it.";
@@ -170,15 +170,6 @@ impl MessagingRelayPanel {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
window.push_notification("Public Key not found", cx);
return;
};
// Get user's write relays
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
// Construct event tags
let tags: Vec<Tag> = self
@@ -191,14 +182,12 @@ impl MessagingRelayPanel {
self.set_updating(true, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
// Construct nip17 event builder
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
let event = client.sign_event_builder(builder).await?;
// Set messaging relays
client.send_event(&event).to(urls).await?;
client.send_event(&event).to_nip65().await?;
Ok(())
});
@@ -349,7 +338,7 @@ impl Render for MessagingRelayPanel {
div()
.italic()
.text_xs()
.text_color(cx.theme().danger_active)
.text_color(cx.theme().text_danger)
.child(error.clone()),
)
}),

View File

@@ -3,21 +3,21 @@ use std::time::Duration;
use anyhow::{Context as AnyhowContext, Error};
use gpui::{
div, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
Window,
Window, div,
};
use nostr_sdk::prelude::*;
use person::{shorten_pubkey, Person, PersonRegistry};
use person::{Person, PersonRegistry, shorten_pubkey};
use settings::AppSettings;
use state::{upload, NostrRegistry};
use state::{NostrRegistry, upload};
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::input::{InputState, TextInput};
use ui::notification::Notification;
use ui::{h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<ProfilePanel> {
cx.new(|cx| ProfilePanel::new(public_key, window, cx))
@@ -186,7 +186,10 @@ impl ProfilePanel {
Err(e) => {
this.update_in(cx, |this, window, cx| {
this.set_uploading(false, cx);
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
})?;
}
}
@@ -269,7 +272,10 @@ impl ProfilePanel {
}
Err(e) => {
cx.update(|window, cx| {
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
})?;
}
};

View File

@@ -1,23 +1,23 @@
use std::collections::HashSet;
use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use anyhow::{Context as AnyhowContext, Error, anyhow};
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, rems, Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Task, TextAlign, Window,
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, TextAlign, Window, div, px, rems,
};
use nostr_sdk::prelude::*;
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use smallvec::{SmallVec, smallvec};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::input::{InputEvent, InputState, TextInput};
use ui::menu::DropdownMenu;
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
const MSG: &str = "Relay List (or Gossip Relays) are a set of relays \
where you will publish all your events. Others also publish events \
@@ -408,7 +408,7 @@ impl Render for RelayListPanel {
div()
.italic()
.text_xs()
.text_color(cx.theme().danger_active)
.text_color(cx.theme().text_danger)
.child(error.clone()),
)
}),

View File

@@ -3,16 +3,16 @@ use std::rc::Rc;
use chat::RoomKind;
use gpui::prelude::FluentBuilder;
use gpui::{
div, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce,
SharedString, StatefulInteractiveElement, Styled, Window,
App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce, SharedString,
StatefulInteractiveElement, Styled, Window, div,
};
use nostr_sdk::prelude::*;
use settings::AppSettings;
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::dock_area::ClosePanel;
use ui::dock::ClosePanel;
use ui::modal::ModalButtonProps;
use ui::{h_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension};
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex};
use crate::dialogs::screening;

View File

@@ -18,7 +18,7 @@ use smallvec::{SmallVec, smallvec};
use state::{FIND_DELAY, NostrRegistry};
use theme::{ActiveTheme, SIDEBAR_WIDTH, TABBAR_HEIGHT};
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput};
use ui::notification::Notification;
@@ -180,7 +180,10 @@ impl Sidebar {
}
Err(e) => {
cx.update(|window, cx| {
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
})?;
}
};
@@ -500,7 +503,7 @@ impl Render for Sidebar {
.h(TABBAR_HEIGHT)
.border_b_1()
.border_color(cx.theme().border)
.bg(cx.theme().elevated_surface_background)
.bg(cx.theme().tab_background)
.child(
TextInput::new(&self.find_input)
.appearance(false)

View File

@@ -1,8 +1,10 @@
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
use ::settings::AppSettings;
use chat::{ChatEvent, ChatRegistry, InboxState};
use device::DeviceRegistry;
use chat::{ChatEvent, ChatRegistry};
use device::{DeviceEvent, DeviceRegistry};
use gpui::prelude::FluentBuilder;
use gpui::{
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
@@ -11,17 +13,15 @@ use gpui::{
use person::PersonRegistry;
use serde::Deserialize;
use smallvec::{SmallVec, smallvec};
use state::{NostrRegistry, RelayState, SignerEvent};
use state::{NostrRegistry, StateEvent};
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
use title_bar::TitleBar;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::dock::DockPlacement;
use ui::dock_area::panel::PanelView;
use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::dock::{ClosePanel, DockArea, DockItem, DockPlacement, PanelView};
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::Notification;
use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
use ui::notification::{Notification, NotificationKind};
use ui::{Disableable, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
use crate::dialogs::{accounts, settings};
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list};
@@ -37,6 +37,8 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
cx.new(|cx| Workspace::new(window, cx))
}
struct RelayNotifcation;
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = workspace, no_json)]
enum Command {
@@ -63,15 +65,23 @@ pub struct Workspace {
/// App's Dock Area
dock: Entity<DockArea>,
/// Whether a user's relay list is connected
relay_connected: bool,
/// Whether the inbox is connected
inbox_connected: bool,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 4]>,
_subscriptions: SmallVec<[Subscription; 6]>,
}
impl Workspace {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx);
let device = DeviceRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let npubs = nostr.read(cx).npubs();
let chat = ChatRegistry::global(cx);
let titlebar = cx.new(|_| TitleBar::new());
let dock = cx.new(|cx| DockArea::new(window, cx));
@@ -96,9 +106,56 @@ impl Workspace {
subscriptions.push(
// Subscribe to the signer events
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
if let SignerEvent::Set = event {
this.set_center_layout(window, cx);
match event {
StateEvent::Connecting => {
let note = Notification::new()
.id::<RelayNotifcation>()
.message("Connecting to the bootstrap relay...")
.with_kind(NotificationKind::Info)
.icon(IconName::Relay);
window.push_notification(note, cx);
}
StateEvent::Connected => {
let note = Notification::new()
.id::<RelayNotifcation>()
.message("Connected to the bootstrap relay")
.with_kind(NotificationKind::Success)
.icon(IconName::Relay);
window.push_notification(note, cx);
}
StateEvent::RelayNotConfigured => {
this.relay_notification(window, cx);
}
StateEvent::RelayConnected => {
window.clear_notification::<RelayNotifcation>(cx);
this.set_relay_connected(true, cx);
}
StateEvent::SignerSet => {
this.set_center_layout(window, cx);
this.set_relay_connected(false, cx);
this.set_inbox_connected(false, cx);
}
_ => {}
};
}),
);
subscriptions.push(
// Observe all events emitted by the device registry
cx.subscribe_in(&device, window, |_this, _device, ev, window, cx| {
match ev {
DeviceEvent::Set => {
window.push_notification(
Notification::success("Encryption Key has been set"),
cx,
);
}
DeviceEvent::Error(error) => {
window.push_notification(Notification::error(error).autohide(false), cx);
}
};
}),
);
@@ -130,6 +187,12 @@ impl Workspace {
});
});
}
ChatEvent::Subscribed => {
this.set_inbox_connected(true, cx);
}
ChatEvent::Error(error) => {
window.push_notification(Notification::error(error).autohide(false), cx);
}
_ => {}
};
}),
@@ -154,6 +217,8 @@ impl Workspace {
Self {
titlebar,
dock,
relay_connected: false,
inbox_connected: false,
_subscriptions: subscriptions,
}
}
@@ -185,6 +250,18 @@ impl Workspace {
.collect()
}
/// Set whether the relay list is connected
fn set_relay_connected(&mut self, connected: bool, cx: &mut Context<Self>) {
self.relay_connected = connected;
cx.notify();
}
/// Set whether the inbox is connected
fn set_inbox_connected(&mut self, connected: bool, cx: &mut Context<Self>) {
self.inbox_connected = connected;
cx.notify();
}
/// Set the dock layout
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
@@ -198,8 +275,8 @@ impl Workspace {
/// Set the center dock layout
fn set_center_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let dock = self.dock.downgrade();
let greeeter = Arc::new(greeter::init(window, cx));
let tabs = DockItem::tabs(vec![greeeter], None, &dock, window, cx);
let greeter = Arc::new(greeter::init(window, cx));
let tabs = DockItem::tabs(vec![greeter], None, &dock, window, cx);
let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx);
// Update the layout with center dock
@@ -267,6 +344,12 @@ impl Workspace {
);
});
}
Command::RefreshMessagingRelays => {
let chat = ChatRegistry::global(cx);
chat.update(cx, |this, cx| {
this.get_messages(cx);
});
}
Command::ShowRelayList => {
self.dock.update(cx, |this, cx| {
this.add_panel(
@@ -277,27 +360,25 @@ impl Workspace {
);
});
}
Command::RefreshRelayList => {
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
if let Some(public_key) = signer.public_key() {
nostr.update(cx, |this, cx| {
this.ensure_relay_list(&public_key, cx);
});
}
}
Command::RefreshEncryption => {
let device = DeviceRegistry::global(cx);
device.update(cx, |this, cx| {
this.get_announcement(cx);
});
}
Command::RefreshRelayList => {
let nostr = NostrRegistry::global(cx);
nostr.update(cx, |this, cx| {
this.ensure_relay_list(cx);
});
}
Command::ResetEncryption => {
self.confirm_reset_encryption(window, cx);
}
Command::RefreshMessagingRelays => {
let chat = ChatRegistry::global(cx);
chat.update(cx, |this, cx| {
this.ensure_messaging_relays(cx);
});
}
Command::ToggleTheme => {
self.theme_selector(window, cx);
}
@@ -341,8 +422,10 @@ impl Workspace {
window.close_modal(cx);
}
Err(e) => {
window
.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
}
})
.ok();
@@ -450,7 +533,56 @@ impl Workspace {
});
}
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
fn relay_notification(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const BODY: &str = "Coop cannot found your gossip relay list. \
Maybe you haven't set it yet or relay not responsed";
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
return;
};
let entity = nostr.downgrade();
let loading = Rc::new(Cell::new(false));
let note = Notification::new()
.autohide(false)
.id::<RelayNotifcation>()
.icon(IconName::Relay)
.title("Gossip Relays are required")
.message(BODY)
.action(move |_this, _window, _cx| {
let entity = entity.clone();
let public_key = public_key.to_owned();
Button::new("retry")
.label("Retry")
.small()
.primary()
.loading(loading.get())
.disabled(loading.get())
.on_click({
let loading = Rc::clone(&loading);
move |_ev, _window, cx| {
// Set loading state to true
loading.set(true);
// Retry
entity
.update(cx, |this, cx| {
this.ensure_relay_list(&public_key, cx);
})
.ok();
}
})
});
window.push_notification(note, cx);
}
fn titlebar_left(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
let current_user = signer.public_key();
@@ -529,14 +661,14 @@ impl Workspace {
})
}
fn titlebar_right(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let relay_connected = self.relay_connected;
let inbox_connected = self.inbox_connected;
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
let chat = ChatRegistry::global(cx);
let inbox_state = chat.read(cx).state(cx);
let Some(pkey) = signer.public_key() else {
let Some(public_key) = signer.public_key() else {
return div();
};
@@ -554,7 +686,7 @@ impl Workspace {
let state = device.read(cx).state();
this.min_w(px(260.))
.item(PopupMenuItem::element(move |_window, _cx| {
.item(PopupMenuItem::element(move |_window, cx| {
h_flex()
.px_1()
.w_full()
@@ -566,7 +698,7 @@ impl Workspace {
.rounded_full()
.when(state.set(), |this| this.bg(gpui::green()))
.when(state.requesting(), |this| {
this.bg(gpui::yellow())
this.bg(cx.theme().icon_accent)
}),
)
.child(SharedString::from(state.to_string()))
@@ -584,37 +716,21 @@ impl Workspace {
)
}),
)
.child(
h_flex()
.gap_2()
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.map(|this| match inbox_state {
InboxState::Checking => this.child(div().child(
SharedString::from("Fetching user's messaging relay list..."),
)),
InboxState::RelayNotAvailable => {
this.child(div().text_color(cx.theme().warning_active).child(
SharedString::from(
"User hasn't configured a messaging relay list",
),
))
}
_ => this,
}),
)
.child(
Button::new("inbox")
.icon(IconName::Inbox)
.tooltip("Inbox")
.small()
.ghost()
.when(inbox_state.subscribing(), |this| this.indicator())
.loading(!inbox_connected)
.disabled(!inbox_connected)
.when(!inbox_connected, |this| {
this.tooltip("Connecting to user's messaging relays...")
})
.when(inbox_connected, |this| this.indicator())
.dropdown_menu(move |this, _window, cx| {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&pkey, cx);
let profile = persons.read(cx).get(&public_key, cx);
let urls: Vec<SharedString> = profile
.messaging_relays()
.iter()
@@ -632,9 +748,7 @@ impl Workspace {
.w_full()
.gap_2()
.text_sm()
.child(
div().size_1p5().rounded_full().bg(gpui::green()),
)
.child(div().size_1p5().rounded_full().bg(gpui::green()))
.child(url.clone())
}))
});
@@ -652,73 +766,32 @@ impl Workspace {
Box::new(Command::ShowMessaging),
)
}),
),
)
.child(
h_flex()
.gap_2()
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.map(|this| match nostr.read(cx).relay_list_state {
RelayState::Checking => this
.child(div().child(SharedString::from(
"Fetching user's relay list...",
))),
RelayState::NotConfigured => {
this.child(div().text_color(cx.theme().warning_active).child(
SharedString::from("User hasn't configured a relay list"),
))
}
_ => this,
}),
)
.child(
Button::new("relay-list")
.icon(IconName::Relay)
.tooltip("User's relay list")
.small()
.ghost()
.when(nostr.read(cx).relay_list_state.configured(), |this| {
this.indicator()
.loading(!relay_connected)
.disabled(!relay_connected)
.when(!relay_connected, |this| {
this.tooltip("Connecting to user's relay list...")
})
.dropdown_menu(move |this, _window, cx| {
let nostr = NostrRegistry::global(cx);
let urls = nostr.read(cx).read_only_relays(&pkey, cx);
// Header
let menu = this.min_w(px(260.)).label("Relays");
// Content
let menu = urls.into_iter().fold(menu, |this, url| {
this.item(PopupMenuItem::element(move |_window, _cx| {
h_flex()
.px_1()
.w_full()
.gap_2()
.text_sm()
.child(
div().size_1p5().rounded_full().bg(gpui::green()),
)
.child(url.clone())
}))
});
// Footer
menu.separator()
.when(relay_connected, |this| this.indicator())
.dropdown_menu(move |this, _window, _cx| {
this.label("User's Relay List")
.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshRelayList),
)
.menu_with_icon(
"Update relay list",
"Update",
IconName::Settings,
Box::new(Command::ShowRelayList),
)
}),
),
)
}
}

View File

@@ -3,22 +3,19 @@ use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use anyhow::{Context as AnyhowContext, Error, anyhow};
use gpui::{
div, App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString,
Styled, Subscription, Task, Window,
App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement,
SharedString, Styled, Task, Window, div, relative,
};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec};
use state::{
app_name, Announcement, DeviceState, NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT,
};
use state::{Announcement, DEVICE_GIFTWRAP, DeviceState, NostrRegistry, TIMEOUT, app_name};
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::notification::Notification;
use ui::{h_flex, v_flex, Disableable, IconName, Sizable, WindowExtension};
use ui::{Disableable, IconName, Sizable, WindowExtension, h_flex, v_flex};
const IDENTIFIER: &str = "coop:device";
const MSG: &str = "You've requested an encryption key from another device. \
@@ -32,6 +29,15 @@ struct GlobalDeviceRegistry(Entity<DeviceRegistry>);
impl Global for GlobalDeviceRegistry {}
/// Device event.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum DeviceEvent {
/// A new encryption signer has been set
Set,
/// An error occurred
Error(SharedString),
}
/// Device Registry
///
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
@@ -42,11 +48,10 @@ pub struct DeviceRegistry {
/// Async tasks
tasks: Vec<Task<Result<(), Error>>>,
/// Subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
}
impl EventEmitter<DeviceEvent> for DeviceRegistry {}
impl DeviceRegistry {
/// Retrieve the global device registry state
pub fn global(cx: &App) -> Entity<Self> {
@@ -60,27 +65,16 @@ impl DeviceRegistry {
/// Create a new device registry instance
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let mut subscriptions = smallvec![];
let state = DeviceState::default();
subscriptions.push(
// Observe the NIP-65 state
cx.observe(&nostr, |this, state, cx| {
if state.read(cx).relay_list_state == RelayState::Configured {
this.get_announcement(cx);
};
}),
);
// Run at the end of current cycle
cx.defer_in(window, |this, window, cx| {
this.handle_notifications(window, cx);
this.get_announcement(cx);
});
Self {
state: DeviceState::default(),
state,
tasks: vec![],
_subscriptions: subscriptions,
}
}
@@ -123,9 +117,7 @@ impl DeviceRegistry {
Ok(())
}));
self.tasks.push(
// Update GPUI states
cx.spawn_in(window, async move |this, cx| {
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
while let Ok(event) = rx.recv_async().await {
match event.kind {
// New request event
@@ -145,8 +137,7 @@ impl DeviceRegistry {
}
Ok(())
}),
);
}));
}
/// Get the device state
@@ -191,45 +182,68 @@ impl DeviceRegistry {
fn get_messages(&mut self, cx: &mut Context<Self>) {
let task = self.subscribe_to_giftwrap_events(cx);
self.tasks.push(cx.spawn(async move |_this, _cx| {
task.await?;
// Update state
self.tasks.push(cx.spawn(async move |this, cx| {
if let Err(e) = task.await {
this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::Error(SharedString::from(e.to_string())));
})?;
}
Ok(())
}));
}
/// Continuously get gift wrap events for the current user in their messaging relays
fn subscribe_to_giftwrap_events(&mut self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
/// Get the messaging relays for the current user
fn get_user_messaging_relays(&self, cx: &App) -> Task<Result<Vec<RelayUrl>, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
return Task::ready(Err(anyhow!("User not found")));
};
cx.background_spawn(async move {
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
let relay_urls = profile.messaging_relays().clone();
if let Some(event) = client.database().query(filter).await?.first_owned() {
// Extract relay URLs from the event
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
// Ensure all relays are connected
for url in urls.iter() {
client.add_relay(url).and_connect().await?;
}
Ok(urls)
} else {
Err(anyhow!("Relays not found"))
}
})
}
/// Continuously get gift wrap events for the current user in their messaging relays
fn subscribe_to_giftwrap_events(&self, cx: &App) -> Task<Result<(), Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let urls = self.get_user_messaging_relays(cx);
cx.background_spawn(async move {
let urls = urls.await?;
let encryption = signer.get_encryption_signer().await.context("not found")?;
let public_key = encryption.get_public_key().await?;
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(DEVICE_GIFTWRAP);
// Construct target for subscription
let target: HashMap<RelayUrl, Filter> = relay_urls
let target: HashMap<RelayUrl, Filter> = urls
.into_iter()
.map(|relay| (relay, filter.clone()))
.collect();
let output = client.subscribe(target).with_id(id).await?;
log::info!(
"Successfully subscribed to encryption gift-wrap messages on: {:?}",
output.success
);
// Subscribe
client.subscribe(target).with_id(id).await?;
Ok(())
})
@@ -239,20 +253,13 @@ impl DeviceRegistry {
pub fn get_announcement(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
return;
};
// Reset state before fetching announcement
self.reset(cx);
// Get user's write relays
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let task: Task<Result<Event, Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
// Construct the filter for the device announcement event
let filter = Filter::new()
@@ -260,29 +267,19 @@ impl DeviceRegistry {
.author(public_key)
.limit(1);
// Construct target for subscription
let target: HashMap<&RelayUrl, Filter> =
urls.iter().map(|relay| (relay, filter.clone())).collect();
// Stream events from user's write relays
let mut stream = client
.stream_events(target)
.stream_events(filter)
.timeout(Duration::from_secs(TIMEOUT))
.await?;
while let Some((_url, res)) = stream.next().await {
match res {
Ok(event) => {
log::info!("Received device announcement event: {event:?}");
if let Ok(event) = res {
return Ok(event);
}
Err(e) => {
log::error!("Failed to receive device announcement event: {e}");
}
}
}
Err(anyhow!("Device announcement not found"))
Err(anyhow!("Announcement not found"))
});
self.tasks.push(cx.spawn(async move |this, cx| {
@@ -307,22 +304,12 @@ impl DeviceRegistry {
pub fn create_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
return Task::ready(Err(anyhow!("User not found")));
};
// Get user's write relays
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let keys = Keys::generate();
let secret = keys.secret_key().to_secret_hex();
let n = keys.public_key();
cx.background_spawn(async move {
let urls = write_relays.await;
// Construct an announcement event
let event = client
.sign_event_builder(EventBuilder::new(Kind::Custom(10044), "").tags(vec![
@@ -332,7 +319,7 @@ impl DeviceRegistry {
.await?;
// Publish announcement
client.send_event(&event).to(urls).await?;
client.send_event(&event).to_nip65().await?;
// Save device keys to the database
set_keys(&client, &secret).await?;
@@ -409,23 +396,15 @@ impl DeviceRegistry {
return;
};
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
// Construct a filter for device key requests
let filter = Filter::new()
.kind(Kind::Custom(4454))
.author(public_key)
.since(Timestamp::now());
// Construct target for subscription
let target: HashMap<&RelayUrl, Filter> =
urls.iter().map(|relay| (relay, filter.clone())).collect();
// Subscribe to the device key requests on user's write relays
client.subscribe(target).await?;
client.subscribe(filter).await?;
Ok(())
});
@@ -443,23 +422,15 @@ impl DeviceRegistry {
return;
};
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
self.tasks.push(cx.background_spawn(async move {
let urls = write_relays.await;
// Construct a filter for device key requests
let filter = Filter::new()
.kind(Kind::Custom(4455))
.author(public_key)
.since(Timestamp::now());
// Construct target for subscription
let target: HashMap<&RelayUrl, Filter> =
urls.iter().map(|relay| (relay, filter.clone())).collect();
// Subscribe to the device key requests on user's write relays
client.subscribe(target).await?;
client.subscribe(filter).await?;
Ok(())
}));
@@ -471,13 +442,7 @@ impl DeviceRegistry {
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
return;
};
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let app_keys = nostr.read(cx).app_keys.clone();
let app_keys = nostr.read(cx).keys();
let app_pubkey = app_keys.public_key();
let task: Task<Result<Option<Keys>, Error>> = cx.background_spawn(async move {
@@ -507,8 +472,6 @@ impl DeviceRegistry {
Ok(Some(keys))
}
None => {
let urls = write_relays.await;
// Construct an event for device key request
let event = client
.sign_event_builder(EventBuilder::new(Kind::Custom(4454), "").tags(vec![
@@ -518,7 +481,7 @@ impl DeviceRegistry {
.await?;
// Send the event to write relays
client.send_event(&event).to(urls).await?;
client.send_event(&event).to_nip65().await?;
Ok(None)
}
@@ -549,7 +512,7 @@ impl DeviceRegistry {
/// Parse the response event for device keys from other devices
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let app_keys = nostr.read(cx).app_keys.clone();
let app_keys = nostr.read(cx).keys();
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
let root_device = event
@@ -586,18 +549,11 @@ impl DeviceRegistry {
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
return;
};
// Get user's write relays
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let event = event.clone();
let id: SharedString = event.id.to_hex().into();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
// Get device keys
let keys = get_keys(&client).await?;
let secret = keys.secret_key().to_secret_hex();
@@ -626,7 +582,7 @@ impl DeviceRegistry {
let event = client.sign_event_builder(builder).await?;
// Send the response event to the user's relay list
client.send_event(&event).to(urls).await?;
client.send_event(&event).to_nip65().await?;
Ok(())
});
@@ -635,13 +591,16 @@ impl DeviceRegistry {
match task.await {
Ok(_) => {
cx.update(|window, cx| {
window.clear_notification(id, cx);
window.clear_notification_by_id::<DeviceNotification>(id, cx);
})
.ok();
}
Err(e) => {
cx.update(|window, cx| {
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
})
.ok();
}
@@ -671,17 +630,23 @@ impl DeviceRegistry {
let entity = cx.entity().downgrade();
let loading = Rc::new(Cell::new(false));
let key = SharedString::from(event.id.to_hex());
Notification::new()
.custom_id(SharedString::from(event.id.to_hex()))
.type_id::<DeviceNotification>(key)
.autohide(false)
.icon(IconName::UserKey)
.title(SharedString::from("New request"))
.content(move |_window, cx| {
.content(move |_this, _window, cx| {
v_flex()
.gap_2()
.text_sm()
.child(SharedString::from(MSG))
.child(
div()
.text_sm()
.line_height(relative(1.25))
.child(SharedString::from(MSG)),
)
.child(
v_flex()
.gap_2()
@@ -733,7 +698,7 @@ impl DeviceRegistry {
)
.into_any_element()
})
.action(move |_window, _cx| {
.action(move |_this, _window, _cx| {
let view = entity.clone();
let event = event.clone();
@@ -759,6 +724,8 @@ impl DeviceRegistry {
}
}
struct DeviceNotification;
/// Verify the author of an event
async fn verify_author(client: &Client, event: &Event) -> bool {
if let Some(signer) = client.signer() {

View File

@@ -15,3 +15,4 @@ smallvec.workspace = true
smol.workspace = true
flume.workspace = true
log.workspace = true
urlencoding = "2.1.3"

View File

@@ -3,19 +3,19 @@ use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::time::Duration;
use anyhow::{anyhow, Error};
use anyhow::{Error, anyhow};
use common::EventUtils;
use gpui::{App, AppContext, Context, Entity, Global, Task};
use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT};
use smallvec::{SmallVec, smallvec};
use state::{Announcement, BOOTSTRAP_RELAYS, NostrRegistry, TIMEOUT};
mod person;
pub use person::*;
pub fn init(cx: &mut App) {
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx);
pub fn init(window: &mut Window, cx: &mut App) {
PersonRegistry::set_global(cx.new(|cx| PersonRegistry::new(window, cx)), cx);
}
struct GlobalPersonRegistry(Entity<PersonRegistry>);
@@ -36,13 +36,13 @@ pub struct PersonRegistry {
persons: HashMap<PublicKey, Entity<Person>>,
/// Set of public keys that have been seen
seen: Rc<RefCell<HashSet<PublicKey>>>,
seens: Rc<RefCell<HashSet<PublicKey>>>,
/// Sender for requesting metadata
sender: flume::Sender<PublicKey>,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 4]>,
tasks: SmallVec<[Task<()>; 4]>,
}
impl PersonRegistry {
@@ -57,13 +57,13 @@ impl PersonRegistry {
}
/// Create a new person registry instance
fn new(cx: &mut Context<Self>) -> Self {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Dispatch>(100);
let (mta_tx, mta_rx) = flume::bounded::<PublicKey>(100);
let (mta_tx, mta_rx) = flume::unbounded::<PublicKey>();
let mut tasks = smallvec![];
@@ -111,33 +111,16 @@ impl PersonRegistry {
}),
);
tasks.push(
// Load all user profiles from the database
cx.spawn(async move |this, cx| {
let result = cx
.background_executor()
.await_on_background(async move { load_persons(&client).await })
.await;
match result {
Ok(persons) => {
this.update(cx, |this, cx| {
this.bulk_inserts(persons, cx);
})
.ok();
}
Err(e) => {
log::error!("Failed to load all persons from the database: {e}");
}
};
}),
);
cx.defer_in(window, |this, _window, cx| {
this.load(cx);
});
Self {
persons: HashMap::new(),
seen: Rc::new(RefCell::new(HashSet::new())),
seens: Rc::new(RefCell::new(HashSet::new())),
sender: mta_tx,
_tasks: tasks,
tasks,
}
}
@@ -163,25 +146,21 @@ impl PersonRegistry {
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
let person = Person::new(event.pubkey, metadata);
let val = Box::new(person);
// Send
tx.send_async(Dispatch::Person(val)).await.ok();
}
Kind::ContactList => {
let public_keys = event.extract_public_keys();
// Get metadata for all public keys
get_metadata(client, public_keys).await.ok();
}
Kind::InboxRelays => {
let val = Box::new(event.into_owned());
// Send
tx.send_async(Dispatch::Relays(val)).await.ok();
}
Kind::Custom(10044) => {
let val = Box::new(event.into_owned());
// Send
tx.send_async(Dispatch::Announcement(val)).await.ok();
}
@@ -198,7 +177,7 @@ impl PersonRegistry {
loop {
match flume::Selector::new()
.recv(rx, |result| result.ok())
.wait_timeout(Duration::from_secs(2))
.wait_timeout(Duration::from_secs(TIMEOUT))
{
Ok(Some(public_key)) => {
batch.insert(public_key);
@@ -208,40 +187,81 @@ impl PersonRegistry {
}
}
_ => {
if !batch.is_empty() {
get_metadata(client, std::mem::take(&mut batch)).await.ok();
}
}
}
}
}
/// Load all user profiles from the database
fn load(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let task: Task<Result<Vec<Person>, Error>> = cx.background_spawn(async move {
let filter = Filter::new().kind(Kind::Metadata).limit(200);
let events = client.database().query(filter).await?;
let persons = events
.into_iter()
.map(|event| {
let metadata = Metadata::from_json(event.content).unwrap_or_default();
Person::new(event.pubkey, metadata)
})
.collect();
Ok(persons)
});
self.tasks.push(cx.spawn(async move |this, cx| {
if let Ok(persons) = task.await {
this.update(cx, |this, cx| {
this.bulk_inserts(persons, cx);
})
.ok();
}
}));
}
/// Set profile encryption keys announcement
fn set_announcement(&mut self, event: &Event, cx: &mut App) {
if let Some(person) = self.persons.get(&event.pubkey) {
let announcement = Announcement::from(event);
if let Some(person) = self.persons.get(&event.pubkey) {
person.update(cx, |person, cx| {
person.set_announcement(announcement);
cx.notify();
});
} else {
let person =
Person::new(event.pubkey, Metadata::default()).with_announcement(announcement);
self.insert(person, cx);
}
}
/// Set messaging relays for a person
fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) {
if let Some(person) = self.persons.get(&event.pubkey) {
let urls: Vec<RelayUrl> = nip17::extract_relay_list(event).cloned().collect();
if let Some(person) = self.persons.get(&event.pubkey) {
person.update(cx, |person, cx| {
person.set_messaging_relays(urls);
cx.notify();
});
} else {
let person = Person::new(event.pubkey, Metadata::default()).with_messaging_relays(urls);
self.insert(person, cx);
}
}
/// Insert batch of persons
fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
for person in persons.into_iter() {
self.persons.insert(person.public_key(), cx.new(|_| person));
let public_key = person.public_key();
self.persons
.entry(public_key)
.or_insert_with(|| cx.new(|_| person));
}
cx.notify();
}
@@ -270,7 +290,7 @@ impl PersonRegistry {
}
let public_key = *public_key;
let mut seen = self.seen.borrow_mut();
let mut seen = self.seens.borrow_mut();
if seen.insert(public_key) {
let sender = self.sender.clone();
@@ -322,19 +342,3 @@ where
Ok(())
}
/// Load all user profiles from the database
async fn load_persons(client: &Client) -> Result<Vec<Person>, Error> {
let filter = Filter::new().kind(Kind::Metadata).limit(200);
let events = client.database().query(filter).await?;
let mut persons = vec![];
for event in events.into_iter() {
let metadata = Metadata::from_json(event.content).unwrap_or_default();
let person = Person::new(event.pubkey, metadata);
persons.push(person);
}
Ok(persons)
}

View File

@@ -65,6 +65,21 @@ impl Person {
}
}
/// Build profile encryption keys announcement
pub fn with_announcement(mut self, announcement: Announcement) -> Self {
self.announcement = Some(announcement);
self
}
/// Build profile messaging relays
pub fn with_messaging_relays<I>(mut self, relays: I) -> Self
where
I: IntoIterator<Item = RelayUrl>,
{
self.messaging_relays = relays.into_iter().collect();
self
}
/// Get profile public key
pub fn public_key(&self) -> PublicKey {
self.public_key
@@ -75,21 +90,11 @@ impl Person {
self.metadata.clone()
}
/// Set profile metadata
pub fn set_metadata(&mut self, metadata: Metadata) {
self.metadata = metadata;
}
/// Get profile encryption keys announcement
pub fn announcement(&self) -> Option<Announcement> {
self.announcement.clone()
}
/// Set profile encryption keys announcement
pub fn set_announcement(&mut self, announcement: Announcement) {
self.announcement = Some(announcement);
}
/// Get profile messaging relays
pub fn messaging_relays(&self) -> &Vec<RelayUrl> {
&self.messaging_relays
@@ -100,14 +105,6 @@ impl Person {
self.messaging_relays.first().cloned()
}
/// Set profile messaging relays
pub fn set_messaging_relays<I>(&mut self, relays: I)
where
I: IntoIterator<Item = RelayUrl>,
{
self.messaging_relays = relays.into_iter().collect();
}
/// Get profile avatar
pub fn avatar(&self) -> SharedString {
self.metadata()
@@ -115,8 +112,9 @@ impl Person {
.as_ref()
.filter(|picture| !picture.is_empty())
.map(|picture| {
let encoded_picture = urlencoding::encode(picture);
let url = format!(
"{IMAGE_RESIZER}/?url={picture}&w=100&h=100&fit=cover&mask=circle&n=-1"
"{IMAGE_RESIZER}/?url={encoded_picture}&w=100&h=100&fit=cover&mask=circle&n=-1"
);
url.into()
})
@@ -139,6 +137,24 @@ impl Person {
SharedString::from(shorten_pubkey(self.public_key(), 4))
}
/// Set profile metadata
pub fn set_metadata(&mut self, metadata: Metadata) {
self.metadata = metadata;
}
/// Set profile encryption keys announcement
pub fn set_announcement(&mut self, announcement: Announcement) {
self.announcement = Some(announcement);
}
/// Set profile messaging relays
pub fn set_messaging_relays<I>(&mut self, relays: I)
where
I: IntoIterator<Item = RelayUrl>,
{
self.messaging_relays = relays.into_iter().collect();
}
}
/// Shorten a [`PublicKey`] to a string with the first and last `len` characters
@@ -148,7 +164,7 @@ pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> String {
let Ok(pubkey) = public_key.to_bech32();
format!(
"{}:{}",
"{}...{}",
&pubkey[0..(len + 1)],
&pubkey[pubkey.len() - len..]
)

View File

@@ -5,19 +5,19 @@ use std::hash::Hash;
use std::rc::Rc;
use std::sync::Arc;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use anyhow::{Context as AnyhowContext, Error, anyhow};
use gpui::{
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
Task, Window,
Task, Window, div, relative,
};
use nostr_sdk::prelude::*;
use settings::{AppSettings, AuthMode};
use smallvec::{smallvec, SmallVec};
use smallvec::{SmallVec, smallvec};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::notification::Notification;
use ui::{v_flex, Disableable, IconName, Sizable, WindowExtension};
use ui::{Disableable, IconName, Sizable, WindowExtension, v_flex};
const AUTH_MESSAGE: &str =
"Approve the authentication request to allow Coop to continue sending or receiving events.";
@@ -34,7 +34,10 @@ struct AuthRequest {
}
impl AuthRequest {
pub fn new(challenge: impl Into<String>, url: RelayUrl) -> Self {
pub fn new<S>(challenge: S, url: RelayUrl) -> Self
where
S: Into<String>,
{
Self {
challenge: challenge.into(),
url,
@@ -106,22 +109,6 @@ impl RelayAuth {
tx.send_async(signal).await.ok();
}
}
RelayMessage::Closed {
subscription_id,
message,
} => {
let msg = MachineReadablePrefix::parse(&message);
if let Some(MachineReadablePrefix::AuthRequired) = msg {
if let Ok(Some(relay)) = client.relay(&relay_url).await {
// Send close message to relay
relay
.send_msg(ClientMessage::Close(subscription_id))
.await
.ok();
}
}
}
RelayMessage::Ok {
event_id, message, ..
} => {
@@ -273,7 +260,7 @@ impl RelayAuth {
fn response(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) {
let settings = AppSettings::global(cx);
let req = req.clone();
let challenge = req.challenge().to_string();
let challenge = SharedString::from(req.challenge().to_string());
// Create a task for authentication
let task = self.auth(&req, cx);
@@ -283,7 +270,7 @@ impl RelayAuth {
let url = req.url();
this.update_in(cx, |this, window, cx| {
window.clear_notification(challenge, cx);
window.clear_notification_by_id::<AuthNotification>(challenge, cx);
match result {
Ok(_) => {
@@ -295,10 +282,19 @@ impl RelayAuth {
this.add_trusted_relay(url, cx);
});
window.push_notification(format!("{} has been authenticated", url), cx);
window.push_notification(
Notification::success(format!(
"Relay {} has been authenticated",
url.domain().unwrap_or_default()
)),
cx,
);
}
Err(e) => {
window.push_notification(Notification::error(e.to_string()), cx);
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
}
}
})
@@ -323,20 +319,25 @@ impl RelayAuth {
/// Build a notification for the authentication request.
fn notification(&self, req: &Arc<AuthRequest>, cx: &Context<Self>) -> Notification {
let req = req.clone();
let challenge = SharedString::from(req.challenge.clone());
let url = SharedString::from(req.url().to_string());
let entity = cx.entity().downgrade();
let loading = Rc::new(Cell::new(false));
Notification::new()
.custom_id(SharedString::from(&req.challenge))
.type_id::<AuthNotification>(challenge)
.autohide(false)
.icon(IconName::Info)
.icon(IconName::Warning)
.title(SharedString::from("Authentication Required"))
.content(move |_window, cx| {
.content(move |_this, _window, cx| {
v_flex()
.gap_2()
.child(
div()
.text_sm()
.child(SharedString::from(AUTH_MESSAGE))
.line_height(relative(1.25))
.child(SharedString::from(AUTH_MESSAGE)),
)
.child(
v_flex()
.py_1()
@@ -349,7 +350,7 @@ impl RelayAuth {
)
.into_any_element()
})
.action(move |_window, _cx| {
.action(move |_this, _window, _cx| {
let view = entity.clone();
let req = req.clone();
@@ -374,3 +375,5 @@ impl RelayAuth {
})
}
}
struct AuthNotification;

View File

@@ -10,6 +10,8 @@ common = { path = "../common" }
nostr.workspace = true
nostr-sdk.workspace = true
nostr-lmdb.workspace = true
nostr-memory.workspace = true
nostr-gossip-sqlite.workspace = true
nostr-connect.workspace = true
nostr-blossom.workspace = true

View File

@@ -40,7 +40,8 @@ pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
/// Default bootstrap relays
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
pub const BOOTSTRAP_RELAYS: [&str; 4] = [
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://indexer.coracle.social",
"wss://user.kindpag.es",

View File

@@ -1,83 +0,0 @@
use std::collections::{HashMap, HashSet};
use gpui::SharedString;
use nostr_sdk::prelude::*;
/// Gossip
#[derive(Debug, Clone, Default)]
pub struct Gossip {
relays: HashMap<PublicKey, HashSet<(RelayUrl, Option<RelayMetadata>)>>,
}
impl Gossip {
pub fn read_only_relays(&self, public_key: &PublicKey) -> Vec<SharedString> {
self.relays
.get(public_key)
.map(|relays| {
relays
.iter()
.map(|(url, _)| url.to_string().into())
.collect()
})
.unwrap_or_default()
}
/// Get read relays for a given public key
pub fn read_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
self.relays
.get(public_key)
.map(|relays| {
relays
.iter()
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == &Some(RelayMetadata::Read) {
Some(url.to_owned())
} else {
None
}
})
.collect()
})
.unwrap_or_default()
}
/// Get write relays for a given public key
pub fn write_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
self.relays
.get(public_key)
.map(|relays| {
relays
.iter()
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
Some(url.to_owned())
} else {
None
}
})
.collect()
})
.unwrap_or_default()
}
/// Insert gossip relays for a public key
pub fn insert_relays(&mut self, event: &Event) {
self.relays.entry(event.pubkey).or_default().extend(
event
.tags
.iter()
.filter_map(|tag| {
if let Some(TagStandard::RelayMetadata {
relay_url,
metadata,
}) = tag.clone().to_standardized()
{
Some((relay_url, metadata))
} else {
None
}
})
.take(3),
);
}
}

View File

@@ -1,4 +1,4 @@
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
@@ -6,20 +6,20 @@ use anyhow::{Context as AnyhowContext, Error, anyhow};
use common::config_dir;
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window};
use nostr_connect::prelude::*;
use nostr_gossip_sqlite::prelude::*;
use nostr_lmdb::prelude::*;
use nostr_memory::prelude::*;
use nostr_sdk::prelude::*;
mod blossom;
mod constants;
mod device;
mod gossip;
mod nip05;
mod signer;
pub use blossom::*;
pub use constants::*;
pub use device::*;
pub use gossip::*;
pub use nip05::*;
pub use signer::*;
@@ -41,6 +41,23 @@ struct GlobalNostrRegistry(Entity<NostrRegistry>);
impl Global for GlobalNostrRegistry {}
/// Signer event.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum StateEvent {
/// Connecting to the bootstrapping relay
Connecting,
/// Connected to the bootstrapping relay
Connected,
/// User has not set up NIP-65 relays
RelayNotConfigured,
/// Connected to NIP-65 relays
RelayConnected,
/// A new signer has been set
SignerSet,
/// An error occurred
Error(SharedString),
}
/// Nostr Registry
#[derive(Debug)]
pub struct NostrRegistry {
@@ -53,21 +70,17 @@ pub struct NostrRegistry {
/// Local public keys
npubs: Entity<Vec<PublicKey>>,
/// Custom gossip implementation
gossip: Entity<Gossip>,
/// App keys
///
/// Used for Nostr Connect and NIP-4e operations
pub app_keys: Keys,
/// Relay list state
pub relay_list_state: RelayState,
app_keys: Keys,
/// Tasks for asynchronous operations
tasks: Vec<Task<Result<(), Error>>>,
tasks: Vec<Task<()>>,
}
impl EventEmitter<StateEvent> for NostrRegistry {}
impl NostrRegistry {
/// Retrieve the global nostr state
pub fn global(cx: &App) -> Entity<Self> {
@@ -88,32 +101,43 @@ impl NostrRegistry {
// Construct the nostr npubs entity
let npubs = cx.new(|_| vec![]);
// Construct the gossip entity
let gossip = cx.new(|_| Gossip::default());
// Construct the nostr gossip instance
let gossip = cx.foreground_executor().block_on(async move {
NostrGossipSqlite::open(config_dir().join("gossip"))
.await
.expect("Failed to initialize gossip instance")
});
// Construct the nostr client builder
let mut builder = ClientBuilder::default()
.signer(signer.clone())
.gossip(gossip)
.automatic_authentication(false)
.verify_subscriptions(false)
.connect_timeout(Duration::from_secs(TIMEOUT))
.sleep_when_idle(SleepWhenIdle::Enabled {
timeout: Duration::from_secs(600),
});
// Add database if not in debug mode
if !cfg!(debug_assertions) {
// Construct the nostr lmdb instance
let lmdb = cx.foreground_executor().block_on(async move {
NostrLmdb::open(config_dir().join("nostr"))
.await
.expect("Failed to initialize database")
});
builder = builder.database(lmdb);
} else {
builder = builder.database(MemoryDatabase::unbounded())
}
// Construct the nostr client
let client = ClientBuilder::default()
.signer(signer.clone())
.database(lmdb)
.automatic_authentication(false)
.verify_subscriptions(false)
.connect_timeout(Duration::from_secs(TIMEOUT))
.sleep_when_idle(SleepWhenIdle::Enabled {
timeout: Duration::from_secs(600),
})
.build();
// Build the nostr client
let client = builder.build();
// Run at the end of current cycle
cx.defer_in(window, |this, _window, cx| {
this.connect(cx);
this.handle_notifications(cx);
});
Self {
@@ -121,8 +145,6 @@ impl NostrRegistry {
signer,
npubs,
app_keys,
gossip,
relay_list_state: RelayState::Idle,
tasks: vec![],
}
}
@@ -142,94 +164,57 @@ impl NostrRegistry {
self.npubs.clone()
}
/// Get the app keys
pub fn keys(&self) -> Keys {
self.app_keys.clone()
}
/// Connect to the bootstrapping relays
fn connect(&mut self, cx: &mut Context<Self>) {
let client = self.client();
self.tasks.push(cx.spawn(async move |this, cx| {
cx.background_executor()
.await_on_background(async move {
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
// Add search relay to the relay pool
for url in SEARCH_RELAYS.into_iter() {
client.add_relay(url).await.ok();
client.add_relay(url).await?;
}
// Add bootstrap relay to the relay pool
for url in BOOTSTRAP_RELAYS.into_iter() {
client.add_relay(url).await.ok();
client.add_relay(url).await?;
}
// Connect to all added relays
client.connect().and_wait(Duration::from_secs(2)).await;
})
.await;
client.connect().await;
// Update the state
Ok(())
});
// Emit connecting event
cx.emit(StateEvent::Connecting);
self.tasks
.push(cx.spawn(async move |this, cx| match task.await {
Ok(_) => {
this.update(cx, |this, cx| {
cx.emit(StateEvent::Connected);
this.get_npubs(cx);
})?;
Ok(())
}));
})
.ok();
}
/// Handle nostr notifications
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let gossip = self.gossip.downgrade();
// Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Event>(2048);
self.tasks.push(cx.background_spawn(async move {
// Handle nostr notifications
let mut notifications = client.notifications();
let mut processed_events = HashSet::new();
while let Some(notification) = notifications.next().await {
if let ClientNotification::Message {
message:
RelayMessage::Event {
event,
subscription_id,
},
..
} = notification
{
if !processed_events.insert(event.id) {
// Skip if the event has already been processed
continue;
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})
.ok();
}
if let Kind::RelayList = event.kind {
if subscription_id.as_str().contains("room-") {
get_events_for_room(&client, &event).await.ok();
}
tx.send_async(event.into_owned()).await?;
}
}
}
Ok(())
}));
self.tasks.push(cx.spawn(async move |_this, cx| {
while let Ok(event) = rx.recv_async().await {
if let Kind::RelayList = event.kind {
gossip.update(cx, |this, cx| {
this.insert_relays(&event);
cx.notify();
})?;
}
}
Ok(())
}));
}
/// Get all used npubs
fn get_npubs(&mut self, cx: &mut Context<Self>) {
let npubs = self.npubs.downgrade();
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
let dir = config_dir().join("keys");
// Ensure keys directory exists
@@ -269,25 +254,26 @@ impl NostrRegistry {
true => {
this.update(cx, |this, cx| {
this.create_identity(cx);
})?;
})
.ok();
}
false => {
// TODO: auto login
npubs.update(cx, |this, cx| {
npubs
.update(cx, |this, cx| {
this.extend(public_keys);
cx.notify();
})?;
})
.ok();
}
},
Err(e) => {
log::error!("Failed to get npubs: {e}");
this.update(cx, |this, cx| {
this.create_identity(cx);
})?;
this.update(cx, |_this, cx| {
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})
.ok();
}
}
Ok(())
}));
}
@@ -307,74 +293,49 @@ impl NostrRegistry {
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let signer = async_keys.into_nostr_signer();
// Get default relay list
// Construct relay list event
let relay_list = default_relay_list();
// Extract write relays
let write_urls: Vec<RelayUrl> = relay_list
.iter()
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
Some(url)
} else {
None
}
})
.cloned()
.collect();
// Ensure connected to all relays
for (url, _metadata) in relay_list.iter() {
client.add_relay(url).and_connect().await?;
}
// Publish relay list event
let event = EventBuilder::relay_list(relay_list).sign(&signer).await?;
let output = client
// Publish relay list
client
.send_event(&event)
.to(BOOTSTRAP_RELAYS)
.ok_timeout(Duration::from_secs(TIMEOUT))
.await?;
log::info!("Sent gossip relay list: {output:?}");
// Construct the default metadata
let name = petname::petname(2, "-").unwrap_or("Cooper".to_string());
let avatar = Url::parse(&format!("https://avatar.vercel.sh/{name}")).unwrap();
let metadata = Metadata::new().display_name(&name).picture(avatar);
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
// Publish metadata event
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
client
.send_event(&event)
.to(&write_urls)
.to_nip65()
.ack_policy(AckPolicy::none())
.await?;
// Construct the default contact list
let contacts = vec![Contact::new(PublicKey::parse(COOP_PUBKEY).unwrap())];
let event = EventBuilder::contact_list(contacts).sign(&signer).await?;
// Publish contact list event
let event = EventBuilder::contact_list(contacts).sign(&signer).await?;
client
.send_event(&event)
.to(&write_urls)
.to_nip65()
.ack_policy(AckPolicy::none())
.await?;
// Construct the default messaging relay list
let relays = default_messaging_relays();
// Ensure connected to all relays
for url in relays.iter() {
client.add_relay(url).and_connect().await?;
}
let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?;
// Publish messaging relay list event
let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?;
client
.send_event(&event)
.to(&write_urls)
.to_nip65()
.ack_policy(AckPolicy::none())
.await?;
@@ -385,15 +346,20 @@ impl NostrRegistry {
});
self.tasks.push(cx.spawn(async move |this, cx| {
// Wait for the task to complete
task.await?;
// Set signer
match task.await {
Ok(_) => {
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
})?;
Ok(())
})
.ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})
.ok();
}
};
}));
}
@@ -472,6 +438,7 @@ impl NostrRegistry {
Ok(public_key) => {
// Update states
this.update(cx, |this, cx| {
this.ensure_relay_list(&public_key, cx);
// Add public key to npubs if not already present
this.npubs.update(cx, |this, cx| {
if !this.contains(&public_key) {
@@ -479,22 +446,18 @@ impl NostrRegistry {
cx.notify();
}
});
// Ensure relay list for the user
this.ensure_relay_list(cx);
// Emit signer changed event
cx.emit(SignerEvent::Set);
})?;
cx.emit(StateEvent::SignerSet);
})
.ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(SignerEvent::Error(e.to_string()));
})?;
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})
.ok();
}
}
Ok(())
};
}));
}
@@ -506,16 +469,15 @@ impl NostrRegistry {
self.tasks.push(cx.spawn(async move |this, cx| {
let key_path = keys_dir.join(format!("{}.npub", npub));
smol::fs::remove_file(key_path).await?;
smol::fs::remove_file(key_path).await.ok();
this.update(cx, |this, cx| {
this.npubs().update(cx, |this, cx| {
this.retain(|k| k != &public_key);
cx.notify();
});
})?;
Ok(())
})
.ok();
}));
}
@@ -533,16 +495,16 @@ impl NostrRegistry {
Ok(_) => {
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
})?;
})
.ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(SignerEvent::Error(e.to_string()));
})?;
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})
.ok();
}
}
Ok(())
};
}));
}
@@ -564,192 +526,90 @@ impl NostrRegistry {
match task.await {
Ok((public_key, uri)) => {
let username = public_key.to_bech32().unwrap();
let write_credential = this.read_with(cx, |_this, cx| {
cx.write_credentials(&username, "nostrconnect", uri.to_string().as_bytes())
})?;
let write_credential = this
.read_with(cx, |_this, cx| {
cx.write_credentials(
&username,
"nostrconnect",
uri.to_string().as_bytes(),
)
})
.unwrap();
match write_credential.await {
Ok(_) => {
this.update(cx, |this, cx| {
this.set_signer(nip46, cx);
})?;
})
.ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(SignerEvent::Error(e.to_string()));
})?;
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})
.ok();
}
}
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(SignerEvent::Error(e.to_string()));
})?;
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})
.ok();
}
}
Ok(())
};
}));
}
/// Set the state of the relay list
fn set_relay_state(&mut self, state: RelayState, cx: &mut Context<Self>) {
self.relay_list_state = state;
cx.notify();
}
pub fn ensure_relay_list(&mut self, cx: &mut Context<Self>) {
let task = self.verify_relay_list(cx);
// Set the state to idle before starting the task
self.set_relay_state(RelayState::default(), cx);
pub fn ensure_relay_list(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
let task = self.get_event(public_key, Kind::RelayList, cx);
self.tasks.push(cx.spawn(async move |this, cx| {
let result = task.await?;
// Update state
this.update(cx, |this, cx| {
this.relay_list_state = result;
cx.notify();
})?;
Ok(())
match task.await {
Ok(_) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::RelayConnected);
})
.ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::RelayNotConfigured);
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})
.ok();
}
};
}));
}
// Verify relay list for current user
fn verify_relay_list(&mut self, cx: &mut Context<Self>) -> Task<Result<RelayState, Error>> {
/// Get an event with the given author and kind.
pub fn get_event(
&self,
author: &PublicKey,
kind: Kind,
cx: &App,
) -> Task<Result<Event, Error>> {
let client = self.client();
let public_key = *author;
cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::RelayList)
.author(public_key)
.limit(1);
// Construct target for subscription
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
.into_iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect();
// Stream events from the bootstrap relays
let filter = Filter::new().kind(kind).author(public_key).limit(1);
let mut stream = client
.stream_events(target)
.timeout(Duration::from_secs(TIMEOUT))
.stream_events(filter)
.timeout(Duration::from_millis(800))
.await?;
while let Some((_url, res)) = stream.next().await {
match res {
Ok(event) => {
log::info!("Received relay list event: {event:?}");
return Ok(RelayState::Configured);
}
Err(e) => {
log::error!("Failed to receive relay list event: {e}");
}
if let Ok(event) = res {
return Ok(event);
}
}
Ok(RelayState::NotConfigured)
Err(anyhow!("No event found"))
})
}
/// Ensure write relays for a given public key
pub fn ensure_write_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
let public_key = *public_key;
cx.background_spawn(async move {
let mut relays = vec![];
let filter = Filter::new()
.kind(Kind::RelayList)
.author(public_key)
.limit(1);
// Construct target for subscription
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
.into_iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect();
if let Ok(mut stream) = client
.stream_events(target)
.timeout(Duration::from_secs(TIMEOUT))
.await
{
while let Some((_url, res)) = stream.next().await {
match res {
Ok(event) => {
// Extract relay urls
relays.extend(nip65::extract_owned_relay_list(event).filter_map(
|(url, metadata)| {
if metadata.is_none() || metadata == Some(RelayMetadata::Write)
{
Some(url)
} else {
None
}
},
));
// Ensure connections
for url in relays.iter() {
client.add_relay(url).and_connect().await.ok();
}
return relays;
}
Err(e) => {
log::error!("Failed to receive relay list event: {e}");
}
}
}
}
relays
})
}
/// Get a list of write relays for a given public key
pub fn write_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
let relays = self.gossip.read(cx).write_relays(public_key);
cx.background_spawn(async move {
// Ensure relay connections
for url in relays.iter() {
client.add_relay(url).and_connect().await.ok();
}
relays
})
}
/// Get a list of read relays for a given public key
pub fn read_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
let relays = self.gossip.read(cx).read_relays(public_key);
cx.background_spawn(async move {
// Ensure relay connections
for url in relays.iter() {
client.add_relay(url).and_connect().await.ok();
}
relays
})
}
/// Get all relays for a given public key without ensuring connections
pub fn read_only_relays(&self, public_key: &PublicKey, cx: &App) -> Vec<SharedString> {
self.gossip.read(cx).read_only_relays(public_key)
}
/// Get the public key of a NIP-05 address
pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
let client = self.client();
@@ -905,8 +765,6 @@ impl NostrRegistry {
}
}
impl EventEmitter<SignerEvent> for NostrRegistry {}
/// Get or create a new app keys
fn get_or_init_app_keys() -> Result<Keys, Error> {
let dir = config_dir().join(".app_keys");
@@ -932,52 +790,6 @@ fn get_or_init_app_keys() -> Result<Keys, Error> {
Ok(keys)
}
async fn get_events_for_room(client: &Client, nip65: &Event) -> Result<(), Error> {
// Subscription options
let opts = SubscribeAutoCloseOptions::default()
.timeout(Some(Duration::from_secs(TIMEOUT)))
.exit_policy(ReqExitPolicy::ExitOnEOSE);
// Extract write relays from event
let write_relays: Vec<&RelayUrl> = nip65::extract_relay_list(nip65)
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
Some(url)
} else {
None
}
})
.collect();
// Ensure relay connections
for url in write_relays.iter() {
client.add_relay(*url).and_connect().await.ok();
}
// Construct filter for inbox relays
let inbox = Filter::new()
.kind(Kind::InboxRelays)
.author(nip65.pubkey)
.limit(1);
// Construct filter for encryption announcement
let announcement = Filter::new()
.kind(Kind::Custom(10044))
.author(nip65.pubkey)
.limit(1);
// Construct target for subscription
let target: HashMap<&RelayUrl, Vec<Filter>> = write_relays
.into_iter()
.map(|relay| (relay, vec![inbox.clone(), announcement.clone()]))
.collect();
// Subscribe to inbox relays and encryption announcements
client.subscribe(target).close_on(opts).await?;
Ok(())
}
fn default_relay_list() -> Vec<(RelayUrl, Option<RelayMetadata>)> {
vec![
(
@@ -1011,43 +823,6 @@ fn default_messaging_relays() -> Vec<RelayUrl> {
]
}
/// Signer event.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum SignerEvent {
/// A new signer has been set
Set,
/// An error occurred
Error(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum RelayState {
#[default]
Idle,
Checking,
NotConfigured,
Configured,
}
impl RelayState {
pub fn idle(&self) -> bool {
matches!(self, RelayState::Idle)
}
pub fn checking(&self) -> bool {
matches!(self, RelayState::Checking)
}
pub fn not_configured(&self) -> bool {
matches!(self, RelayState::NotConfigured)
}
pub fn configured(&self) -> bool {
matches!(self, RelayState::Configured)
}
}
#[derive(Debug, Clone)]
pub struct CoopAuthUrlHandler;

View File

@@ -1,4 +1,4 @@
use gpui::{hsla, Hsla, Rgba};
use gpui::{Hsla, Rgba, hsla};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -30,6 +30,8 @@ pub struct ThemeColors {
pub text_muted: Hsla,
pub text_placeholder: Hsla,
pub text_accent: Hsla,
pub text_danger: Hsla,
pub text_warning: Hsla,
// Icon colors
pub icon: Hsla,
@@ -77,11 +79,11 @@ pub struct ThemeColors {
pub ghost_element_disabled: Hsla,
// Tab colors
pub tab_inactive_background: Hsla,
pub tab_inactive_foreground: Hsla,
pub tab_background: Hsla,
pub tab_foreground: Hsla,
pub tab_hover_background: Hsla,
pub tab_active_background: Hsla,
pub tab_active_foreground: Hsla,
pub tab_hover_foreground: Hsla,
// Scrollbar colors
pub scrollbar_thumb_background: Hsla,
@@ -110,8 +112,8 @@ impl ThemeColors {
elevated_surface_background: neutral().light().step_3(),
panel_background: neutral().light().step_1(),
overlay: neutral().light_alpha().step_3(),
title_bar: neutral().light().step_2(),
title_bar_inactive: neutral().light().step_3(),
title_bar: neutral().light().step_3(),
title_bar_inactive: neutral().light().step_1(),
window_border: hsl(240.0, 5.9, 78.0),
border: neutral().light().step_6(),
@@ -125,7 +127,9 @@ impl ThemeColors {
text: neutral().light().step_12(),
text_muted: neutral().light().step_11(),
text_placeholder: neutral().light().step_10(),
text_accent: brand().light().step_11(),
text_accent: brand().light().step_9(),
text_danger: danger().light().step_9(),
text_warning: warning().light().step_9(),
icon: neutral().light().step_11(),
icon_muted: neutral().light().step_10(),
@@ -166,17 +170,17 @@ impl ThemeColors {
ghost_element_selected: neutral().light().step_5(),
ghost_element_disabled: neutral().light_alpha().step_2(),
tab_inactive_background: neutral().light().step_2(),
tab_inactive_foreground: neutral().light().step_11(),
tab_background: neutral().light().step_3(),
tab_foreground: neutral().light().step_11(),
tab_hover_background: neutral().light_alpha().step_4(),
tab_active_background: neutral().light().step_1(),
tab_active_foreground: neutral().light().step_12(),
tab_hover_foreground: brand().light().step_9(),
scrollbar_thumb_background: neutral().light_alpha().step_3(),
scrollbar_thumb_hover_background: neutral().light_alpha().step_4(),
scrollbar_thumb_border: gpui::transparent_black(),
scrollbar_track_background: gpui::transparent_black(),
scrollbar_track_border: neutral().light().step_5(),
scrollbar_track_border: gpui::transparent_black(),
drop_target_background: brand().light_alpha().step_2(),
cursor: hsl(200., 100., 50.),
@@ -192,9 +196,9 @@ impl ThemeColors {
background: neutral().dark().step_1(),
surface_background: neutral().dark().step_2(),
elevated_surface_background: neutral().dark().step_3(),
panel_background: gpui::black(),
panel_background: neutral().dark().step_1(),
overlay: neutral().dark_alpha().step_3(),
title_bar: gpui::transparent_black(),
title_bar: neutral().dark().step_3(),
title_bar_inactive: neutral().dark().step_1(),
window_border: hsl(240.0, 3.7, 28.0),
@@ -209,7 +213,9 @@ impl ThemeColors {
text: neutral().dark().step_12(),
text_muted: neutral().dark().step_11(),
text_placeholder: neutral().dark().step_10(),
text_accent: brand().dark().step_11(),
text_accent: brand().dark().step_9(),
text_danger: danger().dark().step_9(),
text_warning: warning().dark().step_9(),
icon: neutral().dark().step_11(),
icon_muted: neutral().dark().step_10(),
@@ -250,17 +256,17 @@ impl ThemeColors {
ghost_element_selected: neutral().dark().step_5(),
ghost_element_disabled: neutral().dark_alpha().step_2(),
tab_inactive_background: neutral().dark().step_2(),
tab_inactive_foreground: neutral().dark().step_11(),
tab_active_background: neutral().dark().step_3(),
tab_background: neutral().dark().step_3(),
tab_foreground: neutral().dark().step_11(),
tab_hover_background: neutral().dark_alpha().step_4(),
tab_active_background: neutral().dark().step_1(),
tab_active_foreground: neutral().dark().step_12(),
tab_hover_foreground: brand().dark().step_9(),
scrollbar_thumb_background: neutral().dark_alpha().step_3(),
scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(),
scrollbar_thumb_border: gpui::transparent_black(),
scrollbar_track_background: gpui::transparent_black(),
scrollbar_track_border: neutral().dark().step_5(),
scrollbar_track_border: gpui::transparent_black(),
drop_target_background: brand().dark_alpha().step_2(),
cursor: hsl(200., 100., 50.),

View File

@@ -138,7 +138,7 @@ impl Anchor {
}
}
pub(crate) fn other_side_corner_along(&self, axis: Axis) -> Anchor {
pub fn other_side_corner_along(&self, axis: Axis) -> Anchor {
match axis {
Axis::Vertical => match self {
Self::TopLeft => Self::BottomLeft,

View File

@@ -4,6 +4,8 @@ use std::rc::Rc;
use gpui::{App, Global, Pixels, SharedString, Window, px};
mod colors;
mod geometry;
mod notification;
mod platform_kind;
mod registry;
mod scale;
@@ -11,6 +13,8 @@ mod scrollbar_mode;
mod theme;
pub use colors::*;
pub use geometry::*;
pub use notification::*;
pub use platform_kind::PlatformKind;
pub use registry::*;
pub use scale::*;
@@ -82,6 +86,9 @@ pub struct Theme {
/// Show the scrollbar mode, default: scrolling
pub scrollbar_mode: ScrollbarMode,
/// Notification settings
pub notification: NotificationSettings,
/// Platform kind
pub platform: PlatformKind,
}
@@ -200,10 +207,11 @@ impl From<ThemeFamily> for Theme {
Theme {
font_size: px(15.),
font_family: font_family.into(),
radius: px(5.),
radius: px(6.),
radius_lg: px(10.),
shadow: true,
scrollbar_mode: ScrollbarMode::default(),
notification: NotificationSettings::default(),
mode,
colors: *colors,
theme: Rc::new(family),

View File

@@ -0,0 +1,31 @@
use gpui::{Pixels, px};
use serde::{Deserialize, Serialize};
use crate::{Anchor, Edges, TITLEBAR_HEIGHT};
/// The settings for notifications.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationSettings {
/// The placement of the notification, default: [`Anchor::TopRight`]
pub placement: Anchor,
/// The margins of the notification with respect to the window edges.
pub margins: Edges<Pixels>,
/// The maximum number of notifications to show at once, default: 10
pub max_items: usize,
}
impl Default for NotificationSettings {
fn default() -> Self {
let offset = px(16.);
Self {
placement: Anchor::TopRight,
margins: Edges {
top: TITLEBAR_HEIGHT + offset, // avoid overlap with title bar
right: offset,
bottom: offset,
left: offset,
},
max_items: 10,
}
}
}

View File

@@ -1,13 +1,12 @@
//! This is a fork of gpui's anchored element that adds support for offsetting
//! https://github.com/zed-industries/zed/blob/b06f4088a3565c5e30663106ff79c1ced645d87a/crates/gpui/src/elements/anchored.rs
use gpui::{
point, px, AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, Half,
AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, Half,
InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
Window,
Window, point, px,
};
use smallvec::SmallVec;
use crate::Anchor;
use theme::Anchor;
/// The state that the anchored element element uses to track its children.
pub struct AnchoredState {

View File

@@ -1,12 +1,12 @@
use gpui::prelude::FluentBuilder;
use gpui::{
div, img, px, AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement,
Interactivity, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage,
Window,
AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement, Interactivity,
IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage, Window, div, img,
px,
};
use theme::ActiveTheme;
use crate::{Sizable, Size};
use crate::{Selectable, Sizable, Size};
/// Returns the size of the avatar based on the given [`Size`].
pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
@@ -37,6 +37,7 @@ pub struct Avatar {
style: StyleRefinement,
size: Size,
border_color: Option<Hsla>,
selected: bool,
}
impl Avatar {
@@ -48,6 +49,7 @@ impl Avatar {
style: StyleRefinement::default(),
size: Size::Medium,
border_color: None,
selected: false,
}
}
@@ -89,6 +91,17 @@ impl Styled for Avatar {
}
}
impl Selectable for Avatar {
fn is_selected(&self) -> bool {
self.selected
}
fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl InteractiveElement for Avatar {
fn interactivity(&mut self) -> &mut Interactivity {
self.base.interactivity()

View File

@@ -3,20 +3,26 @@ use std::sync::Arc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, App, AppContext, Axis, Context, Element, Entity, IntoElement, MouseMoveEvent,
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, Styled as _, WeakEntity,
Window,
App, AppContext, Axis, Context, Element, Empty, Entity, IntoElement, MouseMoveEvent,
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, StyleRefinement, Styled as _,
WeakEntity, Window, div, px,
};
use super::{DockArea, DockItem};
use crate::dock_area::panel::PanelView;
use crate::dock_area::tab_panel::TabPanel;
use crate::resizable::{resize_handle, PANEL_MIN_SIZE};
use crate::StyledExt;
use crate::dock::panel::PanelView;
use crate::dock::tab_panel::TabPanel;
use crate::resizable::{PANEL_MIN_SIZE, resize_handle};
#[derive(Clone, Render)]
#[derive(Clone)]
struct ResizePanel;
impl Render for ResizePanel {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
Empty
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DockPlacement {
Center,
@@ -321,6 +327,8 @@ impl Render for Dock {
return div();
}
let cache_style = StyleRefinement::default().absolute().size_full();
div()
.relative()
.overflow_hidden()
@@ -336,7 +344,7 @@ impl Render for Dock {
.map(|this| match &self.panel {
DockItem::Split { view, .. } => this.child(view.clone()),
DockItem::Tabs { view, .. } => this.child(view.clone()),
DockItem::Panel { view, .. } => this.child(view.clone().view()),
DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)),
})
.child(self.render_resize_handle(window, cx))
.child(DockElement {

View File

@@ -2,22 +2,24 @@ use std::sync::Arc;
use gpui::prelude::FluentBuilder;
use gpui::{
actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations,
Edges, Entity, EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement,
ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations, Edges, Entity,
EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div, px,
};
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
use crate::dock_area::dock::{Dock, DockPlacement};
use crate::dock_area::panel::{Panel, PanelEvent, PanelStyle, PanelView};
use crate::dock_area::stack_panel::StackPanel;
use crate::dock_area::tab_panel::TabPanel;
use crate::ElementExt;
pub mod dock;
pub mod panel;
pub mod stack_panel;
pub mod tab_panel;
#[allow(clippy::module_inception)]
mod dock;
mod panel;
mod stack_panel;
mod tab_panel;
pub use dock::*;
pub use panel::*;
pub use stack_panel::*;
pub use tab_panel::*;
actions!(dock, [ToggleZoom, ClosePanel]);

View File

@@ -1,5 +1,5 @@
use gpui::{
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Render,
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Render,
SharedString, Window,
};
@@ -21,12 +21,6 @@ pub enum PanelStyle {
TabBar,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TitleStyle {
pub background: Hsla,
pub foreground: Hsla,
}
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
/// The name of the panel used to serialize, deserialize and identify the panel.
///

View File

@@ -7,16 +7,16 @@ use gpui::{
Window,
};
use smallvec::SmallVec;
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
use theme::{ActiveTheme, AxisExt as _, CLIENT_SIDE_DECORATION_ROUNDING, Placement};
use super::{DockArea, PanelEvent};
use crate::dock_area::panel::{Panel, PanelView};
use crate::dock_area::tab_panel::TabPanel;
use crate::dock::panel::{Panel, PanelView};
use crate::dock::tab_panel::TabPanel;
use crate::h_flex;
use crate::resizable::{
resizable_panel, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
PANEL_MIN_SIZE,
PANEL_MIN_SIZE, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
resizable_panel,
};
use crate::{h_flex, AxisExt as _, Placement};
pub struct StackPanel {
pub(super) parent: Option<WeakEntity<StackPanel>>,

View File

@@ -2,22 +2,22 @@ use std::sync::Arc;
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent,
Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement,
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
StatefulInteractiveElement, Styled, WeakEntity, Window,
App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent, Empty, Entity,
EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, MouseButton,
ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
WeakEntity, Window, div, px, rems,
};
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TABBAR_HEIGHT};
use theme::{ActiveTheme, AxisExt, CLIENT_SIDE_DECORATION_ROUNDING, Placement, TABBAR_HEIGHT};
use crate::button::{Button, ButtonVariants as _};
use crate::dock_area::dock::DockPlacement;
use crate::dock_area::panel::{Panel, PanelView};
use crate::dock_area::stack_panel::StackPanel;
use crate::dock_area::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
use crate::dock::dock::DockPlacement;
use crate::dock::panel::{Panel, PanelView};
use crate::dock::stack_panel::StackPanel;
use crate::dock::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
use crate::menu::{DropdownMenu, PopupMenu};
use crate::tab::tab_bar::TabBar;
use crate::tab::Tab;
use crate::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
use crate::tab::tab_bar::TabBar;
use crate::{IconName, Selectable, Sizable, StyledExt, h_flex, v_flex};
#[derive(Clone)]
struct TabState {
@@ -42,22 +42,20 @@ impl DragPanel {
impl Render for DragPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
h_flex()
.id("drag-panel")
.cursor_grab()
.py_1()
.px_2()
.w_24()
.flex()
.items_center()
.p_2()
.min_w_24()
.justify_center()
.overflow_hidden()
.whitespace_nowrap()
.rounded(cx.theme().radius_lg)
.rounded(cx.theme().radius)
.text_xs()
.when(cx.theme().shadow, |this| this.shadow_lg())
.text_color(cx.theme().text)
.text_ellipsis()
.when(cx.theme().shadow, |this| this.shadow_xs())
.bg(cx.theme().background)
.text_color(cx.theme().text_accent)
.child(self.panel.title(cx))
}
}
@@ -425,14 +423,13 @@ impl TabPanel {
let view = cx.entity().clone();
let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx);
let toolbar = self.toolbar_buttons(window, cx);
let has_toolbar = !toolbar.is_empty();
h_flex()
.p_0p5()
.gap_1()
.gap_1p5()
.occlude()
.rounded_full()
.children(toolbar.into_iter().map(|btn| btn.small().ghost().rounded()))
.children(toolbar.into_iter().map(|btn| btn.small().ghost()))
.when(self.zoomed, |this| {
this.child(
Button::new("zoom")
@@ -445,15 +442,11 @@ impl TabPanel {
})),
)
})
.when(has_toolbar, |this| {
this.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
})
.child(
Button::new("menu")
.icon(IconName::Ellipsis)
.small()
.ghost()
.rounded()
.dropdown_menu({
let zoomable = state.zoomable;
let closable = state.closable;
@@ -578,6 +571,7 @@ impl TabPanel {
let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx);
let has_extend_dock_button = left_dock_button.is_some() || bottom_dock_button.is_some();
let tabs_count = self.panels.len();
let is_bottom_dock = bottom_dock_button.is_some();
if tabs_count == 1 && dock_area.read(cx).panel_style == PanelStyle::Default {
let panel = self.panels.first().unwrap();
@@ -646,7 +640,7 @@ impl TabPanel {
.into_any_element();
}
TabBar::new()
TabBar::new("tab-bar")
.track_scroll(&self.tab_bar_scroll_handle)
.h(TABBAR_HEIGHT)
.when(has_extend_dock_button, |this| {
@@ -659,8 +653,9 @@ impl TabPanel {
.border_b_1()
.h_full()
.border_color(cx.theme().border)
.bg(cx.theme().surface_background)
.px_2()
.bg(cx.theme().tab_background)
.pl_0p5()
.pr_1()
.children(left_dock_button)
.children(bottom_dock_button),
)
@@ -682,16 +677,43 @@ impl TabPanel {
Some(
Tab::new()
.ix(ix)
.label(panel.title(cx))
.py_2()
.tab_bar_prefix(has_extend_dock_button)
.child(panel.title(cx))
.selected(active)
.disabled(disabled)
.suffix(
Button::new("close-{ix}")
.icon(IconName::Close)
.tooltip("Close panel")
.ghost()
.xsmall()
.on_click(cx.listener({
let panel = panel.clone();
move |view, _ev, window, cx| {
view.remove_panel(&panel, window, cx);
}
})),
)
.on_click(cx.listener({
let is_collapsed = self.collapsed;
let dock_area = self.dock_area.clone();
move |view, _, window, cx| {
view.set_active_ix(ix, window, cx);
// Open dock if clicked on the collapsed bottom dock
if is_bottom_dock && is_collapsed {
_ = dock_area.update(cx, |dock_area, cx| {
dock_area.toggle_dock(DockPlacement::Bottom, window, cx);
});
}
}
}))
.when(!disabled, |this| {
this.on_mouse_down(
MouseButton::Middle,
cx.listener({
let panel = panel.clone();
move |view, _, window, cx| {
move |view, _ev, window, cx| {
view.remove_panel(&panel, window, cx);
}
}),
@@ -757,14 +779,15 @@ impl TabPanel {
this.suffix(
h_flex()
.items_center()
.px_2()
.gap_1()
.top_0()
.right_0()
.h_full()
.border_color(cx.theme().border)
.border_l_1()
.border_b_1()
.px_0p5()
.gap_1()
.border_color(cx.theme().border)
.bg(cx.theme().tab_background)
.child(self.render_toolbar(state, window, cx))
.when_some(right_dock_button, |this, btn| this.child(btn)),
)
@@ -1099,6 +1122,7 @@ impl Focusable for TabPanel {
}
impl EventEmitter<DismissEvent> for TabPanel {}
impl EventEmitter<PanelEvent> for TabPanel {}
impl Render for TabPanel {

View File

@@ -1,7 +1,7 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{
svg, AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render,
RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
SharedString, StyleRefinement, Styled, Svg, Transformation, Window, svg,
};
use theme::ActiveTheme;
@@ -39,6 +39,7 @@ pub enum IconName {
Ellipsis,
Emoji,
Eye,
Input,
Info,
Invite,
Inbox,
@@ -110,6 +111,7 @@ impl IconNamed for IconName {
Self::Ellipsis => "icons/ellipsis.svg",
Self::Emoji => "icons/emoji.svg",
Self::Eye => "icons/eye.svg",
Self::Input => "icons/input.svg",
Self::Info => "icons/info.svg",
Self::Invite => "icons/invite.svg",
Self::Inbox => "icons/inbox.svg",

View File

@@ -2,11 +2,10 @@ pub use anchored::*;
pub use element_ext::ElementExt;
pub use event::InteractiveElementExt;
pub use focusable::FocusableCycle;
pub use geometry::*;
pub use icon::*;
pub use index_path::IndexPath;
pub use kbd::*;
pub use root::{window_paddings, Root};
pub use root::{Root, window_paddings};
pub use styled::*;
pub use window_ext::*;
@@ -18,7 +17,7 @@ pub mod avatar;
pub mod button;
pub mod checkbox;
pub mod divider;
pub mod dock_area;
pub mod dock;
pub mod group_box;
pub mod history;
pub mod indicator;
@@ -39,7 +38,6 @@ mod anchored;
mod element_ext;
mod event;
mod focusable;
mod geometry;
mod icon;
mod index_path;
mod kbd;

View File

@@ -5,10 +5,11 @@ use gpui::{
RenderOnce, SharedString, StyleRefinement, Styled, Window,
};
use crate::Selectable;
use crate::avatar::Avatar;
use crate::button::Button;
use crate::menu::PopupMenu;
use crate::popover::Popover;
use crate::Selectable;
/// A dropdown menu trait for buttons and other interactive elements
pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + 'static {
@@ -35,6 +36,8 @@ pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement +
impl DropdownMenu for Button {}
impl DropdownMenu for Avatar {}
#[derive(IntoElement)]
pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
id: ElementId,

View File

@@ -2,19 +2,19 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
anchored, div, px, rems, Action, AnyElement, App, AppContext, Axis, Bounds, ClickEvent,
Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, Half,
InteractiveElement, IntoElement, KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement,
Pixels, Point, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
Subscription, WeakEntity, Window,
Action, AnyElement, App, AppContext, Axis, Bounds, ClickEvent, Context, Corner, DismissEvent,
Edges, Entity, EventEmitter, FocusHandle, Focusable, Half, InteractiveElement, IntoElement,
KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement, Pixels, Point, Render, ScrollHandle,
SharedString, StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, anchored,
div, px, rems,
};
use theme::ActiveTheme;
use theme::{ActiveTheme, Side};
use crate::actions::{Cancel, Confirm, SelectDown, SelectLeft, SelectRight, SelectUp};
use crate::kbd::Kbd;
use crate::menu::menu_item::MenuItemElement;
use crate::scroll::ScrollableElement;
use crate::{h_flex, v_flex, ElementExt, Icon, IconName, Side, Sizable as _, Size, StyledExt};
use crate::{ElementExt, Icon, IconName, Sizable as _, Size, StyledExt, h_flex, v_flex};
const CONTEXT: &str = "PopupMenu";

View File

@@ -3,10 +3,9 @@ use std::time::Duration;
use gpui::prelude::FluentBuilder;
use gpui::{
anchored, div, hsla, point, px, Animation, AnimationExt as _, AnyElement, App, Bounds,
BoxShadow, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, StyleRefinement, Styled,
Window,
Animation, AnimationExt as _, AnyElement, App, Bounds, BoxShadow, ClickEvent, Div, FocusHandle,
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
RenderOnce, SharedString, StyleRefinement, Styled, Window, anchored, div, hsla, point, px,
};
use theme::ActiveTheme;
@@ -14,7 +13,7 @@ use crate::actions::{Cancel, Confirm};
use crate::animation::cubic_bezier;
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
use crate::scroll::ScrollableElement;
use crate::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension};
use crate::{IconName, Root, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
const CONTEXT: &str = "Modal";
@@ -500,6 +499,7 @@ impl RenderOnce for Modal {
.child(self.content),
),
)
.when_none(&self.footer, |this| this.child(div().pt(padding_left)))
.when_some(self.footer, |this, footer| {
this.child(
h_flex()

View File

@@ -1,25 +1,23 @@
use std::any::TypeId;
use std::borrow::Cow;
use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
use std::time::Duration;
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context,
DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
ParentElement as _, Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
Subscription, Window,
Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context, DismissEvent,
ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _,
Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription,
Window, div, px, relative,
};
use smol::Timer;
use theme::ActiveTheme;
use theme::{ActiveTheme, Anchor};
use crate::animation::cubic_bezier;
use crate::button::{Button, ButtonVariants as _};
use crate::{h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt};
use crate::{Icon, IconName, Sizable as _, StyledExt, h_flex, v_flex};
#[derive(Debug, Clone, Copy, Default)]
pub enum NotificationType {
pub enum NotificationKind {
#[default]
Info,
Success,
@@ -27,13 +25,15 @@ pub enum NotificationType {
Error,
}
impl NotificationType {
impl NotificationKind {
fn icon(&self, cx: &App) -> Icon {
match self {
Self::Info => Icon::new(IconName::Info).text_color(cx.theme().element_foreground),
Self::Success => Icon::new(IconName::Info).text_color(cx.theme().secondary_foreground),
Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().warning_foreground),
Self::Error => Icon::new(IconName::Warning).text_color(cx.theme().danger_foreground),
Self::Info => Icon::new(IconName::Info).text_color(cx.theme().icon),
Self::Success => Icon::new(IconName::CheckCircle).text_color(cx.theme().icon_accent),
Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().text_warning),
Self::Error => {
Icon::new(IconName::CloseCircle).text_color(cx.theme().danger_foreground)
}
}
}
}
@@ -56,6 +56,7 @@ impl From<(TypeId, ElementId)> for NotificationId {
}
}
#[allow(clippy::type_complexity)]
/// A notification element.
pub struct Notification {
/// The id is used make the notification unique.
@@ -64,16 +65,13 @@ pub struct Notification {
/// None means the notification will be added to the end of the list.
id: NotificationId,
style: StyleRefinement,
type_: Option<NotificationType>,
kind: Option<NotificationKind>,
title: Option<SharedString>,
message: Option<SharedString>,
icon: Option<Icon>,
autohide: bool,
#[allow(clippy::type_complexity)]
action_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> Button>>,
#[allow(clippy::type_complexity)]
content_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>>,
#[allow(clippy::type_complexity)]
action_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button>>,
content_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement>>,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
closing: bool,
}
@@ -84,12 +82,6 @@ impl From<String> for Notification {
}
}
impl From<Cow<'static, str>> for Notification {
fn from(s: Cow<'static, str>) -> Self {
Self::new().message(s)
}
}
impl From<SharedString> for Notification {
fn from(s: SharedString) -> Self {
Self::new().message(s)
@@ -102,24 +94,24 @@ impl From<&'static str> for Notification {
}
}
impl From<(NotificationType, &'static str)> for Notification {
fn from((type_, content): (NotificationType, &'static str)) -> Self {
Self::new().message(content).with_type(type_)
impl From<(NotificationKind, &'static str)> for Notification {
fn from((kind, content): (NotificationKind, &'static str)) -> Self {
Self::new().message(content).with_kind(kind)
}
}
impl From<(NotificationType, SharedString)> for Notification {
fn from((type_, content): (NotificationType, SharedString)) -> Self {
Self::new().message(content).with_type(type_)
impl From<(NotificationKind, SharedString)> for Notification {
fn from((kind, content): (NotificationKind, SharedString)) -> Self {
Self::new().message(content).with_kind(kind)
}
}
struct DefaultIdType;
impl Notification {
/// Create a new notification with the given content.
/// Create a new notification.
///
/// default width is 320px.
/// The default id is a random UUID.
pub fn new() -> Self {
let id: SharedString = uuid::Uuid::new_v4().to_string().into();
let id = (TypeId::of::<DefaultIdType>(), id.into());
@@ -129,7 +121,7 @@ impl Notification {
style: StyleRefinement::default(),
title: None,
message: None,
type_: None,
kind: None,
icon: None,
autohide: true,
action_builder: None,
@@ -139,33 +131,38 @@ impl Notification {
}
}
/// Set the message of the notification, default is None.
pub fn message(mut self, message: impl Into<SharedString>) -> Self {
self.message = Some(message.into());
self
}
/// Create an info notification with the given message.
pub fn info(message: impl Into<SharedString>) -> Self {
Self::new()
.message(message)
.with_type(NotificationType::Info)
.with_kind(NotificationKind::Info)
}
/// Create a success notification with the given message.
pub fn success(message: impl Into<SharedString>) -> Self {
Self::new()
.message(message)
.with_type(NotificationType::Success)
.with_kind(NotificationKind::Success)
}
/// Create a warning notification with the given message.
pub fn warning(message: impl Into<SharedString>) -> Self {
Self::new()
.message(message)
.with_type(NotificationType::Warning)
.with_kind(NotificationKind::Warning)
}
/// Create an error notification with the given message.
pub fn error(message: impl Into<SharedString>) -> Self {
Self::new()
.message(message)
.with_type(NotificationType::Error)
.with_kind(NotificationKind::Error)
}
/// Set the type for unique identification of the notification.
@@ -180,8 +177,8 @@ impl Notification {
}
/// Set the type and id of the notification, used to uniquely identify the notification.
pub fn custom_id(mut self, key: impl Into<ElementId>) -> Self {
self.id = (TypeId::of::<DefaultIdType>(), key.into()).into();
pub fn type_id<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self {
self.id = (TypeId::of::<T>(), key.into()).into();
self
}
@@ -202,8 +199,8 @@ impl Notification {
}
/// Set the type of the notification, default is NotificationType::Info.
pub fn with_type(mut self, type_: NotificationType) -> Self {
self.type_ = Some(type_);
pub fn with_kind(mut self, kind: NotificationKind) -> Self {
self.kind = Some(kind);
self
}
@@ -223,22 +220,31 @@ impl Notification {
}
/// Set the action button of the notification.
///
/// When an action is set, the notification will not autohide.
pub fn action<F>(mut self, action: F) -> Self
where
F: Fn(&mut Window, &mut Context<Self>) -> Button + 'static,
F: Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button + 'static,
{
self.action_builder = Some(Rc::new(action));
self.autohide = false;
self
}
/// Dismiss the notification.
pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context<Self>) {
if self.closing {
return;
}
self.closing = true;
cx.notify();
// Dismiss the notification after 0.15s to show the animation.
cx.spawn(async move |view, cx| {
Timer::after(Duration::from_secs_f32(0.15)).await;
cx.background_executor()
.timer(Duration::from_secs_f32(0.15))
.await;
cx.update(|cx| {
if let Some(view) = view.upgrade() {
view.update(cx, |view, cx| {
@@ -248,13 +254,13 @@ impl Notification {
}
})
})
.detach()
.detach();
}
/// Set the content of the notification.
pub fn content(
mut self,
content: impl Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static,
content: impl Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement + 'static,
) -> Self {
self.content_builder = Some(Rc::new(content));
self
@@ -276,57 +282,76 @@ impl Styled for Notification {
&mut self.style
}
}
impl Render for Notification {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let closing = self.closing;
let icon = match self.type_ {
let placement = cx.theme().notification.placement;
let content = self
.content_builder
.clone()
.map(|builder| builder(self, window, cx));
let action = self
.action_builder
.clone()
.map(|builder| builder(self, window, cx).small().mr_3p5());
let icon = match self.kind {
None => self.icon.clone(),
Some(type_) => Some(type_.icon(cx)),
Some(kind) => Some(kind.icon(cx)),
};
let background = match self.kind {
Some(NotificationKind::Error) => cx.theme().danger_background,
_ => cx.theme().surface_background,
};
let text_color = match self.kind {
Some(NotificationKind::Error) => cx.theme().danger_foreground,
_ => cx.theme().text,
};
h_flex()
.id("notification")
.refine_style(&self.style)
.group("")
.occlude()
.relative()
.w_96()
.w_112()
.border_1()
.border_color(cx.theme().border)
.bg(cx.theme().surface_background)
.bg(background)
.text_color(text_color)
.rounded(cx.theme().radius_lg)
.when(cx.theme().shadow, |this| this.shadow_md())
.p_2()
.gap_3()
.gap_2()
.justify_start()
.items_start()
.refine_style(&self.style)
.when_some(icon, |this, icon| {
this.child(div().flex_shrink_0().pt_1().child(icon))
this.child(div().flex_shrink_0().child(icon))
})
.child(
v_flex()
.flex_1()
.gap_1()
.overflow_hidden()
.when_some(self.title.clone(), |this, title| {
this.child(div().text_sm().font_semibold().child(title))
})
.when_some(self.message.clone(), |this, message| {
this.child(div().text_sm().child(message))
this.child(div().text_sm().line_height(relative(1.25)).child(message))
})
.when_some(self.content_builder.clone(), |this, child_builder| {
this.child(child_builder(window, cx))
})
.when_some(self.action_builder.clone(), |this, action_builder| {
this.child(action_builder(window, cx).small().w_full().my_2())
.when_some(content, |this, content| this.child(content))
.when_some(action, |this, action| {
this.child(h_flex().flex_1().gap_1().justify_end().child(action))
}),
)
.child(
div()
.absolute()
.top_2p5()
.right_2p5()
.top(px(6.5))
.right(px(6.5))
.invisible()
.group_hover("", |this| this.visible())
.child(
@@ -334,7 +359,7 @@ impl Render for Notification {
.icon(IconName::Close)
.ghost()
.xsmall()
.on_click(cx.listener(|this, _, window, cx| {
.on_click(cx.listener(move |this, _ev, window, cx| {
this.dismiss(window, cx);
})),
),
@@ -345,21 +370,47 @@ impl Render for Notification {
on_click(event, window, cx);
}))
})
.on_aux_click(cx.listener(move |view, event: &ClickEvent, window, cx| {
if event.is_middle_click() {
view.dismiss(window, cx);
}
}))
.with_animation(
ElementId::NamedInteger("slide-down".into(), closing as u64),
Animation::new(Duration::from_secs_f64(0.25))
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
move |this, delta| {
if closing {
let x_offset = px(0.) + delta * px(45.);
let opacity = 1. - delta;
this.left(px(0.) + x_offset)
let that = this
.shadow_none()
.opacity(opacity)
.when(opacity < 0.85, |this| this.shadow_none())
.when(opacity < 0.85, |this| this.shadow_none());
match placement {
Anchor::TopRight | Anchor::BottomRight => {
let x_offset = px(0.) + delta * px(45.);
that.left(px(0.) + x_offset)
}
Anchor::TopLeft | Anchor::BottomLeft => {
let x_offset = px(0.) - delta * px(45.);
that.left(px(0.) + x_offset)
}
Anchor::TopCenter => {
let y_offset = px(0.) - delta * px(45.);
that.top(px(0.) + y_offset)
}
Anchor::BottomCenter => {
let y_offset = px(0.) + delta * px(45.);
that.top(px(0.) + y_offset)
}
}
} else {
let y_offset = px(-45.) + delta * px(45.);
let opacity = delta;
let y_offset = match placement {
placement if placement.is_top() => px(-45.) + delta * px(45.),
placement if placement.is_bottom() => px(45.) - delta * px(45.),
_ => px(0.),
};
this.top(px(0.) + y_offset)
.opacity(opacity)
.when(opacity < 0.85, |this| this.shadow_none())
@@ -373,7 +424,11 @@ impl Render for Notification {
pub struct NotificationList {
/// Notifications that will be auto hidden.
pub(crate) notifications: VecDeque<Entity<Notification>>,
/// Whether the notification list is expanded.
expanded: bool,
/// Subscriptions
_subscriptions: HashMap<NotificationId, Subscription>,
}
@@ -386,10 +441,12 @@ impl NotificationList {
}
}
pub fn push<T>(&mut self, notification: T, window: &mut Window, cx: &mut Context<Self>)
where
T: Into<Notification>,
{
pub fn push(
&mut self,
notification: impl Into<Notification>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let notification = notification.into();
let id = notification.id.clone();
let autohide = notification.autohide;
@@ -411,36 +468,35 @@ impl NotificationList {
if autohide {
// Sleep for 5 seconds to autohide the notification
cx.spawn_in(window, async move |_, cx| {
Timer::after(Duration::from_secs(5)).await;
cx.spawn_in(window, async move |_this, cx| {
cx.background_executor().timer(Duration::from_secs(5)).await;
if let Err(error) =
if let Err(err) =
notification.update_in(cx, |note, window, cx| note.dismiss(window, cx))
{
log::error!("Failed to auto hide notification: {error}");
log::error!("failed to auto hide notification: {:?}", err);
}
})
.detach();
}
cx.notify();
}
pub fn close<T>(&mut self, key: T, window: &mut Window, cx: &mut Context<Self>)
where
T: Into<ElementId>,
{
let id = (TypeId::of::<DefaultIdType>(), key.into()).into();
pub(crate) fn close(
&mut self,
id: impl Into<NotificationId>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let id: NotificationId = id.into();
if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) {
n.update(cx, |note, cx| {
note.dismiss(window, cx);
});
n.update(cx, |note, cx| note.dismiss(window, cx))
}
cx.notify();
}
pub fn clear(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
pub fn clear(&mut self, _: &mut Window, cx: &mut Context<Self>) {
self.notifications.clear();
cx.notify();
}
@@ -451,25 +507,46 @@ impl NotificationList {
}
impl Render for NotificationList {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(
&mut self,
window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
let size = window.viewport_size();
let items = self.notifications.iter().rev().take(10).rev().cloned();
div()
.id("notification-wrapper")
.absolute()
.top_4()
.right_4()
.child(
let placement = cx.theme().notification.placement;
let margins = &cx.theme().notification.margins;
v_flex()
.id("notification-list")
.h(size.height - px(8.))
.max_h(size.height)
.pt(margins.top)
.pb(margins.bottom)
.gap_3()
.children(items)
.when(
matches!(placement, Anchor::TopRight),
|this| this.pr(margins.right), // ignore left
)
.when(
matches!(placement, Anchor::TopLeft),
|this| this.pl(margins.left), // ignore right
)
.when(
matches!(placement, Anchor::BottomLeft),
|this| this.flex_col_reverse().pl(margins.left), // ignore right
)
.when(
matches!(placement, Anchor::BottomRight),
|this| this.flex_col_reverse().pr(margins.right), // ignore left
)
.when(matches!(placement, Anchor::BottomCenter), |this| {
this.flex_col_reverse()
})
.on_hover(cx.listener(|view, hovered, _, cx| {
view.expanded = *hovered;
cx.notify()
})),
)
}))
.children(items)
}
}

View File

@@ -2,14 +2,15 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
deferred, div, px, AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId,
EventEmitter, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding,
MouseButton, ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement,
Styled, Subscription, Window,
AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId, EventEmitter,
FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement, Styled,
Subscription, Window, deferred, div, px,
};
use theme::Anchor;
use crate::actions::Cancel;
use crate::{anchored, v_flex, Anchor, ElementExt, Selectable, StyledExt as _};
use crate::{ElementExt, Selectable, StyledExt as _, anchored, v_flex};
const CONTEXT: &str = "Popover";

View File

@@ -3,14 +3,15 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
div, Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty,
Entity, EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent,
MouseUpEvent, ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window,
Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, Entity,
EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, MouseUpEvent,
ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window, div,
};
use theme::AxisExt;
use super::{resizable_panel, resize_handle, ResizableState};
use super::{ResizableState, resizable_panel, resize_handle};
use crate::resizable::PANEL_MIN_SIZE;
use crate::{h_flex, v_flex, AxisExt, ElementExt};
use crate::{ElementExt, h_flex, v_flex};
pub enum ResizablePanelEvent {
Resized,

View File

@@ -3,14 +3,13 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId,
InteractiveElement, IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels,
Point, Render, StatefulInteractiveElement, Styled as _, Window,
AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, InteractiveElement,
IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
StatefulInteractiveElement, Styled as _, Window, div, px,
};
use theme::ActiveTheme;
use theme::{ActiveTheme, AxisExt};
use crate::dock_area::dock::DockPlacement;
use crate::AxisExt;
use crate::dock::DockPlacement;
pub(crate) const HANDLE_PADDING: Pixels = px(4.);
pub(crate) const HANDLE_SIZE: Pixels = px(1.);

View File

@@ -1,11 +1,12 @@
use std::any::TypeId;
use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, Edges, Entity,
AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, Edges, ElementId, Entity,
FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement as _, Pixels, Point, Render, ResizeEdge, SharedString, Size, Styled, Tiling,
WeakFocusHandle, Window, canvas, div, point, px, size,
ParentElement as _, Pixels, Point, Render, ResizeEdge, Size, Styled, Tiling, WeakFocusHandle,
Window, canvas, div, point, px, size,
};
use theme::{
ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING,
@@ -213,13 +214,30 @@ impl Root {
cx.notify();
}
/// Clear a notification by its ID.
pub fn clear_notification<T>(&mut self, id: T, window: &mut Window, cx: &mut Context<Self>)
where
T: Into<SharedString>,
{
self.notification
.update(cx, |view, cx| view.close(id.into(), window, cx));
/// Clear a notification by its type.
pub fn clear_notification<T: Sized + 'static>(
&mut self,
window: &mut Window,
cx: &mut Context<'_, Root>,
) {
self.notification.update(cx, |view, cx| {
let id = TypeId::of::<T>();
view.close(id, window, cx);
});
cx.notify();
}
/// Clear a notification by its type.
pub fn clear_notification_by_id<T: Sized + 'static>(
&mut self,
key: impl Into<ElementId>,
window: &mut Window,
cx: &mut Context<'_, Root>,
) {
self.notification.update(cx, |view, cx| {
let id = (TypeId::of::<T>(), key.into());
view.close(id, window, cx);
});
cx.notify();
}

View File

@@ -1,10 +1,9 @@
use gpui::{
px, relative, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId,
EntityId, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels,
Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window,
App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId, EntityId,
GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point,
Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window, px, relative,
};
use crate::AxisExt;
use theme::AxisExt;
/// Make a scrollable mask element to cover the parent view with the mouse wheel event listening.
///

View File

@@ -11,9 +11,7 @@ use gpui::{
Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, fill,
point, px, relative, size,
};
use theme::{ActiveTheme, ScrollbarMode};
use crate::AxisExt;
use theme::{ActiveTheme, AxisExt, ScrollbarMode};
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
const WIDTH: Pixels = px(1. * 2. + 8.);
@@ -54,7 +52,7 @@ impl ScrollbarHandle for ScrollHandle {
}
fn content_size(&self) -> Size<Pixels> {
self.max_offset() + self.bounds().size
Size::from(self.max_offset()) + self.bounds().size
}
}
@@ -69,7 +67,7 @@ impl ScrollbarHandle for UniformListScrollHandle {
fn content_size(&self) -> Size<Pixels> {
let base_handle = &self.0.borrow().base_handle;
base_handle.max_offset() + base_handle.bounds().size
Size::from(base_handle.max_offset()) + base_handle.bounds().size
}
}
@@ -83,7 +81,7 @@ impl ScrollbarHandle for ListState {
}
fn content_size(&self) -> Size<Pixels> {
self.viewport_bounds().size + self.max_offset_for_scrollbar()
Size::from(self.max_offset_for_scrollbar()) + self.viewport_bounds().size
}
fn start_drag(&self) {

View File

@@ -1,4 +1,4 @@
use gpui::{div, px, App, Div, Pixels, Refineable, StyleRefinement, Styled};
use gpui::{App, DefiniteLength, Div, Edges, Pixels, Refineable, StyleRefinement, Styled, div, px};
use serde::{Deserialize, Serialize};
use theme::ActiveTheme;
@@ -46,6 +46,30 @@ pub trait StyledExt: Styled + Sized {
self.flex().flex_col()
}
/// Apply paddings to the element.
fn paddings<L>(self, paddings: impl Into<Edges<L>>) -> Self
where
L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
{
let paddings = paddings.into();
self.pt(paddings.top.into())
.pb(paddings.bottom.into())
.pl(paddings.left.into())
.pr(paddings.right.into())
}
/// Apply margins to the element.
fn margins<L>(self, margins: impl Into<Edges<L>>) -> Self
where
L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
{
let margins = margins.into();
self.mt(margins.top.into())
.mb(margins.bottom.into())
.ml(margins.left.into())
.mr(margins.right.into())
}
font_weight!(font_thin, THIN);
font_weight!(font_extralight, EXTRA_LIGHT);
font_weight!(font_light, LIGHT);

View File

@@ -4,13 +4,13 @@ use std::time::Duration;
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, white, Animation, AnimationExt as _, AnyElement, App, Element, ElementId,
GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString,
Styled as _, Window,
Animation, AnimationExt as _, AnyElement, App, Element, ElementId, GlobalElementId,
InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, Styled as _,
Window, div, px, white,
};
use theme::ActiveTheme;
use theme::{ActiveTheme, Side};
use crate::{Disableable, Side, Sizable, Size};
use crate::{Disableable, Sizable, Size};
type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;

View File

@@ -1,74 +1,557 @@
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
RenderOnce, StatefulInteractiveElement, Styled, Window,
};
use theme::{ActiveTheme, TABBAR_HEIGHT};
use std::rc::Rc;
use crate::{Selectable, Sizable, Size};
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, ClickEvent, Div, Edges, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
div, px, relative,
};
use theme::ActiveTheme;
use crate::{Icon, IconName, Selectable, Sizable, Size, StyledExt, h_flex};
pub mod tab_bar;
/// Tab variants.
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
pub enum TabVariant {
#[default]
Tab,
Outline,
Pill,
Segmented,
Underline,
}
impl TabVariant {
fn height(&self, size: Size) -> Pixels {
match size {
Size::XSmall => match self {
TabVariant::Underline => px(26.),
_ => px(20.),
},
Size::Small => match self {
TabVariant::Underline => px(30.),
_ => px(24.),
},
Size::Large => match self {
TabVariant::Underline => px(44.),
_ => px(36.),
},
_ => match self {
TabVariant::Underline => px(36.),
_ => px(32.),
},
}
}
fn inner_height(&self, size: Size) -> Pixels {
match size {
Size::XSmall => match self {
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(18.),
TabVariant::Segmented => px(16.),
TabVariant::Underline => px(20.),
},
Size::Small => match self {
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(22.),
TabVariant::Segmented => px(18.),
TabVariant::Underline => px(22.),
},
Size::Large => match self {
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(36.),
TabVariant::Segmented => px(28.),
TabVariant::Underline => px(32.),
},
_ => match self {
TabVariant::Tab => px(30.),
TabVariant::Outline | TabVariant::Pill => px(26.),
TabVariant::Segmented => px(24.),
TabVariant::Underline => px(26.),
},
}
}
/// Default px(12) to match panel px_3, See [`crate::dock::TabPanel`]
fn inner_paddings(&self, size: Size) -> Edges<Pixels> {
let mut padding_x = match size {
Size::XSmall => px(8.),
Size::Small => px(10.),
Size::Large => px(16.),
_ => px(12.),
};
if matches!(self, TabVariant::Underline) {
padding_x = px(0.);
}
Edges {
left: padding_x,
right: padding_x,
..Default::default()
}
}
fn inner_margins(&self, size: Size) -> Edges<Pixels> {
match size {
Size::XSmall => match self {
TabVariant::Underline => Edges {
top: px(1.),
bottom: px(2.),
..Default::default()
},
_ => Edges::all(px(0.)),
},
Size::Small => match self {
TabVariant::Underline => Edges {
top: px(2.),
bottom: px(3.),
..Default::default()
},
_ => Edges::all(px(0.)),
},
Size::Large => match self {
TabVariant::Underline => Edges {
top: px(5.),
bottom: px(6.),
..Default::default()
},
_ => Edges::all(px(0.)),
},
_ => match self {
TabVariant::Underline => Edges {
top: px(3.),
bottom: px(4.),
..Default::default()
},
_ => Edges::all(px(0.)),
},
}
}
fn normal(&self, cx: &App) -> TabStyle {
match self {
TabVariant::Tab => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
borders: Edges {
left: px(1.),
right: px(1.),
..Default::default()
},
border_color: gpui::transparent_black(),
..Default::default()
},
TabVariant::Outline => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
borders: Edges::all(px(1.)),
border_color: cx.theme().border,
..Default::default()
},
TabVariant::Pill => TabStyle {
fg: cx.theme().text,
bg: gpui::transparent_black(),
..Default::default()
},
TabVariant::Segmented => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
..Default::default()
},
TabVariant::Underline => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
inner_bg: gpui::transparent_black(),
borders: Edges {
bottom: px(2.),
..Default::default()
},
border_color: gpui::transparent_black(),
..Default::default()
},
}
}
fn hovered(&self, selected: bool, cx: &App) -> TabStyle {
match self {
TabVariant::Tab => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
borders: Edges {
left: px(1.),
right: px(1.),
..Default::default()
},
border_color: gpui::transparent_black(),
..Default::default()
},
TabVariant::Outline => TabStyle {
fg: cx.theme().secondary_foreground,
bg: cx.theme().secondary_hover,
borders: Edges::all(px(1.)),
border_color: cx.theme().border,
..Default::default()
},
TabVariant::Pill => TabStyle {
fg: cx.theme().secondary_foreground,
bg: cx.theme().secondary_background,
..Default::default()
},
TabVariant::Segmented => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
inner_bg: if selected {
cx.theme().background
} else {
gpui::transparent_black()
},
..Default::default()
},
TabVariant::Underline => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
inner_bg: gpui::transparent_black(),
borders: Edges {
bottom: px(2.),
..Default::default()
},
border_color: gpui::transparent_black(),
..Default::default()
},
}
}
fn selected(&self, cx: &App) -> TabStyle {
match self {
TabVariant::Tab => TabStyle {
fg: cx.theme().tab_active_foreground,
bg: cx.theme().tab_active_background,
borders: Edges {
left: px(1.),
right: px(1.),
..Default::default()
},
border_color: cx.theme().border,
..Default::default()
},
TabVariant::Outline => TabStyle {
fg: cx.theme().text_accent,
bg: gpui::transparent_black(),
borders: Edges::all(px(1.)),
border_color: cx.theme().element_active,
..Default::default()
},
TabVariant::Pill => TabStyle {
fg: cx.theme().element_foreground,
bg: cx.theme().element_background,
..Default::default()
},
TabVariant::Segmented => TabStyle {
fg: cx.theme().tab_active_foreground,
bg: gpui::transparent_black(),
inner_bg: cx.theme().background,
shadow: true,
..Default::default()
},
TabVariant::Underline => TabStyle {
fg: cx.theme().tab_active_foreground,
bg: gpui::transparent_black(),
borders: Edges {
bottom: px(2.),
..Default::default()
},
border_color: cx.theme().element_active,
..Default::default()
},
}
}
fn disabled(&self, selected: bool, cx: &App) -> TabStyle {
match self {
TabVariant::Tab => TabStyle {
fg: cx.theme().text_muted,
bg: gpui::transparent_black(),
border_color: if selected {
cx.theme().border
} else {
gpui::transparent_black()
},
borders: Edges {
left: px(1.),
right: px(1.),
..Default::default()
},
..Default::default()
},
TabVariant::Outline => TabStyle {
fg: cx.theme().text_muted,
bg: gpui::transparent_black(),
borders: Edges::all(px(1.)),
border_color: if selected {
cx.theme().element_active
} else {
cx.theme().border
},
..Default::default()
},
TabVariant::Pill => TabStyle {
fg: if selected {
cx.theme().element_foreground.opacity(0.5)
} else {
cx.theme().text_muted
},
bg: if selected {
cx.theme().element_background.opacity(0.5)
} else {
gpui::transparent_black()
},
..Default::default()
},
TabVariant::Segmented => TabStyle {
fg: cx.theme().text_muted,
bg: cx.theme().tab_background,
inner_bg: if selected {
cx.theme().background
} else {
gpui::transparent_black()
},
..Default::default()
},
TabVariant::Underline => TabStyle {
fg: cx.theme().text_muted,
bg: gpui::transparent_black(),
border_color: if selected {
cx.theme().border
} else {
gpui::transparent_black()
},
borders: Edges {
bottom: px(2.),
..Default::default()
},
..Default::default()
},
}
}
pub(super) fn tab_bar_radius(&self, size: Size, cx: &App) -> Pixels {
if *self != TabVariant::Segmented {
return px(0.);
}
match size {
Size::XSmall | Size::Small => cx.theme().radius,
Size::Large => cx.theme().radius_lg,
_ => cx.theme().radius_lg,
}
}
fn radius(&self, size: Size, cx: &App) -> Pixels {
match self {
TabVariant::Outline | TabVariant::Pill => px(99.),
TabVariant::Segmented => match size {
Size::XSmall | Size::Small => cx.theme().radius,
Size::Large => cx.theme().radius_lg,
_ => cx.theme().radius_lg,
},
_ => px(0.),
}
}
fn inner_radius(&self, size: Size, cx: &App) -> Pixels {
match self {
TabVariant::Segmented => match size {
Size::Large => self.tab_bar_radius(size, cx) - px(3.),
_ => self.tab_bar_radius(size, cx) - px(2.),
},
_ => px(0.),
}
}
}
#[allow(dead_code)]
struct TabStyle {
borders: Edges<Pixels>,
border_color: Hsla,
bg: Hsla,
fg: Hsla,
shadow: bool,
inner_bg: Hsla,
}
impl Default for TabStyle {
fn default() -> Self {
TabStyle {
borders: Edges::all(px(0.)),
border_color: gpui::transparent_white(),
bg: gpui::transparent_white(),
fg: gpui::transparent_white(),
shadow: false,
inner_bg: gpui::transparent_white(),
}
}
}
#[allow(clippy::type_complexity)]
/// A Tab element for the [`super::TabBar`].
#[derive(IntoElement)]
pub struct Tab {
ix: usize,
base: Div,
label: Option<AnyElement>,
pub(super) label: Option<SharedString>,
icon: Option<Icon>,
prefix: Option<AnyElement>,
pub(super) tab_bar_prefix: Option<bool>,
suffix: Option<AnyElement>,
disabled: bool,
selected: bool,
children: Vec<AnyElement>,
variant: TabVariant,
size: Size,
pub(super) disabled: bool,
pub(super) selected: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
}
impl Tab {
pub fn new() -> Self {
Self {
ix: 0,
base: div(),
label: None,
disabled: false,
selected: false,
prefix: None,
suffix: None,
size: Size::default(),
}
impl From<&'static str> for Tab {
fn from(label: &'static str) -> Self {
Self::new().label(label)
}
}
/// Set label for the tab.
pub fn label(mut self, label: impl Into<AnyElement>) -> Self {
self.label = Some(label.into());
self
impl From<String> for Tab {
fn from(label: String) -> Self {
Self::new().label(label)
}
}
/// Set the left side of the tab
pub fn prefix(mut self, prefix: impl Into<AnyElement>) -> Self {
self.prefix = Some(prefix.into());
self
impl From<SharedString> for Tab {
fn from(label: SharedString) -> Self {
Self::new().label(label)
}
}
/// Set the right side of the tab
pub fn suffix(mut self, suffix: impl Into<AnyElement>) -> Self {
self.suffix = Some(suffix.into());
self
impl From<Icon> for Tab {
fn from(icon: Icon) -> Self {
Self::default().icon(icon)
}
}
/// Set disabled state to the tab
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Set index to the tab.
pub fn ix(mut self, ix: usize) -> Self {
self.ix = ix;
self
impl From<IconName> for Tab {
fn from(icon_name: IconName) -> Self {
Self::default().icon(Icon::new(icon_name))
}
}
impl Default for Tab {
fn default() -> Self {
Self::new()
Self {
ix: 0,
base: div(),
label: None,
icon: None,
tab_bar_prefix: None,
children: Vec::new(),
disabled: false,
selected: false,
prefix: None,
suffix: None,
variant: TabVariant::default(),
size: Size::default(),
on_click: None,
}
}
}
impl Tab {
/// Create a new tab with a label.
pub fn new() -> Self {
Self::default()
}
/// Set label for the tab.
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
/// Set icon for the tab.
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(icon.into());
self
}
/// Set Tab Variant.
pub fn with_variant(mut self, variant: TabVariant) -> Self {
self.variant = variant;
self
}
/// Use Pill variant.
pub fn pill(mut self) -> Self {
self.variant = TabVariant::Pill;
self
}
/// Use outline variant.
pub fn outline(mut self) -> Self {
self.variant = TabVariant::Outline;
self
}
/// Use Segmented variant.
pub fn segmented(mut self) -> Self {
self.variant = TabVariant::Segmented;
self
}
/// Use Underline variant.
pub fn underline(mut self) -> Self {
self.variant = TabVariant::Underline;
self
}
/// Set the left side of the tab
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
self.prefix = Some(prefix.into_any_element());
self
}
/// Set the right side of the tab
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
self.suffix = Some(suffix.into_any_element());
self
}
/// Set disabled state to the tab, default false.
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Set the click handler for the tab.
pub fn on_click(
mut self,
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_click = Some(Rc::new(on_click));
self
}
/// Set index to the tab.
pub(crate) fn ix(mut self, ix: usize) -> Self {
self.ix = ix;
self
}
/// Set if the tab bar has a prefix.
pub(crate) fn tab_bar_prefix(mut self, tab_bar_prefix: bool) -> Self {
self.tab_bar_prefix = Some(tab_bar_prefix);
self
}
}
impl ParentElement for Tab {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
@@ -105,62 +588,115 @@ impl Sizable for Tab {
}
impl RenderOnce for Tab {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let (text_color, hover_text_color, bg_color, border_color) =
match (self.selected, self.disabled) {
(true, false) => (
cx.theme().tab_active_foreground,
cx.theme().tab_hover_foreground,
cx.theme().tab_active_background,
cx.theme().border,
),
(false, false) => (
cx.theme().tab_inactive_foreground,
cx.theme().tab_hover_foreground,
cx.theme().ghost_element_background,
cx.theme().border_transparent,
),
(true, true) => (
cx.theme().tab_inactive_foreground,
cx.theme().tab_hover_foreground,
cx.theme().ghost_element_background,
cx.theme().border_disabled,
),
(false, true) => (
cx.theme().tab_inactive_foreground,
cx.theme().tab_hover_foreground,
cx.theme().ghost_element_background,
cx.theme().border_disabled,
),
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let mut tab_style = if self.selected {
self.variant.selected(cx)
} else {
self.variant.normal(cx)
};
let mut hover_style = self.variant.hovered(self.selected, cx);
if self.disabled {
tab_style = self.variant.disabled(self.selected, cx);
hover_style = self.variant.disabled(self.selected, cx);
}
let tab_bar_prefix = self.tab_bar_prefix.unwrap_or_default();
if !tab_bar_prefix && self.ix == 0 && self.variant == TabVariant::Tab {
tab_style.borders.left = px(0.);
hover_style.borders.left = px(0.);
}
let radius = self.variant.radius(self.size, cx);
let inner_radius = self.variant.inner_radius(self.size, cx);
let inner_paddings = self.variant.inner_paddings(self.size);
let inner_margins = self.variant.inner_margins(self.size);
let inner_height = self.variant.inner_height(self.size);
let height = self.variant.height(self.size);
self.base
.id(self.ix)
.h(TABBAR_HEIGHT)
.px_4()
.relative()
.flex()
.flex_wrap()
.gap_1()
.items_center()
.flex_shrink_0()
.cursor_pointer()
.h(height)
.overflow_hidden()
.text_xs()
.text_ellipsis()
.text_color(text_color)
.bg(bg_color)
.border_l(px(1.))
.border_r(px(1.))
.border_color(border_color)
.text_color(tab_style.fg)
.map(|this| match self.size {
Size::XSmall => this.text_xs(),
Size::Large => this.text_base(),
_ => this.text_sm(),
})
.bg(tab_style.bg)
.border_l(tab_style.borders.left)
.border_r(tab_style.borders.right)
.border_t(tab_style.borders.top)
.border_b(tab_style.borders.bottom)
.border_color(tab_style.border_color)
.rounded(radius)
.when(!self.selected && !self.disabled, |this| {
this.hover(|this| this.text_color(hover_text_color))
this.hover(|this| {
this.text_color(hover_style.fg)
.bg(hover_style.bg)
.border_l(hover_style.borders.left)
.border_r(hover_style.borders.right)
.border_t(hover_style.borders.top)
.border_b(hover_style.borders.bottom)
.border_color(hover_style.border_color)
.rounded(radius)
})
.when_some(self.prefix, |this, prefix| {
this.child(prefix).text_color(text_color)
})
.when_some(self.label, |this, label| this.child(label))
.when_some(self.suffix, |this, suffix| this.child(suffix))
.on_mouse_down(MouseButton::Left, |_ev, _window, cx| {
.when_some(self.prefix, |this, prefix| this.child(prefix))
.child(
h_flex()
.flex_1()
.h(inner_height)
.line_height(relative(1.))
.whitespace_nowrap()
.items_center()
.justify_center()
.overflow_hidden()
.margins(inner_margins)
.flex_shrink_0()
.map(|this| match self.icon {
Some(icon) => {
this.w(inner_height * 1.25)
.child(icon.map(|this| match self.size {
Size::XSmall => this.size_2p5(),
Size::Small => this.size_3p5(),
Size::Large => this.size_4(),
_ => this.size_4(),
}))
}
None => this
.paddings(inner_paddings)
.map(|this| match self.label {
Some(label) => this.child(label),
None => this,
})
.children(self.children),
})
.bg(tab_style.inner_bg)
.rounded(inner_radius)
.when(tab_style.shadow, |this| this.shadow_xs())
.hover(|this| this.bg(hover_style.inner_bg).rounded(inner_radius)),
)
.when_some(self.suffix, |this, suffix| {
this.child(div().pr_2().child(suffix))
})
.on_mouse_down(MouseButton::Left, |_, _, cx| {
// Stop propagation behavior, for works on TitleBar.
// https://github.com/longbridge/gpui-component/issues/1836
cx.stop_propagation();
})
.when(!self.disabled, |this| {
this.when_some(self.on_click.clone(), |this, on_click| {
this.on_click(move |event, window, cx| on_click(event, window, cx))
})
})
}
}

View File

@@ -1,41 +1,92 @@
use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
#[cfg(not(target_os = "windows"))]
use gpui::Pixels;
use gpui::{
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, ParentElement, RenderOnce,
ScrollHandle, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
AnyElement, App, Corner, Div, Edges, ElementId, InteractiveElement, IntoElement, ParentElement,
RenderOnce, ScrollHandle, Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled,
Window, div, px,
};
use smallvec::SmallVec;
use theme::ActiveTheme;
use crate::{h_flex, Sizable, Size, StyledExt};
use super::{Tab, TabVariant};
use crate::button::{Button, ButtonVariants as _};
use crate::menu::{DropdownMenu as _, PopupMenuItem};
use crate::{IconName, Selectable, Sizable, Size, StyledExt, h_flex};
#[allow(clippy::type_complexity)]
/// A TabBar element that contains multiple [`Tab`] items.
#[derive(IntoElement)]
pub struct TabBar {
base: Div,
base: Stateful<Div>,
style: StyleRefinement,
scroll_handle: Option<ScrollHandle>,
prefix: Option<AnyElement>,
suffix: Option<AnyElement>,
children: SmallVec<[Tab; 2]>,
last_empty_space: AnyElement,
children: SmallVec<[AnyElement; 2]>,
selected_index: Option<usize>,
variant: TabVariant,
size: Size,
menu: bool,
on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
}
impl TabBar {
pub fn new() -> Self {
/// Create a new TabBar.
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
base: h_flex().px(px(-1.)),
base: div().id(id).px(px(-1.)),
style: StyleRefinement::default(),
scroll_handle: None,
children: SmallVec::new(),
scroll_handle: None,
prefix: None,
suffix: None,
variant: TabVariant::default(),
size: Size::default(),
last_empty_space: div().w_3().into_any_element(),
selected_index: None,
on_click: None,
menu: false,
}
}
/// Set the Tab variant, all children will inherit the variant.
pub fn with_variant(mut self, variant: TabVariant) -> Self {
self.variant = variant;
self
}
/// Set the Tab variant to Pill, all children will inherit the variant.
pub fn pill(mut self) -> Self {
self.variant = TabVariant::Pill;
self
}
/// Set the Tab variant to Outline, all children will inherit the variant.
pub fn outline(mut self) -> Self {
self.variant = TabVariant::Outline;
self
}
/// Set the Tab variant to Segmented, all children will inherit the variant.
pub fn segmented(mut self) -> Self {
self.variant = TabVariant::Segmented;
self
}
/// Set the Tab variant to Underline, all children will inherit the variant.
pub fn underline(mut self) -> Self {
self.variant = TabVariant::Underline;
self
}
/// Set whether to show the menu button when tabs overflow, default is false.
pub fn menu(mut self, menu: bool) -> Self {
self.menu = menu;
self
}
/// Track the scroll of the TabBar.
pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
self.scroll_handle = Some(scroll_handle.clone());
@@ -54,27 +105,39 @@ impl TabBar {
self
}
/// Add children of the TabBar, all children will inherit the variant.
pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Tab>>) -> Self {
self.children.extend(children.into_iter().map(Into::into));
self
}
/// Add child of the TabBar, tab will inherit the variant.
pub fn child(mut self, child: impl Into<Tab>) -> Self {
self.children.push(child.into());
self
}
/// Set the selected index of the TabBar.
pub fn selected_index(mut self, index: usize) -> Self {
self.selected_index = Some(index);
self
}
/// Set the last empty space element of the TabBar.
pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self {
self.last_empty_space = last_empty_space.into_any_element();
self
}
#[cfg(not(target_os = "windows"))]
pub fn height(window: &mut Window) -> Pixels {
(1.75 * window.rem_size()).max(px(36.))
}
}
impl Default for TabBar {
fn default() -> Self {
Self::new()
}
}
impl ParentElement for TabBar {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
/// Set the on_click callback of the TabBar, the first parameter is the index of the clicked tab.
///
/// When this is set, the children's on_click will be ignored.
pub fn on_click<F>(mut self, on_click: F) -> Self
where
F: Fn(&usize, &mut Window, &mut App) + 'static,
{
self.on_click = Some(Rc::new(on_click));
self
}
}
@@ -92,15 +155,69 @@ impl Sizable for TabBar {
}
impl RenderOnce for TabBar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let default_gap = match self.size {
Size::Small | Size::XSmall => px(8.),
Size::Large => px(16.),
_ => px(12.),
};
let (bg, paddings, gap) = match self.variant {
TabVariant::Tab => {
let padding = Edges::all(px(0.));
(cx.theme().tab_background, padding, px(0.))
}
TabVariant::Outline => {
let padding = Edges::all(px(0.));
(gpui::transparent_black(), padding, default_gap)
}
TabVariant::Pill => {
let padding = Edges::all(px(0.));
(gpui::transparent_black(), padding, px(4.))
}
TabVariant::Segmented => {
let padding_x = match self.size {
Size::XSmall => px(2.),
Size::Small => px(3.),
_ => px(4.),
};
let padding = Edges {
left: padding_x,
right: padding_x,
..Default::default()
};
(cx.theme().tab_background, padding, px(2.))
}
TabVariant::Underline => {
// This gap is same as the tab inner_paddings
let gap = match self.size {
Size::XSmall => px(10.),
Size::Small => px(12.),
Size::Large => px(20.),
_ => px(16.),
};
(gpui::transparent_black(), Edges::all(px(0.)), gap)
}
};
let mut item_labels = Vec::new();
let selected_index = self.selected_index;
let on_click = self.on_click.clone();
self.base
.group("tab-bar")
.relative()
.refine_style(&self.style)
.bg(cx.theme().surface_background)
.child(
.flex()
.items_center()
.bg(bg)
.text_color(cx.theme().tab_foreground)
.when(
self.variant == TabVariant::Underline || self.variant == TabVariant::Tab,
|this| {
this.child(
div()
.id("border-bottom")
.id("border-b")
.absolute()
.left_0()
.bottom_0()
@@ -108,21 +225,66 @@ impl RenderOnce for TabBar {
.border_b_1()
.border_color(cx.theme().border),
)
.text_color(cx.theme().text)
},
)
.rounded(self.variant.tab_bar_radius(self.size, cx))
.paddings(paddings)
.refine_style(&self.style)
.when_some(self.prefix, |this, prefix| this.child(prefix))
.child(
h_flex()
.id("tabs")
.flex_grow()
.flex_1()
.overflow_x_scroll()
.when_some(self.scroll_handle, |this, scroll_handle| {
this.track_scroll(&scroll_handle)
})
.children(self.children)
.when(self.suffix.is_some(), |this| {
.gap(gap)
.children(self.children.into_iter().enumerate().map(|(ix, child)| {
item_labels.push((child.label.clone(), child.disabled));
let tab_bar_prefix = child.tab_bar_prefix.unwrap_or(true);
child
.ix(ix)
.tab_bar_prefix(tab_bar_prefix)
.with_variant(self.variant)
.with_size(self.size)
.when_some(self.selected_index, |this, selected_ix| {
this.selected(selected_ix == ix)
})
.when_some(self.on_click.clone(), move |this, on_click| {
this.on_click(move |_, window, cx| on_click(&ix, window, cx))
})
}))
.when(self.suffix.is_some() || self.menu, |this| {
this.child(self.last_empty_space)
}),
)
.when(self.menu, |this| {
this.child(
Button::new("more")
.xsmall()
.ghost()
.icon(IconName::ChevronDown)
.dropdown_menu(move |mut this, _, _| {
this = this.scrollable(true);
for (ix, (label, disabled)) in item_labels.iter().enumerate() {
this = this.item(
PopupMenuItem::new(label.clone().unwrap_or_default())
.checked(selected_index == Some(ix))
.disabled(*disabled)
.when_some(on_click.clone(), |this, on_click| {
this.on_click(move |_, window, cx| {
on_click(&ix, window, cx)
})
}),
)
}
this
})
.anchor(Corner::TopRight),
)
})
.when_some(self.suffix, |this, suffix| this.child(suffix))
}
}

View File

@@ -1,11 +1,11 @@
use std::rc::Rc;
use gpui::{App, Entity, SharedString, Window};
use gpui::{App, ElementId, Entity, Window};
use crate::Root;
use crate::input::InputState;
use crate::modal::Modal;
use crate::notification::Notification;
use crate::Root;
/// Extension trait for [`Window`] to add modal, notification .. functionality.
pub trait WindowExtension: Sized {
@@ -31,10 +31,15 @@ pub trait WindowExtension: Sized {
where
T: Into<Notification>;
/// Clears a notification by its ID.
fn clear_notification<T>(&mut self, id: T, cx: &mut App)
where
T: Into<SharedString>;
/// Clear the unique notification.
fn clear_notification<T: Sized + 'static>(&mut self, cx: &mut App);
/// Clear the unique notification with the given id.
fn clear_notification_by_id<T: Sized + 'static>(
&mut self,
key: impl Into<ElementId>,
cx: &mut App,
);
/// Clear all notifications
fn clear_notifications(&mut self, cx: &mut App);
@@ -88,13 +93,21 @@ impl WindowExtension for Window {
}
#[inline]
fn clear_notification<T>(&mut self, id: T, cx: &mut App)
where
T: Into<SharedString>,
{
let id = id.into();
Root::update(self, cx, move |root, window, cx| {
root.clear_notification(id, window, cx);
fn clear_notification<T: Sized + 'static>(&mut self, cx: &mut App) {
Root::update(self, cx, |root, window, cx| {
root.clear_notification::<T>(window, cx);
})
}
#[inline]
fn clear_notification_by_id<T: Sized + 'static>(
&mut self,
key: impl Into<ElementId>,
cx: &mut App,
) {
let key: ElementId = key.into();
Root::update(self, cx, |root, window, cx| {
root.clear_notification_by_id::<T>(key, window, cx);
})
}