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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -7,21 +7,11 @@ use settings::SignerKind;
#[action(namespace = chat, no_json)] #[action(namespace = chat, no_json)]
pub enum Command { pub enum Command {
Insert(&'static str), Insert(&'static str),
ChangeSubject(&'static str), ChangeSubject(String),
ChangeSigner(SignerKind), ChangeSigner(SignerKind),
ToggleBackup, 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::collections::{BTreeMap, BTreeSet, HashSet};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
pub use actions::*; pub use actions::*;
use anyhow::{Context as AnyhowContext, Error}; use anyhow::{Context as AnyhowContext, Error};
@@ -24,10 +23,10 @@ use state::{NostrRegistry, upload};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock::{Panel, PanelEvent};
use ui::indicator::Indicator; use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::menu::{ContextMenuExt, DropdownMenu}; use ui::menu::DropdownMenu;
use ui::notification::Notification; use ui::notification::Notification;
use ui::scroll::Scrollbar; use ui::scroll::Scrollbar;
use ui::{ use ui::{
@@ -42,11 +41,6 @@ mod text;
const ANNOUNCEMENT: &str = const ANNOUNCEMENT: &str =
"This conversation is private. Only members can see each other's messages."; "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> { pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
cx.new(|cx| ChatPanel::new(room, window, cx)) cx.new(|cx| ChatPanel::new(room, window, cx))
@@ -72,9 +66,15 @@ pub struct ChatPanel {
/// Mapping message (rumor event) ids to their reports /// Mapping message (rumor event) ids to their reports
reports_by_id: Entity<BTreeMap<EventId, Vec<SendReport>>>, reports_by_id: Entity<BTreeMap<EventId, Vec<SendReport>>>,
/// Input state /// Chat input state
input: Entity<InputState>, input: Entity<InputState>,
/// Subject input state
subject_input: Entity<InputState>,
/// Subject bar visibility
subject_bar: Entity<bool>,
/// Sent message ids /// Sent message ids
sent_ids: Arc<RwLock<Vec<EventId>>>, sent_ids: Arc<RwLock<Vec<EventId>>>,
@@ -91,7 +91,7 @@ pub struct ChatPanel {
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<Result<(), Error>>>,
/// Event subscriptions /// Event subscriptions
subscriptions: SmallVec<[Subscription; 2]>, subscriptions: SmallVec<[Subscription; 3]>,
} }
impl ChatPanel { impl ChatPanel {
@@ -124,19 +124,37 @@ impl ChatPanel {
.clean_on_escape() .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 // Define subscriptions
let subscriptions = let mut subscriptions = smallvec![];
smallvec![
subscriptions.push(
// Subscribe the chat input event
cx.subscribe_in(&input, window, move |this, _input, event, window, cx| { cx.subscribe_in(&input, window, move |this, _input, event, window, cx| {
if let InputEvent::PressEnter { .. } = event { if let InputEvent::PressEnter { .. } = event {
this.send_text_message(window, cx); 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 // Define all functions that will run after the current cycle
cx.defer_in(window, |this, window, cx| { cx.defer_in(window, |this, window, cx| {
this.connect(window, cx);
this.handle_notifications(cx); this.handle_notifications(cx);
this.subscribe_room_events(window, cx); this.subscribe_room_events(window, cx);
this.get_messages(window, cx); this.get_messages(window, cx);
@@ -149,6 +167,8 @@ impl ChatPanel {
room, room,
list_state, list_state,
input, input,
subject_input,
subject_bar,
replies_to, replies_to,
attachments, attachments,
rendered_texts_by_id: BTreeMap::new(), 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 /// Handle nostr notifications
fn handle_notifications(&mut self, cx: &mut Context<Self>) { fn handle_notifications(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
@@ -254,10 +231,7 @@ impl ChatPanel {
} }
fn subscribe_room_events(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn subscribe_room_events(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(room) = self.room.upgrade() else { if let Some(room) = self.room.upgrade() {
return;
};
self.subscriptions.push( self.subscriptions.push(
// Subscribe to room events // Subscribe to room events
cx.subscribe_in(&room, window, move |this, _room, event, window, cx| { cx.subscribe_in(&room, window, move |this, _room, event, window, cx| {
@@ -272,6 +246,7 @@ impl ChatPanel {
}), }),
); );
} }
}
/// Load all messages belonging to this room /// Load all messages belonging to this room
fn get_messages(&mut self, _window: &mut Window, cx: &mut Context<Self>) { fn get_messages(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
@@ -316,6 +291,16 @@ impl ChatPanel {
content 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>) { fn send_text_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// Get the message which includes all attachments // Get the message which includes all attachments
let content = self.get_input_value(cx); let content = self.get_input_value(cx);
@@ -505,10 +490,21 @@ impl ChatPanel {
} }
} }
fn copy_message(&self, id: &EventId, cx: &Context<Self>) { fn copy_author(&self, public_key: &PublicKey, cx: &App) {
if let Some(message) = self.message(id) { let content = public_key.to_bech32().unwrap();
cx.write_to_clipboard(ClipboardItem::new_string(message.content.to_string())); 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>) { fn reply_to(&mut self, id: &EventId, cx: &mut Context<Self>) {
@@ -558,7 +554,10 @@ impl ChatPanel {
Err(e) => { Err(e) => {
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
this.set_uploading(false, 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); let persons = PersonRegistry::global(cx);
persons.read(cx).get(public_key, cx) persons.read(cx).get(public_key, cx)
} }
@@ -602,11 +601,14 @@ impl ChatPanel {
if self if self
.room .room
.update(cx, |this, cx| { .update(cx, |this, cx| {
this.set_subject(*subject, cx); this.set_subject(subject, cx);
}) })
.is_err() .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) => { Command::ChangeSigner(kind) => {
@@ -617,7 +619,10 @@ impl ChatPanel {
}) })
.is_err() .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 => { Command::ToggleBackup => {
@@ -628,10 +633,101 @@ impl ChatPanel {
}) })
.is_err() .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 { fn render_announcement(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
@@ -758,18 +854,14 @@ impl ChatPanel {
.flex() .flex()
.gap_3() .gap_3()
.when(!hide_avatar, |this| { .when(!hide_avatar, |this| {
this.child( this.child(Avatar::new(author.avatar()).dropdown_menu(
div() move |this, _window, _cx| {
.id(SharedString::from(format!("{ix}-avatar"))) this.menu("Copy Public Key", Box::new(Command::Copy(public_key)))
.child(Avatar::new(author.avatar())) .menu("View Relays", Box::new(Command::Relays(public_key)))
.context_menu(move |this, _window, _cx| { .separator()
let view = Box::new(OpenPublicKey(public_key)); .menu("View on njump.me", Box::new(Command::Njump(public_key)))
let copy = Box::new(CopyPublicKey(public_key)); },
))
this.menu("View Profile", view)
.menu("Copy Public Key", copy)
}),
)
}) })
.child( .child(
v_flex() v_flex()
@@ -807,8 +899,17 @@ impl ChatPanel {
}), }),
), ),
) )
.child(self.render_border(cx)) .child(
.child(self.render_actions(&id, cx)) 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( .on_mouse_down(
MouseButton::Middle, MouseButton::Middle,
cx.listener(move |this, _, _window, cx| { cx.listener(move |this, _, _window, cx| {
@@ -896,11 +997,11 @@ impl ChatPanel {
fn render_message_reports(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement { fn render_message_reports(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
h_flex() h_flex()
.id(SharedString::from(id.to_hex())) .id(SharedString::from(id.to_hex()))
.gap_0p5() .gap_1()
.text_color(cx.theme().danger_active) .text_color(cx.theme().text_danger)
.text_xs() .text_xs()
.italic() .italic()
.child(Icon::new(IconName::Info).xsmall()) .child(Icon::new(IconName::Info).small())
.child(SharedString::from( .child(SharedString::from(
"Failed to send message. Click to see details.", "Failed to send message. Click to see details.",
)) ))
@@ -911,7 +1012,7 @@ impl ChatPanel {
window.open_modal(cx, move |this, _window, cx| { window.open_modal(cx, move |this, _window, cx| {
this.show_close(true) this.show_close(true)
.title(SharedString::from("Sent Reports")) .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()); let mut items = Vec::with_capacity(reports.len());
for report in reports.iter() { for report in reports.iter() {
@@ -1030,18 +1131,12 @@ impl ChatPanel {
}) })
} }
fn render_border(&self, cx: &Context<Self>) -> impl IntoElement { fn render_actions(
div() &self,
.group_hover("", |this| this.bg(cx.theme().element_active)) id: &EventId,
.absolute() public_key: &PublicKey,
.left_0() cx: &Context<Self>,
.top_0() ) -> impl IntoElement {
.w(px(2.))
.h_full()
.bg(cx.theme().border_transparent)
}
fn render_actions(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
h_flex() h_flex()
.p_0p5() .p_0p5()
.gap_1() .gap_1()
@@ -1082,13 +1177,22 @@ impl ChatPanel {
) )
.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border)) .child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
.child( .child(
Button::new("seen-on") Button::new("advance")
.icon(IconName::Ellipsis) .icon(IconName::Ellipsis)
.small() .small()
.ghost() .ghost()
.dropdown_menu({ .dropdown_menu({
let id = id.to_owned(); let public_key = *public_key;
move |this, _window, _cx| this.menu("Seen on", Box::new(SeenOn(id))) 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()) .group_hover("", |this| this.visible())
@@ -1286,12 +1390,30 @@ impl Panel for ChatPanel {
h_flex() h_flex()
.gap_1p5() .gap_1p5()
.child(Avatar::new(url).small()) .child(Avatar::new(url).xsmall())
.child(label) .child(label)
.into_any_element() .into_any_element()
}) })
.unwrap_or(div().child("Unknown").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 {} impl EventEmitter<PanelEvent> for ChatPanel {}
@@ -1307,6 +1429,33 @@ impl Render for ChatPanel {
v_flex() v_flex()
.on_action(cx.listener(Self::on_command)) .on_action(cx.listener(Self::on_command))
.size_full() .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( .child(
v_flex() v_flex()
.flex_1() .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 std::sync::Arc;
use anyhow::{anyhow, Error}; use anyhow::{Error, anyhow};
use chrono::{Local, TimeZone}; use chrono::{Local, TimeZone};
use gpui::{Image, ImageFormat, SharedString}; use gpui::{Image, ImageFormat, SharedString};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
use gpui::http_client::Url; use gpui::http_client::Url;
use gpui::{ use gpui::{
div, px, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
Styled, Window, Window, div, px,
}; };
use settings::{AppSettings, AuthMode}; use settings::{AppSettings, AuthMode};
use theme::{ActiveTheme, ThemeMode}; use theme::{ActiveTheme, ThemeMode};
@@ -11,7 +11,7 @@ use ui::input::{InputState, TextInput};
use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::Notification; use ui::notification::Notification;
use ui::switch::Switch; 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> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
cx.new(|cx| Preferences::new(window, cx)) cx.new(|cx| Preferences::new(window, cx))
@@ -41,7 +41,7 @@ impl Preferences {
AppSettings::update_file_server(url, cx); AppSettings::update_file_server(url, cx);
} }
Err(e) => { 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 assets::Assets;
use gpui::{ use gpui::{
actions, point, px, size, App, AppContext, Bounds, KeyBinding, Menu, MenuItem, SharedString, App, AppContext, Bounds, KeyBinding, Menu, MenuItem, SharedString, TitlebarOptions,
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
WindowOptions, actions, point, px, size,
}; };
use gpui_platform::application; use gpui_platform::application;
use state::{APP_ID, CLIENT_NAME}; use state::{APP_ID, CLIENT_NAME};
@@ -86,7 +86,7 @@ fn main() {
state::init(window, cx); state::init(window, cx);
// Initialize person registry // Initialize person registry
person::init(cx); person::init(window, cx);
// Initialize relay auth registry // Initialize relay auth registry
relay_auth::init(window, cx); relay_auth::init(window, cx);

View File

@@ -2,16 +2,16 @@ use std::time::Duration;
use anyhow::Error; use anyhow::Error;
use gpui::{ use gpui::{
div, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use state::KEYRING; use state::KEYRING;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock::{Panel, PanelEvent};
use ui::input::{InputState, TextInput}; 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. \ const MSG: &str = "Store your account keys in a safe location. \
You can restore your account or move to another client anytime you want."; 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 anyhow::{Context as AnyhowContext, Error};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ 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, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, TextAlign, Window, Task, TextAlign, Window, div, rems,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
use smallvec::{smallvec, SmallVec}; use smallvec::{SmallVec, smallvec};
use state::NostrRegistry; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; 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::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> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<ContactListPanel> {
cx.new(|cx| ContactListPanel::new(window, cx)) cx.new(|cx| ContactListPanel::new(window, cx))
@@ -156,15 +156,6 @@ impl ContactListPanel {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); 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 // Get contacts
let contacts: Vec<Contact> = self let contacts: Vec<Contact> = self
@@ -177,14 +168,12 @@ impl ContactListPanel {
self.set_updating(true, cx); self.set_updating(true, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
// Construct contact list event builder // Construct contact list event builder
let builder = EventBuilder::contact_list(contacts); let builder = EventBuilder::contact_list(contacts);
let event = client.sign_event_builder(builder).await?; let event = client.sign_event_builder(builder).await?;
// Set contact list // Set contact list
client.send_event(&event).to(urls).await?; client.send_event(&event).to_nip65().await?;
Ok(()) Ok(())
}); });
@@ -333,7 +322,7 @@ impl Render for ContactListPanel {
div() div()
.italic() .italic()
.text_xs() .text_xs()
.text_color(cx.theme().danger_active) .text_color(cx.theme().text_danger)
.child(error.clone()), .child(error.clone()),
) )
}), }),

View File

@@ -1,17 +1,14 @@
use chat::{ChatRegistry, InboxState};
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Window, IntoElement, ParentElement, Render, SharedString, Styled, Window, div, svg,
}; };
use state::{NostrRegistry, RelayState}; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::dock::DockPlacement; use ui::dock::{DockPlacement, Panel, PanelEvent};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt};
use crate::panels::{messaging_relays, profile, relay_list}; use crate::panels::profile;
use crate::workspace::Workspace; use crate::workspace::Workspace;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> { 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 TITLE: &str = "Welcome to Coop!";
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr."; 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() h_flex()
.size_full() .size_full()
.items_center() .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( .child(
v_flex() v_flex()
.gap_2() .gap_2()

View File

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

View File

@@ -3,21 +3,21 @@ use std::time::Duration;
use anyhow::{Context as AnyhowContext, Error}; use anyhow::{Context as AnyhowContext, Error};
use gpui::{ 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, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
Window, Window, div,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::{shorten_pubkey, Person, PersonRegistry}; use person::{Person, PersonRegistry, shorten_pubkey};
use settings::AppSettings; use settings::AppSettings;
use state::{upload, NostrRegistry}; use state::{NostrRegistry, upload};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock::{Panel, PanelEvent};
use ui::input::{InputState, TextInput}; use ui::input::{InputState, TextInput};
use ui::notification::Notification; 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> { pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<ProfilePanel> {
cx.new(|cx| ProfilePanel::new(public_key, window, cx)) cx.new(|cx| ProfilePanel::new(public_key, window, cx))
@@ -186,7 +186,10 @@ impl ProfilePanel {
Err(e) => { Err(e) => {
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
this.set_uploading(false, 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) => { Err(e) => {
cx.update(|window, cx| { 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::collections::HashSet;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error}; use anyhow::{Context as AnyhowContext, Error, anyhow};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, px, rems, Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Subscription, Task, TextAlign, Window, Task, TextAlign, Window, div, px, rems,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::Deserialize; use serde::Deserialize;
use smallvec::{smallvec, SmallVec}; use smallvec::{SmallVec, smallvec};
use state::NostrRegistry; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock::{Panel, PanelEvent};
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::menu::DropdownMenu; 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 \ const MSG: &str = "Relay List (or Gossip Relays) are a set of relays \
where you will publish all your events. Others also publish events \ where you will publish all your events. Others also publish events \
@@ -408,7 +408,7 @@ impl Render for RelayListPanel {
div() div()
.italic() .italic()
.text_xs() .text_xs()
.text_color(cx.theme().danger_active) .text_color(cx.theme().text_danger)
.child(error.clone()), .child(error.clone()),
) )
}), }),

View File

@@ -3,16 +3,16 @@ use std::rc::Rc;
use chat::RoomKind; use chat::RoomKind;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce, SharedString,
SharedString, StatefulInteractiveElement, Styled, Window, StatefulInteractiveElement, Styled, Window, div,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use settings::AppSettings; use settings::AppSettings;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::dock_area::ClosePanel; use ui::dock::ClosePanel;
use ui::modal::ModalButtonProps; 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; use crate::dialogs::screening;

View File

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

View File

@@ -1,8 +1,10 @@
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use ::settings::AppSettings; use ::settings::AppSettings;
use chat::{ChatEvent, ChatRegistry, InboxState}; use chat::{ChatEvent, ChatRegistry};
use device::DeviceRegistry; use device::{DeviceEvent, DeviceRegistry};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement, Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
@@ -11,17 +13,15 @@ use gpui::{
use person::PersonRegistry; use person::PersonRegistry;
use serde::Deserialize; use serde::Deserialize;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
use state::{NostrRegistry, RelayState, SignerEvent}; use state::{NostrRegistry, StateEvent};
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry}; use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
use title_bar::TitleBar; use title_bar::TitleBar;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::dock::DockPlacement; use ui::dock::{ClosePanel, DockArea, DockItem, DockPlacement, PanelView};
use ui::dock_area::panel::PanelView;
use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::Notification; use ui::notification::{Notification, NotificationKind};
use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; use ui::{Disableable, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
use crate::dialogs::{accounts, settings}; use crate::dialogs::{accounts, settings};
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list}; 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)) cx.new(|cx| Workspace::new(window, cx))
} }
struct RelayNotifcation;
#[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = workspace, no_json)] #[action(namespace = workspace, no_json)]
enum Command { enum Command {
@@ -63,15 +65,23 @@ pub struct Workspace {
/// App's Dock Area /// App's Dock Area
dock: Entity<DockArea>, dock: Entity<DockArea>,
/// Whether a user's relay list is connected
relay_connected: bool,
/// Whether the inbox is connected
inbox_connected: bool,
/// Event subscriptions /// Event subscriptions
_subscriptions: SmallVec<[Subscription; 4]>, _subscriptions: SmallVec<[Subscription; 6]>,
} }
impl Workspace { impl Workspace {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx);
let device = DeviceRegistry::global(cx);
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let npubs = nostr.read(cx).npubs(); let npubs = nostr.read(cx).npubs();
let chat = ChatRegistry::global(cx);
let titlebar = cx.new(|_| TitleBar::new()); let titlebar = cx.new(|_| TitleBar::new());
let dock = cx.new(|cx| DockArea::new(window, cx)); let dock = cx.new(|cx| DockArea::new(window, cx));
@@ -96,9 +106,56 @@ impl Workspace {
subscriptions.push( subscriptions.push(
// Subscribe to the signer events // Subscribe to the signer events
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| { cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
if let SignerEvent::Set = event { match event {
this.set_center_layout(window, cx); 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 { Self {
titlebar, titlebar,
dock, dock,
relay_connected: false,
inbox_connected: false,
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
} }
@@ -185,6 +250,18 @@ impl Workspace {
.collect() .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 /// Set the dock layout
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let left = DockItem::panel(Arc::new(sidebar::init(window, cx))); let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
@@ -198,8 +275,8 @@ impl Workspace {
/// Set the center dock layout /// Set the center dock layout
fn set_center_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn set_center_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let dock = self.dock.downgrade(); let dock = self.dock.downgrade();
let greeeter = Arc::new(greeter::init(window, cx)); let greeter = Arc::new(greeter::init(window, cx));
let tabs = DockItem::tabs(vec![greeeter], None, &dock, window, cx); let tabs = DockItem::tabs(vec![greeter], None, &dock, window, cx);
let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx); let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx);
// Update the layout with center dock // 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 => { Command::ShowRelayList => {
self.dock.update(cx, |this, cx| { self.dock.update(cx, |this, cx| {
this.add_panel( 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 => { Command::RefreshEncryption => {
let device = DeviceRegistry::global(cx); let device = DeviceRegistry::global(cx);
device.update(cx, |this, cx| { device.update(cx, |this, cx| {
this.get_announcement(cx); this.get_announcement(cx);
}); });
} }
Command::RefreshRelayList => {
let nostr = NostrRegistry::global(cx);
nostr.update(cx, |this, cx| {
this.ensure_relay_list(cx);
});
}
Command::ResetEncryption => { Command::ResetEncryption => {
self.confirm_reset_encryption(window, cx); 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 => { Command::ToggleTheme => {
self.theme_selector(window, cx); self.theme_selector(window, cx);
} }
@@ -341,8 +422,10 @@ impl Workspace {
window.close_modal(cx); window.close_modal(cx);
} }
Err(e) => { Err(e) => {
window window.push_notification(
.push_notification(Notification::error(e.to_string()), cx); Notification::error(e.to_string()).autohide(false),
cx,
);
} }
}) })
.ok(); .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 nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let current_user = signer.public_key(); 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 nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let chat = ChatRegistry::global(cx); let Some(public_key) = signer.public_key() else {
let inbox_state = chat.read(cx).state(cx);
let Some(pkey) = signer.public_key() else {
return div(); return div();
}; };
@@ -554,7 +686,7 @@ impl Workspace {
let state = device.read(cx).state(); let state = device.read(cx).state();
this.min_w(px(260.)) this.min_w(px(260.))
.item(PopupMenuItem::element(move |_window, _cx| { .item(PopupMenuItem::element(move |_window, cx| {
h_flex() h_flex()
.px_1() .px_1()
.w_full() .w_full()
@@ -566,7 +698,7 @@ impl Workspace {
.rounded_full() .rounded_full()
.when(state.set(), |this| this.bg(gpui::green())) .when(state.set(), |this| this.bg(gpui::green()))
.when(state.requesting(), |this| { .when(state.requesting(), |this| {
this.bg(gpui::yellow()) this.bg(cx.theme().icon_accent)
}), }),
) )
.child(SharedString::from(state.to_string())) .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( .child(
Button::new("inbox") Button::new("inbox")
.icon(IconName::Inbox) .icon(IconName::Inbox)
.tooltip("Inbox")
.small() .small()
.ghost() .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| { .dropdown_menu(move |this, _window, cx| {
let persons = PersonRegistry::global(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 let urls: Vec<SharedString> = profile
.messaging_relays() .messaging_relays()
.iter() .iter()
@@ -632,9 +748,7 @@ impl Workspace {
.w_full() .w_full()
.gap_2() .gap_2()
.text_sm() .text_sm()
.child( .child(div().size_1p5().rounded_full().bg(gpui::green()))
div().size_1p5().rounded_full().bg(gpui::green()),
)
.child(url.clone()) .child(url.clone())
})) }))
}); });
@@ -652,73 +766,32 @@ impl Workspace {
Box::new(Command::ShowMessaging), 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( .child(
Button::new("relay-list") Button::new("relay-list")
.icon(IconName::Relay) .icon(IconName::Relay)
.tooltip("User's relay list")
.small() .small()
.ghost() .ghost()
.when(nostr.read(cx).relay_list_state.configured(), |this| { .loading(!relay_connected)
this.indicator() .disabled(!relay_connected)
.when(!relay_connected, |this| {
this.tooltip("Connecting to user's relay list...")
}) })
.dropdown_menu(move |this, _window, cx| { .when(relay_connected, |this| this.indicator())
let nostr = NostrRegistry::global(cx); .dropdown_menu(move |this, _window, _cx| {
let urls = nostr.read(cx).read_only_relays(&pkey, cx); this.label("User's Relay List")
.separator()
// 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()
.menu_with_icon( .menu_with_icon(
"Reload", "Reload",
IconName::Refresh, IconName::Refresh,
Box::new(Command::RefreshRelayList), Box::new(Command::RefreshRelayList),
) )
.menu_with_icon( .menu_with_icon(
"Update relay list", "Update",
IconName::Settings, IconName::Settings,
Box::new(Command::ShowRelayList), Box::new(Command::ShowRelayList),
) )
}), }),
),
) )
} }
} }

View File

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

View File

@@ -15,3 +15,4 @@ smallvec.workspace = true
smol.workspace = true smol.workspace = true
flume.workspace = true flume.workspace = true
log.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::rc::Rc;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{Error, anyhow};
use common::EventUtils; 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 nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{SmallVec, smallvec};
use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT}; use state::{Announcement, BOOTSTRAP_RELAYS, NostrRegistry, TIMEOUT};
mod person; mod person;
pub use person::*; pub use person::*;
pub fn init(cx: &mut App) { pub fn init(window: &mut Window, cx: &mut App) {
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx); PersonRegistry::set_global(cx.new(|cx| PersonRegistry::new(window, cx)), cx);
} }
struct GlobalPersonRegistry(Entity<PersonRegistry>); struct GlobalPersonRegistry(Entity<PersonRegistry>);
@@ -36,13 +36,13 @@ pub struct PersonRegistry {
persons: HashMap<PublicKey, Entity<Person>>, persons: HashMap<PublicKey, Entity<Person>>,
/// Set of public keys that have been seen /// Set of public keys that have been seen
seen: Rc<RefCell<HashSet<PublicKey>>>, seens: Rc<RefCell<HashSet<PublicKey>>>,
/// Sender for requesting metadata /// Sender for requesting metadata
sender: flume::Sender<PublicKey>, sender: flume::Sender<PublicKey>,
/// Tasks for asynchronous operations /// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 4]>, tasks: SmallVec<[Task<()>; 4]>,
} }
impl PersonRegistry { impl PersonRegistry {
@@ -57,13 +57,13 @@ impl PersonRegistry {
} }
/// Create a new person registry instance /// 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 nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
// Channel for communication between nostr and gpui // Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Dispatch>(100); 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![]; let mut tasks = smallvec![];
@@ -111,33 +111,16 @@ impl PersonRegistry {
}), }),
); );
tasks.push(
// Load all user profiles from the database // Load all user profiles from the database
cx.spawn(async move |this, cx| { cx.defer_in(window, |this, _window, cx| {
let result = cx this.load(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}");
}
};
}),
);
Self { Self {
persons: HashMap::new(), persons: HashMap::new(),
seen: Rc::new(RefCell::new(HashSet::new())), seens: Rc::new(RefCell::new(HashSet::new())),
sender: mta_tx, sender: mta_tx,
_tasks: tasks, tasks,
} }
} }
@@ -163,25 +146,21 @@ impl PersonRegistry {
let metadata = Metadata::from_json(&event.content).unwrap_or_default(); let metadata = Metadata::from_json(&event.content).unwrap_or_default();
let person = Person::new(event.pubkey, metadata); let person = Person::new(event.pubkey, metadata);
let val = Box::new(person); let val = Box::new(person);
// Send // Send
tx.send_async(Dispatch::Person(val)).await.ok(); tx.send_async(Dispatch::Person(val)).await.ok();
} }
Kind::ContactList => { Kind::ContactList => {
let public_keys = event.extract_public_keys(); let public_keys = event.extract_public_keys();
// Get metadata for all public keys // Get metadata for all public keys
get_metadata(client, public_keys).await.ok(); get_metadata(client, public_keys).await.ok();
} }
Kind::InboxRelays => { Kind::InboxRelays => {
let val = Box::new(event.into_owned()); let val = Box::new(event.into_owned());
// Send // Send
tx.send_async(Dispatch::Relays(val)).await.ok(); tx.send_async(Dispatch::Relays(val)).await.ok();
} }
Kind::Custom(10044) => { Kind::Custom(10044) => {
let val = Box::new(event.into_owned()); let val = Box::new(event.into_owned());
// Send // Send
tx.send_async(Dispatch::Announcement(val)).await.ok(); tx.send_async(Dispatch::Announcement(val)).await.ok();
} }
@@ -198,7 +177,7 @@ impl PersonRegistry {
loop { loop {
match flume::Selector::new() match flume::Selector::new()
.recv(rx, |result| result.ok()) .recv(rx, |result| result.ok())
.wait_timeout(Duration::from_secs(2)) .wait_timeout(Duration::from_secs(TIMEOUT))
{ {
Ok(Some(public_key)) => { Ok(Some(public_key)) => {
batch.insert(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(); 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 /// Set profile encryption keys announcement
fn set_announcement(&mut self, event: &Event, cx: &mut App) { fn set_announcement(&mut self, event: &Event, cx: &mut App) {
if let Some(person) = self.persons.get(&event.pubkey) {
let announcement = Announcement::from(event); let announcement = Announcement::from(event);
if let Some(person) = self.persons.get(&event.pubkey) {
person.update(cx, |person, cx| { person.update(cx, |person, cx| {
person.set_announcement(announcement); person.set_announcement(announcement);
cx.notify(); cx.notify();
}); });
} else {
let person =
Person::new(event.pubkey, Metadata::default()).with_announcement(announcement);
self.insert(person, cx);
} }
} }
/// Set messaging relays for a person /// Set messaging relays for a person
fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) { 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(); 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.update(cx, |person, cx| {
person.set_messaging_relays(urls); person.set_messaging_relays(urls);
cx.notify(); cx.notify();
}); });
} else {
let person = Person::new(event.pubkey, Metadata::default()).with_messaging_relays(urls);
self.insert(person, cx);
} }
} }
/// Insert batch of persons /// Insert batch of persons
fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) { fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
for person in persons.into_iter() { 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(); cx.notify();
} }
@@ -270,7 +290,7 @@ impl PersonRegistry {
} }
let public_key = *public_key; let public_key = *public_key;
let mut seen = self.seen.borrow_mut(); let mut seen = self.seens.borrow_mut();
if seen.insert(public_key) { if seen.insert(public_key) {
let sender = self.sender.clone(); let sender = self.sender.clone();
@@ -322,19 +342,3 @@ where
Ok(()) 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 /// Get profile public key
pub fn public_key(&self) -> PublicKey { pub fn public_key(&self) -> PublicKey {
self.public_key self.public_key
@@ -75,21 +90,11 @@ impl Person {
self.metadata.clone() self.metadata.clone()
} }
/// Set profile metadata
pub fn set_metadata(&mut self, metadata: Metadata) {
self.metadata = metadata;
}
/// Get profile encryption keys announcement /// Get profile encryption keys announcement
pub fn announcement(&self) -> Option<Announcement> { pub fn announcement(&self) -> Option<Announcement> {
self.announcement.clone() self.announcement.clone()
} }
/// Set profile encryption keys announcement
pub fn set_announcement(&mut self, announcement: Announcement) {
self.announcement = Some(announcement);
}
/// Get profile messaging relays /// Get profile messaging relays
pub fn messaging_relays(&self) -> &Vec<RelayUrl> { pub fn messaging_relays(&self) -> &Vec<RelayUrl> {
&self.messaging_relays &self.messaging_relays
@@ -100,14 +105,6 @@ impl Person {
self.messaging_relays.first().cloned() 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 /// Get profile avatar
pub fn avatar(&self) -> SharedString { pub fn avatar(&self) -> SharedString {
self.metadata() self.metadata()
@@ -115,8 +112,9 @@ impl Person {
.as_ref() .as_ref()
.filter(|picture| !picture.is_empty()) .filter(|picture| !picture.is_empty())
.map(|picture| { .map(|picture| {
let encoded_picture = urlencoding::encode(picture);
let url = format!( 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() url.into()
}) })
@@ -139,6 +137,24 @@ impl Person {
SharedString::from(shorten_pubkey(self.public_key(), 4)) 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 /// 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(); let Ok(pubkey) = public_key.to_bech32();
format!( format!(
"{}:{}", "{}...{}",
&pubkey[0..(len + 1)], &pubkey[0..(len + 1)],
&pubkey[pubkey.len() - len..] &pubkey[pubkey.len() - len..]
) )

View File

@@ -5,19 +5,19 @@ use std::hash::Hash;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, Context as AnyhowContext, Error}; use anyhow::{Context as AnyhowContext, Error, anyhow};
use gpui::{ use gpui::{
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled, App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
Task, Window, Task, Window, div, relative,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use settings::{AppSettings, AuthMode}; use settings::{AppSettings, AuthMode};
use smallvec::{smallvec, SmallVec}; use smallvec::{SmallVec, smallvec};
use state::NostrRegistry; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::notification::Notification; use ui::notification::Notification;
use ui::{v_flex, Disableable, IconName, Sizable, WindowExtension}; use ui::{Disableable, IconName, Sizable, WindowExtension, v_flex};
const AUTH_MESSAGE: &str = const AUTH_MESSAGE: &str =
"Approve the authentication request to allow Coop to continue sending or receiving events."; "Approve the authentication request to allow Coop to continue sending or receiving events.";
@@ -34,7 +34,10 @@ struct AuthRequest {
} }
impl 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 { Self {
challenge: challenge.into(), challenge: challenge.into(),
url, url,
@@ -106,22 +109,6 @@ impl RelayAuth {
tx.send_async(signal).await.ok(); 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 { RelayMessage::Ok {
event_id, message, .. event_id, message, ..
} => { } => {
@@ -273,7 +260,7 @@ impl RelayAuth {
fn response(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) { fn response(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) {
let settings = AppSettings::global(cx); let settings = AppSettings::global(cx);
let req = req.clone(); let req = req.clone();
let challenge = req.challenge().to_string(); let challenge = SharedString::from(req.challenge().to_string());
// Create a task for authentication // Create a task for authentication
let task = self.auth(&req, cx); let task = self.auth(&req, cx);
@@ -283,7 +270,7 @@ impl RelayAuth {
let url = req.url(); let url = req.url();
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
window.clear_notification(challenge, cx); window.clear_notification_by_id::<AuthNotification>(challenge, cx);
match result { match result {
Ok(_) => { Ok(_) => {
@@ -295,10 +282,19 @@ impl RelayAuth {
this.add_trusted_relay(url, cx); 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) => { 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. /// Build a notification for the authentication request.
fn notification(&self, req: &Arc<AuthRequest>, cx: &Context<Self>) -> Notification { fn notification(&self, req: &Arc<AuthRequest>, cx: &Context<Self>) -> Notification {
let req = req.clone(); let req = req.clone();
let challenge = SharedString::from(req.challenge.clone());
let url = SharedString::from(req.url().to_string()); let url = SharedString::from(req.url().to_string());
let entity = cx.entity().downgrade(); let entity = cx.entity().downgrade();
let loading = Rc::new(Cell::new(false)); let loading = Rc::new(Cell::new(false));
Notification::new() Notification::new()
.custom_id(SharedString::from(&req.challenge)) .type_id::<AuthNotification>(challenge)
.autohide(false) .autohide(false)
.icon(IconName::Info) .icon(IconName::Warning)
.title(SharedString::from("Authentication Required")) .title(SharedString::from("Authentication Required"))
.content(move |_window, cx| { .content(move |_this, _window, cx| {
v_flex() v_flex()
.gap_2() .gap_2()
.child(
div()
.text_sm() .text_sm()
.child(SharedString::from(AUTH_MESSAGE)) .line_height(relative(1.25))
.child(SharedString::from(AUTH_MESSAGE)),
)
.child( .child(
v_flex() v_flex()
.py_1() .py_1()
@@ -349,7 +350,7 @@ impl RelayAuth {
) )
.into_any_element() .into_any_element()
}) })
.action(move |_window, _cx| { .action(move |_this, _window, _cx| {
let view = entity.clone(); let view = entity.clone();
let req = req.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.workspace = true
nostr-sdk.workspace = true nostr-sdk.workspace = true
nostr-lmdb.workspace = true nostr-lmdb.workspace = true
nostr-memory.workspace = true
nostr-gossip-sqlite.workspace = true
nostr-connect.workspace = true nostr-connect.workspace = true
nostr-blossom.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"]; pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
/// Default bootstrap relays /// Default bootstrap relays
pub const BOOTSTRAP_RELAYS: [&str; 3] = [ pub const BOOTSTRAP_RELAYS: [&str; 4] = [
"wss://relay.damus.io",
"wss://relay.primal.net", "wss://relay.primal.net",
"wss://indexer.coracle.social", "wss://indexer.coracle.social",
"wss://user.kindpag.es", "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::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -6,20 +6,20 @@ use anyhow::{Context as AnyhowContext, Error, anyhow};
use common::config_dir; use common::config_dir;
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window}; use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window};
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use nostr_gossip_sqlite::prelude::*;
use nostr_lmdb::prelude::*; use nostr_lmdb::prelude::*;
use nostr_memory::prelude::*;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
mod blossom; mod blossom;
mod constants; mod constants;
mod device; mod device;
mod gossip;
mod nip05; mod nip05;
mod signer; mod signer;
pub use blossom::*; pub use blossom::*;
pub use constants::*; pub use constants::*;
pub use device::*; pub use device::*;
pub use gossip::*;
pub use nip05::*; pub use nip05::*;
pub use signer::*; pub use signer::*;
@@ -41,6 +41,23 @@ struct GlobalNostrRegistry(Entity<NostrRegistry>);
impl Global for GlobalNostrRegistry {} 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 /// Nostr Registry
#[derive(Debug)] #[derive(Debug)]
pub struct NostrRegistry { pub struct NostrRegistry {
@@ -53,21 +70,17 @@ pub struct NostrRegistry {
/// Local public keys /// Local public keys
npubs: Entity<Vec<PublicKey>>, npubs: Entity<Vec<PublicKey>>,
/// Custom gossip implementation
gossip: Entity<Gossip>,
/// App keys /// App keys
/// ///
/// Used for Nostr Connect and NIP-4e operations /// Used for Nostr Connect and NIP-4e operations
pub app_keys: Keys, app_keys: Keys,
/// Relay list state
pub relay_list_state: RelayState,
/// Tasks for asynchronous operations /// Tasks for asynchronous operations
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<()>>,
} }
impl EventEmitter<StateEvent> for NostrRegistry {}
impl NostrRegistry { impl NostrRegistry {
/// Retrieve the global nostr state /// Retrieve the global nostr state
pub fn global(cx: &App) -> Entity<Self> { pub fn global(cx: &App) -> Entity<Self> {
@@ -88,32 +101,43 @@ impl NostrRegistry {
// Construct the nostr npubs entity // Construct the nostr npubs entity
let npubs = cx.new(|_| vec![]); let npubs = cx.new(|_| vec![]);
// Construct the gossip entity // Construct the nostr gossip instance
let gossip = cx.new(|_| Gossip::default()); 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 // Construct the nostr lmdb instance
let lmdb = cx.foreground_executor().block_on(async move { let lmdb = cx.foreground_executor().block_on(async move {
NostrLmdb::open(config_dir().join("nostr")) NostrLmdb::open(config_dir().join("nostr"))
.await .await
.expect("Failed to initialize database") .expect("Failed to initialize database")
}); });
builder = builder.database(lmdb);
} else {
builder = builder.database(MemoryDatabase::unbounded())
}
// Construct the nostr client // Build the nostr client
let client = ClientBuilder::default() let client = builder.build();
.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();
// Run at the end of current cycle // Run at the end of current cycle
cx.defer_in(window, |this, _window, cx| { cx.defer_in(window, |this, _window, cx| {
this.connect(cx); this.connect(cx);
this.handle_notifications(cx);
}); });
Self { Self {
@@ -121,8 +145,6 @@ impl NostrRegistry {
signer, signer,
npubs, npubs,
app_keys, app_keys,
gossip,
relay_list_state: RelayState::Idle,
tasks: vec![], tasks: vec![],
} }
} }
@@ -142,94 +164,57 @@ impl NostrRegistry {
self.npubs.clone() self.npubs.clone()
} }
/// Get the app keys
pub fn keys(&self) -> Keys {
self.app_keys.clone()
}
/// Connect to the bootstrapping relays /// Connect to the bootstrapping relays
fn connect(&mut self, cx: &mut Context<Self>) { fn connect(&mut self, cx: &mut Context<Self>) {
let client = self.client(); let client = self.client();
self.tasks.push(cx.spawn(async move |this, cx| { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
cx.background_executor()
.await_on_background(async move {
// Add search relay to the relay pool // Add search relay to the relay pool
for url in SEARCH_RELAYS.into_iter() { 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 // Add bootstrap relay to the relay pool
for url in BOOTSTRAP_RELAYS.into_iter() { for url in BOOTSTRAP_RELAYS.into_iter() {
client.add_relay(url).await.ok(); client.add_relay(url).await?;
} }
// Connect to all added relays // Connect to all added relays
client.connect().and_wait(Duration::from_secs(2)).await; client.connect().await;
})
.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| { this.update(cx, |this, cx| {
cx.emit(StateEvent::Connected);
this.get_npubs(cx); this.get_npubs(cx);
})?; })
.ok();
Ok(())
}));
} }
Err(e) => {
/// Handle nostr notifications this.update(cx, |_this, cx| {
fn handle_notifications(&mut self, cx: &mut Context<Self>) { cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
let client = self.client(); })
let gossip = self.gossip.downgrade(); .ok();
// 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;
} }
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 /// Get all used npubs
fn get_npubs(&mut self, cx: &mut Context<Self>) { fn get_npubs(&mut self, cx: &mut Context<Self>) {
let npubs = self.npubs.downgrade(); let npubs = self.npubs.downgrade();
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move { let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
let dir = config_dir().join("keys"); let dir = config_dir().join("keys");
// Ensure keys directory exists // Ensure keys directory exists
@@ -269,25 +254,26 @@ impl NostrRegistry {
true => { true => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.create_identity(cx); this.create_identity(cx);
})?; })
.ok();
} }
false => { false => {
// TODO: auto login // TODO: auto login
npubs.update(cx, |this, cx| { npubs
.update(cx, |this, cx| {
this.extend(public_keys); this.extend(public_keys);
cx.notify(); cx.notify();
})?; })
.ok();
} }
}, },
Err(e) => { Err(e) => {
log::error!("Failed to get npubs: {e}"); this.update(cx, |_this, cx| {
this.update(cx, |this, cx| { cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
this.create_identity(cx); })
})?; .ok();
} }
} }
Ok(())
})); }));
} }
@@ -307,74 +293,49 @@ impl NostrRegistry {
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let signer = async_keys.into_nostr_signer(); let signer = async_keys.into_nostr_signer();
// Get default relay list // Construct relay list event
let relay_list = default_relay_list(); 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 event = EventBuilder::relay_list(relay_list).sign(&signer).await?;
let output = client
// Publish relay list
client
.send_event(&event) .send_event(&event)
.to(BOOTSTRAP_RELAYS) .to(BOOTSTRAP_RELAYS)
.ok_timeout(Duration::from_secs(TIMEOUT)) .ok_timeout(Duration::from_secs(TIMEOUT))
.await?; .await?;
log::info!("Sent gossip relay list: {output:?}");
// Construct the default metadata // Construct the default metadata
let name = petname::petname(2, "-").unwrap_or("Cooper".to_string()); let name = petname::petname(2, "-").unwrap_or("Cooper".to_string());
let avatar = Url::parse(&format!("https://avatar.vercel.sh/{name}")).unwrap(); let avatar = Url::parse(&format!("https://avatar.vercel.sh/{name}")).unwrap();
let metadata = Metadata::new().display_name(&name).picture(avatar); let metadata = Metadata::new().display_name(&name).picture(avatar);
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
// Publish metadata event // Publish metadata event
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
client client
.send_event(&event) .send_event(&event)
.to(&write_urls) .to_nip65()
.ack_policy(AckPolicy::none()) .ack_policy(AckPolicy::none())
.await?; .await?;
// Construct the default contact list // Construct the default contact list
let contacts = vec![Contact::new(PublicKey::parse(COOP_PUBKEY).unwrap())]; let contacts = vec![Contact::new(PublicKey::parse(COOP_PUBKEY).unwrap())];
let event = EventBuilder::contact_list(contacts).sign(&signer).await?;
// Publish contact list event // Publish contact list event
let event = EventBuilder::contact_list(contacts).sign(&signer).await?;
client client
.send_event(&event) .send_event(&event)
.to(&write_urls) .to_nip65()
.ack_policy(AckPolicy::none()) .ack_policy(AckPolicy::none())
.await?; .await?;
// Construct the default messaging relay list // Construct the default messaging relay list
let relays = default_messaging_relays(); let relays = default_messaging_relays();
let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?;
// Ensure connected to all relays
for url in relays.iter() {
client.add_relay(url).and_connect().await?;
}
// Publish messaging relay list event // Publish messaging relay list event
let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?;
client client
.send_event(&event) .send_event(&event)
.to(&write_urls) .to_nip65()
.ack_policy(AckPolicy::none()) .ack_policy(AckPolicy::none())
.await?; .await?;
@@ -385,15 +346,20 @@ impl NostrRegistry {
}); });
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
// Wait for the task to complete match task.await {
task.await?; Ok(_) => {
// Set signer
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_signer(keys, 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) => { Ok(public_key) => {
// Update states // Update states
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.ensure_relay_list(&public_key, cx);
// Add public key to npubs if not already present // Add public key to npubs if not already present
this.npubs.update(cx, |this, cx| { this.npubs.update(cx, |this, cx| {
if !this.contains(&public_key) { if !this.contains(&public_key) {
@@ -479,22 +446,18 @@ impl NostrRegistry {
cx.notify(); cx.notify();
} }
}); });
// Ensure relay list for the user
this.ensure_relay_list(cx);
// Emit signer changed event // Emit signer changed event
cx.emit(SignerEvent::Set); cx.emit(StateEvent::SignerSet);
})?; })
.ok();
} }
Err(e) => { Err(e) => {
this.update(cx, |_this, cx| { 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| { self.tasks.push(cx.spawn(async move |this, cx| {
let key_path = keys_dir.join(format!("{}.npub", npub)); 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.update(cx, |this, cx| {
this.npubs().update(cx, |this, cx| { this.npubs().update(cx, |this, cx| {
this.retain(|k| k != &public_key); this.retain(|k| k != &public_key);
cx.notify(); cx.notify();
}); });
})?; })
.ok();
Ok(())
})); }));
} }
@@ -533,16 +495,16 @@ impl NostrRegistry {
Ok(_) => { Ok(_) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_signer(keys, cx); this.set_signer(keys, cx);
})?; })
.ok();
} }
Err(e) => { Err(e) => {
this.update(cx, |_this, cx| { 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 { match task.await {
Ok((public_key, uri)) => { Ok((public_key, uri)) => {
let username = public_key.to_bech32().unwrap(); let username = public_key.to_bech32().unwrap();
let write_credential = this.read_with(cx, |_this, cx| { let write_credential = this
cx.write_credentials(&username, "nostrconnect", uri.to_string().as_bytes()) .read_with(cx, |_this, cx| {
})?; cx.write_credentials(
&username,
"nostrconnect",
uri.to_string().as_bytes(),
)
})
.unwrap();
match write_credential.await { match write_credential.await {
Ok(_) => { Ok(_) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_signer(nip46, cx); this.set_signer(nip46, cx);
})?; })
.ok();
} }
Err(e) => { Err(e) => {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(SignerEvent::Error(e.to_string())); cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})?; })
.ok();
} }
} }
} }
Err(e) => { Err(e) => {
this.update(cx, |_this, cx| { 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 pub fn ensure_relay_list(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
fn set_relay_state(&mut self, state: RelayState, cx: &mut Context<Self>) { let task = self.get_event(public_key, Kind::RelayList, cx);
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);
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
let result = task.await?; match task.await {
Ok(_) => {
// Update state this.update(cx, |_this, cx| {
this.update(cx, |this, cx| { cx.emit(StateEvent::RelayConnected);
this.relay_list_state = result; })
cx.notify(); .ok();
})?; }
Err(e) => {
Ok(()) 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 /// Get an event with the given author and kind.
fn verify_relay_list(&mut self, cx: &mut Context<Self>) -> Task<Result<RelayState, Error>> { pub fn get_event(
&self,
author: &PublicKey,
kind: Kind,
cx: &App,
) -> Task<Result<Event, Error>> {
let client = self.client(); let client = self.client();
let public_key = *author;
cx.background_spawn(async move { cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?; let filter = Filter::new().kind(kind).author(public_key).limit(1);
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 mut stream = client let mut stream = client
.stream_events(target) .stream_events(filter)
.timeout(Duration::from_secs(TIMEOUT)) .timeout(Duration::from_millis(800))
.await?; .await?;
while let Some((_url, res)) = stream.next().await { while let Some((_url, res)) = stream.next().await {
match res { if let Ok(event) = res {
Ok(event) => { return Ok(event);
log::info!("Received relay list event: {event:?}");
return Ok(RelayState::Configured);
}
Err(e) => {
log::error!("Failed to receive relay list event: {e}");
}
} }
} }
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 /// Get the public key of a NIP-05 address
pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> { pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
let client = self.client(); let client = self.client();
@@ -905,8 +765,6 @@ impl NostrRegistry {
} }
} }
impl EventEmitter<SignerEvent> for NostrRegistry {}
/// Get or create a new app keys /// Get or create a new app keys
fn get_or_init_app_keys() -> Result<Keys, Error> { fn get_or_init_app_keys() -> Result<Keys, Error> {
let dir = config_dir().join(".app_keys"); let dir = config_dir().join(".app_keys");
@@ -932,52 +790,6 @@ fn get_or_init_app_keys() -> Result<Keys, Error> {
Ok(keys) 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>)> { fn default_relay_list() -> Vec<(RelayUrl, Option<RelayMetadata>)> {
vec![ 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)] #[derive(Debug, Clone)]
pub struct CoopAuthUrlHandler; pub struct CoopAuthUrlHandler;

View File

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

View File

@@ -4,6 +4,8 @@ use std::rc::Rc;
use gpui::{App, Global, Pixels, SharedString, Window, px}; use gpui::{App, Global, Pixels, SharedString, Window, px};
mod colors; mod colors;
mod geometry;
mod notification;
mod platform_kind; mod platform_kind;
mod registry; mod registry;
mod scale; mod scale;
@@ -11,6 +13,8 @@ mod scrollbar_mode;
mod theme; mod theme;
pub use colors::*; pub use colors::*;
pub use geometry::*;
pub use notification::*;
pub use platform_kind::PlatformKind; pub use platform_kind::PlatformKind;
pub use registry::*; pub use registry::*;
pub use scale::*; pub use scale::*;
@@ -82,6 +86,9 @@ pub struct Theme {
/// Show the scrollbar mode, default: scrolling /// Show the scrollbar mode, default: scrolling
pub scrollbar_mode: ScrollbarMode, pub scrollbar_mode: ScrollbarMode,
/// Notification settings
pub notification: NotificationSettings,
/// Platform kind /// Platform kind
pub platform: PlatformKind, pub platform: PlatformKind,
} }
@@ -200,10 +207,11 @@ impl From<ThemeFamily> for Theme {
Theme { Theme {
font_size: px(15.), font_size: px(15.),
font_family: font_family.into(), font_family: font_family.into(),
radius: px(5.), radius: px(6.),
radius_lg: px(10.), radius_lg: px(10.),
shadow: true, shadow: true,
scrollbar_mode: ScrollbarMode::default(), scrollbar_mode: ScrollbarMode::default(),
notification: NotificationSettings::default(),
mode, mode,
colors: *colors, colors: *colors,
theme: Rc::new(family), 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 //! 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 //! https://github.com/zed-industries/zed/blob/b06f4088a3565c5e30663106ff79c1ced645d87a/crates/gpui/src/elements/anchored.rs
use gpui::{ 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, InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
Window, Window, point, px,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use theme::Anchor;
use crate::Anchor;
/// The state that the anchored element element uses to track its children. /// The state that the anchored element element uses to track its children.
pub struct AnchoredState { pub struct AnchoredState {

View File

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

View File

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

View File

@@ -2,22 +2,24 @@ use std::sync::Arc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations, Edges, Entity,
Edges, Entity, EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div, px,
}; };
use theme::CLIENT_SIDE_DECORATION_ROUNDING; 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; use crate::ElementExt;
pub mod dock; #[allow(clippy::module_inception)]
pub mod panel; mod dock;
pub mod stack_panel; mod panel;
pub mod tab_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]); actions!(dock, [ToggleZoom, ClosePanel]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
use gpui::{ use gpui::{
px, relative, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId, EntityId,
EntityId, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point,
Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window, px, relative,
}; };
use theme::AxisExt;
use crate::AxisExt;
/// Make a scrollable mask element to cover the parent view with the mouse wheel event listening. /// 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, Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, fill,
point, px, relative, size, point, px, relative, size,
}; };
use theme::{ActiveTheme, ScrollbarMode}; use theme::{ActiveTheme, AxisExt, ScrollbarMode};
use crate::AxisExt;
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH) /// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
const WIDTH: Pixels = px(1. * 2. + 8.); const WIDTH: Pixels = px(1. * 2. + 8.);
@@ -54,7 +52,7 @@ impl ScrollbarHandle for ScrollHandle {
} }
fn content_size(&self) -> Size<Pixels> { 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> { fn content_size(&self) -> Size<Pixels> {
let base_handle = &self.0.borrow().base_handle; 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> { 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) { 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 serde::{Deserialize, Serialize};
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -46,6 +46,30 @@ pub trait StyledExt: Styled + Sized {
self.flex().flex_col() 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_thin, THIN);
font_weight!(font_extralight, EXTRA_LIGHT); font_weight!(font_extralight, EXTRA_LIGHT);
font_weight!(font_light, LIGHT); font_weight!(font_light, LIGHT);

View File

@@ -4,13 +4,13 @@ use std::time::Duration;
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, px, white, Animation, AnimationExt as _, AnyElement, App, Element, ElementId, Animation, AnimationExt as _, AnyElement, App, Element, ElementId, GlobalElementId,
GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, Styled as _,
Styled as _, Window, 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)>>; type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;

View File

@@ -1,74 +1,557 @@
use gpui::prelude::FluentBuilder; use std::rc::Rc;
use gpui::{
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
RenderOnce, StatefulInteractiveElement, Styled, Window,
};
use theme::{ActiveTheme, TABBAR_HEIGHT};
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; 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)] #[derive(IntoElement)]
pub struct Tab { pub struct Tab {
ix: usize, ix: usize,
base: Div, base: Div,
label: Option<AnyElement>, pub(super) label: Option<SharedString>,
icon: Option<Icon>,
prefix: Option<AnyElement>, prefix: Option<AnyElement>,
pub(super) tab_bar_prefix: Option<bool>,
suffix: Option<AnyElement>, suffix: Option<AnyElement>,
disabled: bool, children: Vec<AnyElement>,
selected: bool, variant: TabVariant,
size: Size, size: Size,
pub(super) disabled: bool,
pub(super) selected: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
} }
impl Tab { impl From<&'static str> for Tab {
pub fn new() -> Self { fn from(label: &'static str) -> Self {
Self { Self::new().label(label)
ix: 0,
base: div(),
label: None,
disabled: false,
selected: false,
prefix: None,
suffix: None,
size: Size::default(),
} }
} }
/// Set label for the tab. impl From<String> for Tab {
pub fn label(mut self, label: impl Into<AnyElement>) -> Self { fn from(label: String) -> Self {
self.label = Some(label.into()); Self::new().label(label)
self }
} }
/// Set the left side of the tab impl From<SharedString> for Tab {
pub fn prefix(mut self, prefix: impl Into<AnyElement>) -> Self { fn from(label: SharedString) -> Self {
self.prefix = Some(prefix.into()); Self::new().label(label)
self }
} }
/// Set the right side of the tab impl From<Icon> for Tab {
pub fn suffix(mut self, suffix: impl Into<AnyElement>) -> Self { fn from(icon: Icon) -> Self {
self.suffix = Some(suffix.into()); Self::default().icon(icon)
self }
} }
/// Set disabled state to the tab impl From<IconName> for Tab {
pub fn disabled(mut self, disabled: bool) -> Self { fn from(icon_name: IconName) -> Self {
self.disabled = disabled; Self::default().icon(Icon::new(icon_name))
self
}
/// Set index to the tab.
pub fn ix(mut self, ix: usize) -> Self {
self.ix = ix;
self
} }
} }
impl Default for Tab { impl Default for Tab {
fn default() -> Self { 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 { impl RenderOnce for Tab {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let (text_color, hover_text_color, bg_color, border_color) = let mut tab_style = if self.selected {
match (self.selected, self.disabled) { self.variant.selected(cx)
(true, false) => ( } else {
cx.theme().tab_active_foreground, self.variant.normal(cx)
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,
),
}; };
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 self.base
.id(self.ix) .id(self.ix)
.h(TABBAR_HEIGHT)
.px_4()
.relative()
.flex() .flex()
.flex_wrap()
.gap_1()
.items_center() .items_center()
.flex_shrink_0() .flex_shrink_0()
.cursor_pointer() .h(height)
.overflow_hidden() .overflow_hidden()
.text_xs() .text_color(tab_style.fg)
.text_ellipsis() .map(|this| match self.size {
.text_color(text_color) Size::XSmall => this.text_xs(),
.bg(bg_color) Size::Large => this.text_base(),
.border_l(px(1.)) _ => this.text_sm(),
.border_r(px(1.)) })
.border_color(border_color) .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| { .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.prefix, |this, prefix| this.child(prefix))
.when_some(self.suffix, |this, suffix| this.child(suffix)) .child(
.on_mouse_down(MouseButton::Left, |_ev, _window, cx| { 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(); 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 _; use gpui::prelude::FluentBuilder as _;
#[cfg(not(target_os = "windows"))]
use gpui::Pixels;
use gpui::{ use gpui::{
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, ParentElement, RenderOnce, AnyElement, App, Corner, Div, Edges, ElementId, InteractiveElement, IntoElement, ParentElement,
ScrollHandle, StatefulInteractiveElement as _, StyleRefinement, Styled, Window, RenderOnce, ScrollHandle, Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled,
Window, div, px,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use theme::ActiveTheme; 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)] #[derive(IntoElement)]
pub struct TabBar { pub struct TabBar {
base: Div, base: Stateful<Div>,
style: StyleRefinement, style: StyleRefinement,
scroll_handle: Option<ScrollHandle>, scroll_handle: Option<ScrollHandle>,
prefix: Option<AnyElement>, prefix: Option<AnyElement>,
suffix: Option<AnyElement>, suffix: Option<AnyElement>,
children: SmallVec<[Tab; 2]>,
last_empty_space: AnyElement, last_empty_space: AnyElement,
children: SmallVec<[AnyElement; 2]>, selected_index: Option<usize>,
variant: TabVariant,
size: Size, size: Size,
menu: bool,
on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
} }
impl TabBar { impl TabBar {
pub fn new() -> Self { /// Create a new TabBar.
pub fn new(id: impl Into<ElementId>) -> Self {
Self { Self {
base: h_flex().px(px(-1.)), base: div().id(id).px(px(-1.)),
style: StyleRefinement::default(), style: StyleRefinement::default(),
scroll_handle: None,
children: SmallVec::new(), children: SmallVec::new(),
scroll_handle: None,
prefix: None, prefix: None,
suffix: None, suffix: None,
variant: TabVariant::default(),
size: Size::default(), size: Size::default(),
last_empty_space: div().w_3().into_any_element(), 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. /// Track the scroll of the TabBar.
pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
self.scroll_handle = Some(scroll_handle.clone()); self.scroll_handle = Some(scroll_handle.clone());
@@ -54,27 +105,39 @@ impl TabBar {
self 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. /// Set the last empty space element of the TabBar.
pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self { pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self {
self.last_empty_space = last_empty_space.into_any_element(); self.last_empty_space = last_empty_space.into_any_element();
self self
} }
#[cfg(not(target_os = "windows"))] /// Set the on_click callback of the TabBar, the first parameter is the index of the clicked tab.
pub fn height(window: &mut Window) -> Pixels { ///
(1.75 * window.rem_size()).max(px(36.)) /// 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,
impl Default for TabBar { {
fn default() -> Self { self.on_click = Some(Rc::new(on_click));
Self::new() self
}
}
impl ParentElement for TabBar {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
} }
} }
@@ -92,15 +155,69 @@ impl Sizable for TabBar {
} }
impl RenderOnce 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 self.base
.group("tab-bar") .group("tab-bar")
.relative() .relative()
.refine_style(&self.style) .flex()
.bg(cx.theme().surface_background) .items_center()
.child( .bg(bg)
.text_color(cx.theme().tab_foreground)
.when(
self.variant == TabVariant::Underline || self.variant == TabVariant::Tab,
|this| {
this.child(
div() div()
.id("border-bottom") .id("border-b")
.absolute() .absolute()
.left_0() .left_0()
.bottom_0() .bottom_0()
@@ -108,21 +225,66 @@ impl RenderOnce for TabBar {
.border_b_1() .border_b_1()
.border_color(cx.theme().border), .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)) .when_some(self.prefix, |this, prefix| this.child(prefix))
.child( .child(
h_flex() h_flex()
.id("tabs") .id("tabs")
.flex_grow() .flex_1()
.overflow_x_scroll() .overflow_x_scroll()
.when_some(self.scroll_handle, |this, scroll_handle| { .when_some(self.scroll_handle, |this, scroll_handle| {
this.track_scroll(&scroll_handle) this.track_scroll(&scroll_handle)
}) })
.children(self.children) .gap(gap)
.when(self.suffix.is_some(), |this| { .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) 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)) .when_some(self.suffix, |this, suffix| this.child(suffix))
} }
} }

View File

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