chore: refactor the global state and improve signer (#56)

* refactor

* update

* .

* rustfmt

* .

* .

* .

* .

* .

* add document

* .

* add logout

* handle error

* chore: update gpui

* adjust timeout
This commit is contained in:
reya
2025-06-07 14:52:21 +07:00
committed by GitHub
parent 50beaebd2c
commit e687204361
73 changed files with 1871 additions and 1504 deletions

189
Cargo.lock generated
View File

@@ -2,21 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "account"
version = "0.1.5"
dependencies = [
"anyhow",
"common",
"global",
"gpui",
"log",
"nostr-sdk",
"smallvec",
"smol",
"ui",
]
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.24.2" version = "0.24.2"
@@ -417,7 +402,7 @@ dependencies = [
"gpui", "gpui",
"log", "log",
"nostr-sdk", "nostr-sdk",
"reqwest 0.12.18", "reqwest 0.12.19",
"smol", "smol",
"tempfile", "tempfile",
] ]
@@ -497,9 +482,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "base64ct" name = "base64ct"
version = "1.7.3" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]] [[package]]
name = "bech32" name = "bech32"
@@ -754,9 +739,9 @@ checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.17.0" version = "3.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
@@ -851,9 +836,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.24" version = "1.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@@ -934,7 +919,6 @@ dependencies = [
name = "chats" name = "chats"
version = "0.1.5" version = "0.1.5"
dependencies = [ dependencies = [
"account",
"anyhow", "anyhow",
"chrono", "chrono",
"common", "common",
@@ -1078,7 +1062,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b" source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@@ -1156,7 +1140,6 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
name = "coop" name = "coop"
version = "0.1.5" version = "0.1.5"
dependencies = [ dependencies = [
"account",
"anyhow", "anyhow",
"auto_update", "auto_update",
"chats", "chats",
@@ -1459,7 +1442,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#a387bf5f54edce7558dec5c3804b03b51cbbfe9b" source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1802,9 +1785,9 @@ dependencies = [
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.1" version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide",
@@ -2199,7 +2182,12 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
name = "global" name = "global"
version = "0.1.5" version = "0.1.5"
dependencies = [ dependencies = [
"anyhow",
"dirs 5.0.1", "dirs 5.0.1",
"futures",
"log",
"nostr-connect",
"nostr-keyring",
"nostr-sdk", "nostr-sdk",
"rustls", "rustls",
"smol", "smol",
@@ -2276,7 +2264,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b" source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@@ -2313,6 +2301,7 @@ dependencies = [
"image", "image",
"inventory", "inventory",
"itertools 0.14.0", "itertools 0.14.0",
"libc",
"log", "log",
"lyon", "lyon",
"media", "media",
@@ -2368,7 +2357,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#a387bf5f54edce7558dec5c3804b03b51cbbfe9b" source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -2474,12 +2463,6 @@ dependencies = [
"heed-traits", "heed-traits",
] ]
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.5.1" version = "0.5.1"
@@ -2597,7 +2580,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#a387bf5f54edce7558dec5c3804b03b51cbbfe9b" source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -2614,7 +2597,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#a387bf5f54edce7558dec5c3804b03b51cbbfe9b" source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
@@ -2649,9 +2632,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.27.6" version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [ dependencies = [
"http", "http",
"hyper", "hyper",
@@ -2683,9 +2666,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.13" version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
@@ -2704,7 +2687,7 @@ dependencies = [
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
"windows-registry 0.4.0", "windows-registry 0.5.2",
] ]
[[package]] [[package]]
@@ -3064,6 +3047,20 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "keyring"
version = "3.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1961983669d57bdfe6c0f3ef8e4c229b5ef751afcc7d87e4271d2f71f6ccfa8b"
dependencies = [
"byteorder",
"linux-keyutils",
"log",
"security-framework 2.11.1",
"security-framework 3.2.0",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "khronos-egl" name = "khronos-egl"
version = "6.0.0" version = "6.0.0"
@@ -3171,6 +3168,16 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "linux-keyutils"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e"
dependencies = [
"bitflags 2.9.1",
"libc",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.15" version = "0.4.15"
@@ -3331,7 +3338,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b" source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen 0.71.1", "bindgen 0.71.1",
@@ -3533,7 +3540,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.42.1" version = "0.42.1"
source = "git+https://github.com/rust-nostr/nostr#08b421634cee639d50d0f592db42350e337d3bde" source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
dependencies = [ dependencies = [
"aes", "aes",
"base64", "base64",
@@ -3545,8 +3552,7 @@ dependencies = [
"chacha20poly1305", "chacha20poly1305",
"getrandom 0.2.16", "getrandom 0.2.16",
"instant", "instant",
"regex", "reqwest 0.12.19",
"reqwest 0.12.18",
"scrypt", "scrypt",
"secp256k1", "secp256k1",
"serde", "serde",
@@ -3558,7 +3564,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.42.0" version = "0.42.0"
source = "git+https://github.com/rust-nostr/nostr#08b421634cee639d50d0f592db42350e337d3bde" source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3570,7 +3576,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.42.0" version = "0.42.0"
source = "git+https://github.com/rust-nostr/nostr#08b421634cee639d50d0f592db42350e337d3bde" source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
dependencies = [ dependencies = [
"flatbuffers", "flatbuffers",
"lru", "lru",
@@ -3578,10 +3584,19 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "nostr-keyring"
version = "0.42.0"
source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
dependencies = [
"keyring",
"nostr",
]
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.42.0" version = "0.42.0"
source = "git+https://github.com/rust-nostr/nostr#08b421634cee639d50d0f592db42350e337d3bde" source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"heed", "heed",
@@ -3594,7 +3609,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-relay-pool" name = "nostr-relay-pool"
version = "0.42.0" version = "0.42.0"
source = "git+https://github.com/rust-nostr/nostr#08b421634cee639d50d0f592db42350e337d3bde" source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -3610,7 +3625,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.42.0" version = "0.42.0"
source = "git+https://github.com/rust-nostr/nostr#08b421634cee639d50d0f592db42350e337d3bde" source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3745,11 +3760,11 @@ dependencies = [
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.16.0" version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
dependencies = [ dependencies = [
"hermit-abi 0.3.9", "hermit-abi",
"libc", "libc",
] ]
@@ -4234,7 +4249,7 @@ checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"concurrent-queue", "concurrent-queue",
"hermit-abi 0.5.1", "hermit-abi",
"pin-project-lite", "pin-project-lite",
"rustix 1.0.7", "rustix 1.0.7",
"tracing", "tracing",
@@ -4295,9 +4310,9 @@ dependencies = [
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.32" version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"syn 2.0.101", "syn 2.0.101",
@@ -4615,9 +4630,9 @@ dependencies = [
[[package]] [[package]]
name = "read-fonts" name = "read-fonts"
version = "0.29.2" version = "0.29.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f96bfbb7df43d34a2b7b8582fcbcb676ba02a763265cb90bc8aabfd62b57d64" checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"font-types", "font-types",
@@ -4646,7 +4661,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b" source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
"workspace-hack", "workspace-hack",
@@ -4731,9 +4746,9 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.18" version = "0.12.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5" checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
@@ -4783,7 +4798,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#a387bf5f54edce7558dec5c3804b03b51cbbfe9b" source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -5254,7 +5269,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b" source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@@ -5346,9 +5361,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.8" version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@@ -5477,9 +5492,9 @@ dependencies = [
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.0" version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "smol" name = "smol"
@@ -5606,7 +5621,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#a387bf5f54edce7558dec5c3804b03b51cbbfe9b" source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -6117,9 +6132,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.22" version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
@@ -6129,18 +6144,18 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.9" version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.26" version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@@ -6152,9 +6167,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_write" name = "toml_write"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]] [[package]]
name = "tower" name = "tower"
@@ -6173,9 +6188,9 @@ dependencies = [
[[package]] [[package]]
name = "tower-http" name = "tower-http"
version = "0.6.4" version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"bytes", "bytes",
@@ -6214,9 +6229,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.28" version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -6225,9 +6240,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.33" version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable", "valuable",
@@ -6521,7 +6536,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b" source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",
@@ -7864,9 +7879,9 @@ dependencies = [
[[package]] [[package]]
name = "zune-jpeg" name = "zune-jpeg"
version = "0.4.14" version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" checksum = "3e4a518c0ea2576f4da876349d7f67a7be489297cd77c2cf9e04c2e05fcd3974"
dependencies = [ dependencies = [
"zune-core", "zune-core",
] ]

View File

@@ -16,7 +16,7 @@ gpui = { git = "https://github.com/zed-industries/zed" }
reqwest_client = { git = "https://github.com/zed-industries/zed" } reqwest_client = { git = "https://github.com/zed-industries/zed" }
# Nostr # Nostr
nostr = { git = "https://github.com/rust-nostr/nostr", features = ["parser"] } nostr = { git = "https://github.com/rust-nostr/nostr" }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = ["lmdb", "nip96", "nip59", "nip49", "nip44", "nip05"] } nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = ["lmdb", "nip96", "nip59", "nip49", "nip44", "nip05"] }
nostr-connect = { git = "https://github.com/rust-nostr/nostr" } nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
nostr-keyring = { git = "https://github.com/rust-nostr/nostr" } nostr-keyring = { git = "https://github.com/rust-nostr/nostr" }

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

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20.25 12H9m11.25 0-4.5 4.5m4.5-4.5-4.5-4.5m-4.5 12.75h-5.5a2 2 0 0 1-2-2V5.75a2 2 0 0 1 2-2h5.5"/>
</svg>

After

Width:  |  Height:  |  Size: 302 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linejoin="round" stroke-width="1.5" d="m7.878 5.214-.703-.162a1.77 1.77 0 0 0-2.123 2.123l.162.703a2 2 0 0 1-.84 2.114l-.854.57a1.728 1.728 0 0 0 0 2.876l.855.57a2 2 0 0 1 .84 2.114l-.163.703a1.77 1.77 0 0 0 2.123 2.123l.703-.162a2 2 0 0 1 2.114.84l.57.854a1.728 1.728 0 0 0 2.876 0l.57-.855a2 2 0 0 1 2.114-.84l.703.163a1.77 1.77 0 0 0 2.123-2.123l-.162-.703a2 2 0 0 1 .84-2.114l.854-.57a1.728 1.728 0 0 0 0-2.876l-.855-.57a2 2 0 0 1-.84-2.114l.163-.703a1.77 1.77 0 0 0-2.123-2.123l-.703.162a2 2 0 0 1-2.114-.84l-.57-.854a1.728 1.728 0 0 0-2.876 0l-.57.855a2 2 0 0 1-2.114.84Z"/>
<path stroke="currentColor" stroke-linejoin="round" stroke-width="1.5" d="M14.75 12a2.75 2.75 0 1 1-5.5 0 2.75 2.75 0 0 1 5.5 0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 855 B

View File

@@ -1,17 +0,0 @@
[package]
name = "account"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
ui = { path = "../ui" }
common = { path = "../common" }
global = { path = "../global" }
gpui.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
smol.workspace = true
smallvec.workspace = true
log.workspace = true

View File

@@ -1,236 +0,0 @@
use std::time::Duration;
use anyhow::Error;
use global::{
constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID},
get_client,
};
use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
use nostr_sdk::prelude::*;
use ui::{notification::Notification, ContextModal};
struct GlobalAccount(Entity<Account>);
impl Global for GlobalAccount {}
pub fn init(cx: &mut App) {
Account::set_global(cx.new(|_| Account { profile: None }), cx);
}
#[derive(Debug, Clone)]
pub struct Account {
pub profile: Option<Profile>,
}
impl Account {
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalAccount>().0.clone()
}
pub fn get_global(cx: &App) -> &Self {
cx.global::<GlobalAccount>().0.read(cx)
}
pub fn set_global(account: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalAccount(account));
}
/// Login to the account using the given signer.
pub fn login<S>(&mut self, signer: S, window: &mut Window, cx: &mut Context<Self>)
where
S: NostrSigner + 'static,
{
let task: Task<Result<Profile, Error>> = cx.background_spawn(async move {
let client = get_client();
let public_key = signer.get_public_key().await?;
// Update signer
client.set_signer(signer).await;
// Fetch user's metadata
let metadata = client
.fetch_metadata(public_key, Duration::from_secs(2))
.await?
.unwrap_or_default();
Ok(Profile::new(public_key, metadata))
});
cx.spawn_in(window, async move |this, cx| match task.await {
Ok(profile) => {
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.profile(profile, cx);
cx.defer_in(window, |this, _, cx| {
this.subscribe(cx);
});
})
.ok();
})
.ok();
}
Err(e) => {
cx.update(|window, cx| {
window.push_notification(Notification::error(e.to_string()), cx)
})
.ok();
}
})
.detach();
}
/// Create a new account with the given metadata.
pub fn new_account(&mut self, metadata: Metadata, window: &mut Window, cx: &mut Context<Self>) {
const DEFAULT_NIP_65_RELAYS: [&str; 4] = [
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://relay.nostr.net",
"wss://nos.lol",
];
const DEFAULT_MESSAGING_RELAYS: [&str; 2] =
["wss://auth.nostr1.com", "wss://relay.0xchat.com"];
let keys = Keys::generate();
let public_key = keys.public_key();
let task: Task<Result<Profile, Error>> = cx.background_spawn(async move {
let client = get_client();
// Update signer
client.set_signer(keys).await;
// Set metadata
client.set_metadata(&metadata).await?;
// Create relay list
let tags: Vec<Tag> = DEFAULT_NIP_65_RELAYS
.into_iter()
.filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay_metadata(url, None))
} else {
None
}
})
.collect();
let builder = EventBuilder::new(Kind::RelayList, "").tags(tags);
if let Err(e) = client.send_event_builder(builder).await {
log::error!("Failed to send relay list event: {}", e);
};
// Create messaging relay list
let tags: Vec<Tag> = DEFAULT_MESSAGING_RELAYS
.into_iter()
.filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay(url))
} else {
None
}
})
.collect();
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
if let Err(e) = client.send_event_builder(builder).await {
log::error!("Failed to send messaging relay list event: {}", e);
};
Ok(Profile::new(public_key, metadata))
});
cx.spawn_in(window, async move |this, cx| {
if let Ok(profile) = task.await {
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.profile(profile, cx);
cx.defer_in(window, |this, _, cx| {
this.subscribe(cx);
});
})
.ok();
})
.ok();
} else {
cx.update(|window, cx| {
window.push_notification(Notification::error("Failed to create account."), cx)
})
.ok();
}
})
.detach();
}
/// Get the reference to profile.
pub fn profile_ref(&self) -> Option<&Profile> {
self.profile.as_ref()
}
/// Sets the profile for the account.
pub fn profile(&mut self, profile: Profile, cx: &mut Context<Self>) {
self.profile = Some(profile);
cx.notify();
}
/// Subscribes to the current account's metadata.
pub fn subscribe(&self, cx: &mut Context<Self>) {
let Some(profile) = self.profile.as_ref() else {
return;
};
let user = profile.public_key();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let metadata = Filter::new()
.kinds(vec![
Kind::Metadata,
Kind::ContactList,
Kind::InboxRelays,
Kind::MuteList,
Kind::SimpleGroups,
])
.author(user)
.limit(10);
let data = Filter::new()
.author(user)
.since(Timestamp::now())
.kinds(vec![
Kind::Metadata,
Kind::ContactList,
Kind::MuteList,
Kind::SimpleGroups,
Kind::InboxRelays,
Kind::RelayList,
]);
let msg = Filter::new().kind(Kind::GiftWrap).pubkey(user);
let new_msg = Filter::new().kind(Kind::GiftWrap).pubkey(user).limit(0);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = get_client();
client.subscribe(metadata, Some(opts)).await?;
client.subscribe(data, None).await?;
let sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
client.subscribe_with_id(sub_id, msg, Some(opts)).await?;
let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
client.subscribe_with_id(sub_id, new_msg, None).await?;
Ok(())
});
cx.spawn(async move |_, _| {
if let Err(e) = task.await {
log::error!("Error: {}", e);
}
})
.detach();
}
}

View File

@@ -1,18 +1,15 @@
use std::{ use std::env::consts::OS;
env::{self, consts::OS}, use std::env::{self};
ffi::OsString, use std::ffi::OsString;
path::PathBuf, use std::path::PathBuf;
};
use anyhow::{anyhow, Context as _, Error}; use anyhow::{anyhow, Context as _, Error};
use global::get_client; use global::shared_state;
use gpui::{App, AppContext, Context, Entity, Global, SemanticVersion, Task}; use gpui::{App, AppContext, Context, Entity, Global, SemanticVersion, Task};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smol::{ use smol::fs::{self, File};
fs::{self, File}, use smol::io::AsyncWriteExt;
io::AsyncWriteExt, use smol::process::Command;
process::Command,
};
use tempfile::TempDir; use tempfile::TempDir;
struct GlobalAutoUpdate(Entity<AutoUpdater>); struct GlobalAutoUpdate(Entity<AutoUpdater>);
@@ -129,10 +126,9 @@ impl AutoUpdater {
self.set_status(AutoUpdateStatus::Downloading, cx); self.set_status(AutoUpdateStatus::Downloading, cx);
let task: Task<Result<(TempDir, PathBuf), Error>> = cx.background_spawn(async move { let task: Task<Result<(TempDir, PathBuf), Error>> = cx.background_spawn(async move {
let client = get_client();
let ids = event.tags.event_ids().copied(); let ids = event.tags.event_ids().copied();
let filter = Filter::new().ids(ids).kind(Kind::FileMetadata); let filter = Filter::new().ids(ids).kind(Kind::FileMetadata);
let events = client.database().query(filter).await?; let events = shared_state().client.database().query(filter).await?;
if let Some(event) = events.into_iter().find(|event| event.content == OS) { if let Some(event) = events.into_iter().find(|event| event.content == OS) {
let tag = event.tags.find(TagKind::Url).context("url not found")?; let tag = event.tags.find(TagKind::Url).context("url not found")?;

View File

@@ -5,7 +5,6 @@ edition.workspace = true
publish.workspace = true publish.workspace = true
[dependencies] [dependencies]
account = { path = "../account" }
common = { path = "../common" } common = { path = "../common" }
global = { path = "../global" } global = { path = "../global" }

View File

@@ -1,10 +1,11 @@
use std::{cmp::Reverse, collections::BTreeSet}; use std::cmp::Reverse;
use std::collections::BTreeSet;
use account::Account;
use anyhow::Error; use anyhow::Error;
use common::room_hash; use common::room_hash;
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use fuzzy_matcher::skim::SkimMatcherV2;
use global::get_client; use fuzzy_matcher::FuzzyMatcher;
use global::shared_state;
use gpui::{ use gpui::{
App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window, App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window,
}; };
@@ -67,7 +68,7 @@ impl ChatRegistry {
} }
/// Set the global ChatRegistry instance /// Set the global ChatRegistry instance
pub fn set_global(state: Entity<Self>, cx: &mut App) { pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalChatRegistry(state)); cx.set_global(GlobalChatRegistry(state));
} }
@@ -160,14 +161,11 @@ impl ChatRegistry {
/// 3. Determines each room's type based on message frequency and trust status /// 3. Determines each room's type based on message frequency and trust status
/// 4. Creates Room entities for each unique room /// 4. Creates Room entities for each unique room
pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// If the user is not logged in, do nothing let client = &shared_state().client;
let Some(current_user) = Account::get_global(cx).profile_ref() else { let Some(public_key) = shared_state().identity().map(|i| i.public_key()) else {
return; return;
}; };
let client = get_client();
let public_key = current_user.public_key();
let task: Task<Result<BTreeSet<Room>, Error>> = cx.background_spawn(async move { let task: Task<Result<BTreeSet<Room>, Error>> = cx.background_spawn(async move {
// Get messages sent by the user // Get messages sent by the user
let send = Filter::new() let send = Filter::new()
@@ -290,8 +288,7 @@ impl ChatRegistry {
pub fn event_to_message(&mut self, event: Event, window: &mut Window, cx: &mut Context<Self>) { pub fn event_to_message(&mut self, event: Event, window: &mut Window, cx: &mut Context<Self>) {
let id = room_hash(&event); let id = room_hash(&event);
let author = event.pubkey; let author = event.pubkey;
let Some(public_key) = shared_state().identity().map(|i| i.public_key()) else {
let Some(profile) = Account::get_global(cx).profile.to_owned() else {
return; return;
}; };
@@ -301,7 +298,7 @@ impl ChatRegistry {
this.created_at(event.created_at, cx); this.created_at(event.created_at, cx);
// Set this room is ongoing if the new message is from current user // Set this room is ongoing if the new message is from current user
if author == profile.public_key() { if author == public_key {
this.set_ongoing(cx); this.set_ongoing(cx);
} }

View File

@@ -1,7 +1,10 @@
use std::cell::RefCell;
use std::iter::IntoIterator;
use std::rc::Rc;
use chrono::{Local, TimeZone}; use chrono::{Local, TimeZone};
use gpui::SharedString; use gpui::SharedString;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use std::{cell::RefCell, iter::IntoIterator, rc::Rc};
use crate::room::SendError; use crate::room::SendError;

View File

@@ -1,18 +1,17 @@
use std::{cmp::Ordering, sync::Arc}; use std::cmp::Ordering;
use std::sync::Arc;
use account::Account;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use chrono::{Local, TimeZone}; use chrono::{Local, TimeZone};
use common::{compare, profile::RenderProfile, room_hash}; use common::profile::RenderProfile;
use global::{async_cache_profile, get_cache_profile, get_client, profiles}; use common::{compare, room_hash};
use global::shared_state;
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window}; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use crate::{ use crate::constants::{DAYS_IN_MONTH, HOURS_IN_DAY, MINUTES_IN_HOUR, NOW, SECONDS_IN_MINUTE};
constants::{DAYS_IN_MONTH, HOURS_IN_DAY, MINUTES_IN_HOUR, NOW, SECONDS_IN_MINUTE}, use crate::message::Message;
message::Message,
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Incoming(pub Message); pub struct Incoming(pub Message);
@@ -165,22 +164,21 @@ impl Room {
/// # Returns /// # Returns
/// ///
/// The Profile of the first member in the room /// The Profile of the first member in the room
pub fn first_member(&self, cx: &App) -> Profile { pub fn first_member(&self, _cx: &App) -> Profile {
let account = Account::global(cx).read(cx); let Some(account) = shared_state().identity() else {
let Some(profile) = account.profile.clone() else { return shared_state().person(&self.members[0]);
return get_cache_profile(&self.members[0]);
}; };
if let Some(public_key) = self if let Some(public_key) = self
.members .members
.iter() .iter()
.filter(|&pubkey| pubkey != &profile.public_key()) .filter(|&pubkey| pubkey != &account.public_key())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.first() .first()
{ {
get_cache_profile(public_key) shared_state().person(public_key)
} else { } else {
profile account
} }
} }
@@ -200,7 +198,7 @@ impl Room {
let profiles = self let profiles = self
.members .members
.iter() .iter()
.map(get_cache_profile) .map(|public_key| shared_state().person(public_key))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut name = profiles let mut name = profiles
@@ -325,14 +323,18 @@ impl Room {
/// A Task that resolves to Result<Vec<(PublicKey, Option<Metadata>)>, Error> /// A Task that resolves to Result<Vec<(PublicKey, Option<Metadata>)>, Error>
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub fn load_metadata(&self, cx: &mut Context<Self>) -> Task<Result<(), Error>> { pub fn load_metadata(&self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
let client = get_client();
let public_keys = Arc::clone(&self.members); let public_keys = Arc::clone(&self.members);
cx.background_spawn(async move { cx.background_spawn(async move {
for public_key in public_keys.iter() { for public_key in public_keys.iter() {
let metadata = client.database().metadata(*public_key).await?; let metadata = shared_state()
.client
.database()
.metadata(*public_key)
.await?;
profiles() shared_state()
.persons
.write() .write()
.await .await
.entry(*public_key) .entry(*public_key)
@@ -359,7 +361,6 @@ impl Room {
/// A Task that resolves to Result<Vec<(PublicKey, bool)>, Error> where /// A Task that resolves to Result<Vec<(PublicKey, bool)>, Error> where
/// the boolean indicates if the member has inbox relays configured /// the boolean indicates if the member has inbox relays configured
pub fn messaging_relays(&self, cx: &App) -> Task<Result<Vec<(PublicKey, bool)>, Error>> { pub fn messaging_relays(&self, cx: &App) -> Task<Result<Vec<(PublicKey, bool)>, Error>> {
let client = get_client();
let pubkeys = Arc::clone(&self.members); let pubkeys = Arc::clone(&self.members);
cx.background_spawn(async move { cx.background_spawn(async move {
@@ -370,8 +371,13 @@ impl Room {
.kind(Kind::InboxRelays) .kind(Kind::InboxRelays)
.author(*pubkey) .author(*pubkey)
.limit(1); .limit(1);
let is_ready = shared_state()
let is_ready = client.database().query(filter).await?.first().is_some(); .client
.database()
.query(filter)
.await?
.first()
.is_some();
result.push((*pubkey, is_ready)); result.push((*pubkey, is_ready));
} }
@@ -391,9 +397,7 @@ impl Room {
/// A Task that resolves to Result<Vec<RoomMessage>, Error> containing /// A Task that resolves to Result<Vec<RoomMessage>, Error> containing
/// all messages for this room /// all messages for this room
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<Message>, Error>> { pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<Message>, Error>> {
let client = get_client();
let pubkeys = Arc::clone(&self.members); let pubkeys = Arc::clone(&self.members);
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::PrivateDirectMessage) .kind(Kind::PrivateDirectMessage)
.authors(pubkeys.to_vec()) .authors(pubkeys.to_vec())
@@ -404,7 +408,8 @@ impl Room {
let parser = NostrParser::new(); let parser = NostrParser::new();
// Get all events from database // Get all events from database
let events = client let events = shared_state()
.client
.database() .database()
.query(filter) .query(filter)
.await? .await?
@@ -452,10 +457,10 @@ impl Room {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for pubkey in pubkey_tokens.iter() { for pubkey in pubkey_tokens.iter() {
mentions.push(async_cache_profile(pubkey).await); mentions.push(shared_state().async_person(pubkey).await);
} }
let author = async_cache_profile(&event.pubkey).await; let author = shared_state().async_person(&event.pubkey).await;
if let Ok(message) = Message::builder() if let Ok(message) = Message::builder()
.id(event.id) .id(event.id)
@@ -486,7 +491,7 @@ impl Room {
/// ///
/// Processes the event and emits an Incoming to the UI when complete /// Processes the event and emits an Incoming to the UI when complete
pub fn emit_message(&self, event: Event, _window: &mut Window, cx: &mut Context<Self>) { pub fn emit_message(&self, event: Event, _window: &mut Window, cx: &mut Context<Self>) {
let author = get_cache_profile(&event.pubkey); let author = shared_state().person(&event.pubkey);
// Extract all mentions from content // Extract all mentions from content
let mentions = extract_mentions(&event.content); let mentions = extract_mentions(&event.content);
@@ -542,9 +547,9 @@ impl Room {
&self, &self,
content: &str, content: &str,
replies: Option<&Vec<Message>>, replies: Option<&Vec<Message>>,
cx: &App, _cx: &App,
) -> Option<Message> { ) -> Option<Message> {
let author = Account::get_global(cx).profile.clone()?; let author = shared_state().identity()?;
let public_key = author.public_key(); let public_key = author.public_key();
let builder = EventBuilder::private_msg_rumor(public_key, content); let builder = EventBuilder::private_msg_rumor(public_key, content);
@@ -627,11 +632,10 @@ impl Room {
let public_keys = Arc::clone(&self.members); let public_keys = Arc::clone(&self.members);
cx.background_spawn(async move { cx.background_spawn(async move {
let client = get_client(); let signer = shared_state().client.signer().await?;
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
let mut reports = vec![];
let mut reports = vec![];
let mut tags: Vec<Tag> = public_keys let mut tags: Vec<Tag> = public_keys
.iter() .iter()
.filter_map(|pubkey| { .filter_map(|pubkey| {
@@ -671,11 +675,13 @@ impl Room {
}; };
for receiver in receivers.iter() { for receiver in receivers.iter() {
if let Err(e) = client if let Err(e) = shared_state()
.client
.send_private_msg(*receiver, &content, tags.clone()) .send_private_msg(*receiver, &content, tags.clone())
.await .await
{ {
let metadata = client let metadata = shared_state()
.client
.database() .database()
.metadata(*receiver) .metadata(*receiver)
.await? .await?
@@ -692,11 +698,13 @@ impl Room {
// Only send a backup message to current user if there are no issues when sending to others // Only send a backup message to current user if there are no issues when sending to others
if reports.is_empty() { if reports.is_empty() {
if let Err(e) = client if let Err(e) = shared_state()
.client
.send_private_msg(*current_user, &content, tags.clone()) .send_private_msg(*current_user, &content, tags.clone())
.await .await
{ {
let metadata = client let metadata = shared_state()
.client
.database() .database()
.metadata(*current_user) .metadata(*current_user)
.await? .await?
@@ -732,7 +740,7 @@ pub fn extract_mentions(content: &str) -> Vec<Profile> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for pubkey in pubkey_tokens.into_iter() { for pubkey in pubkey_tokens.into_iter() {
mentions.push(get_cache_profile(&pubkey)); mentions.push(shared_state().person(&pubkey));
} }
mentions mentions

View File

@@ -1,6 +1,9 @@
use futures::{channel::oneshot, FutureExt}; use std::marker::PhantomData;
use std::time::Duration;
use futures::channel::oneshot;
use futures::FutureExt;
use gpui::{Context, Task}; use gpui::{Context, Task};
use std::{marker::PhantomData, time::Duration};
pub struct DebouncedDelay<E: 'static> { pub struct DebouncedDelay<E: 'static> {
task: Option<Task<()>>, task: Option<Task<()>>,

View File

@@ -1,8 +1,6 @@
use std::{ use std::collections::HashSet;
collections::HashSet, use std::hash::{DefaultHasher, Hash, Hasher};
hash::{DefaultHasher, Hash, Hasher}, use std::sync::Arc;
sync::Arc,
};
use global::constants::NIP96_SERVER; use global::constants::NIP96_SERVER;
use gpui::{Image, ImageFormat}; use gpui::{Image, ImageFormat};
@@ -42,11 +40,12 @@ pub fn room_hash(event: &Event) -> u64 {
hasher.finish() hasher.finish()
} }
pub fn string_to_qr(data: &str) -> Result<Arc<Image>, anyhow::Error> { pub fn string_to_qr(data: &str) -> Option<Arc<Image>> {
let bytes = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256)?; let Ok(bytes) = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256) else {
let img = Arc::new(Image::from_bytes(ImageFormat::Png, bytes)); return None;
};
Ok(img) Some(Arc::new(Image::from_bytes(ImageFormat::Png, bytes)))
} }
pub fn compare<T>(a: &[T], b: &[T]) -> bool pub fn compare<T>(a: &[T], b: &[T]) -> bool

View File

@@ -14,7 +14,6 @@ theme = { path = "../theme" }
common = { path = "../common" } common = { path = "../common" }
global = { path = "../global" } global = { path = "../global" }
chats = { path = "../chats" } chats = { path = "../chats" }
account = { path = "../account" }
auto_update = { path = "../auto_update" } auto_update = { path = "../auto_update" }
gpui.workspace = true gpui.workspace = true

View File

@@ -1,29 +1,27 @@
use std::sync::Arc; use std::sync::Arc;
use account::Account;
use anyhow::Error; use anyhow::Error;
use chats::{ChatRegistry, RoomEmitter}; use chats::{ChatRegistry, RoomEmitter};
use global::{ use global::constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH};
constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH}, use global::shared_state;
get_client, use gpui::prelude::FluentBuilder;
};
use gpui::{ use gpui::{
div, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis, Context, Entity, div, impl_internal_actions, px, App, AppContext, Axis, Context, Entity, InteractiveElement,
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, Window, IntoElement, ParentElement, Render, Styled, Subscription, Task, Window,
}; };
use nostr_sdk::prelude::*; use nostr_connect::prelude::*;
use serde::Deserialize; use serde::Deserialize;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use theme::{ActiveTheme, Theme, ThemeMode}; use theme::{ActiveTheme, Theme, ThemeMode};
use ui::{ use ui::button::{Button, ButtonVariants};
button::{Button, ButtonVariants}, use ui::dock_area::dock::DockPlacement;
dock_area::{dock::DockPlacement, panel::PanelView, DockArea, DockItem}, use ui::dock_area::panel::PanelView;
ContextModal, IconName, Root, Sizable, TitleBar, use ui::dock_area::{DockArea, DockItem};
}; use ui::{ContextModal, IconName, Root, Sizable, TitleBar};
use crate::views::chat::{self, Chat};
use crate::views::{ use crate::views::{
chat::{self, Chat}, compose, login, new_account, onboarding, profile, relays, sidebar, startup, welcome,
compose, login, new_account, onboarding, profile, relays, sidebar, welcome,
}; };
impl_internal_actions!(dock, [ToggleModal]); impl_internal_actions!(dock, [ToggleModal]);
@@ -63,16 +61,16 @@ pub struct ToggleModal {
} }
pub struct ChatSpace { pub struct ChatSpace {
titlebar: bool,
dock: Entity<DockArea>, dock: Entity<DockArea>,
titlebar: bool,
#[allow(unused)] #[allow(unused)]
subscriptions: SmallVec<[Subscription; 3]>, subscriptions: SmallVec<[Subscription; 2]>,
} }
impl ChatSpace { impl ChatSpace {
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> { pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
let dock = cx.new(|cx| { let dock = cx.new(|cx| {
let panel = Arc::new(onboarding::init(window, cx)); let panel = Arc::new(startup::init(window, cx));
let center = DockItem::panel(panel); let center = DockItem::panel(panel);
let mut dock = DockArea::new(window, cx); let mut dock = DockArea::new(window, cx);
// Initialize the dock area with the center panel // Initialize the dock area with the center panel
@@ -81,46 +79,39 @@ impl ChatSpace {
}); });
cx.new(|cx| { cx.new(|cx| {
let account = Account::global(cx);
let chats = ChatRegistry::global(cx); let chats = ChatRegistry::global(cx);
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
subscriptions.push(cx.observe_in( // Automatically load messages when chat panel opens
&account, subscriptions.push(cx.observe_new::<Chat>(|this: &mut Chat, window, cx| {
window,
|this: &mut ChatSpace, account, window, cx| {
if account.read(cx).profile.is_some() {
this.open_chats(window, cx);
} else {
this.open_onboarding(window, cx);
}
},
));
subscriptions.push(cx.subscribe_in(
&chats,
window,
|this, _state, event, window, cx| {
if let RoomEmitter::Open(room) = event {
if let Some(room) = room.upgrade() {
this.dock.update(cx, |this, cx| {
let panel = chat::init(room, window, cx);
this.add_panel(panel, DockPlacement::Center, window, cx);
});
} else {
window
.push_notification("Failed to open room. Please retry later.", cx);
}
}
},
));
subscriptions.push(cx.observe_new::<Chat>(|this, window, cx| {
if let Some(window) = window { if let Some(window) = window {
this.load_messages(window, cx); this.load_messages(window, cx);
} }
})); }));
// Subscribe to open chat room requests
subscriptions.push(cx.subscribe_in(
&chats,
window,
|this: &mut ChatSpace, _state, event, window, cx| {
if let RoomEmitter::Open(room) = event {
if let Some(room) = room.upgrade() {
this.dock.update(cx, |this, cx| {
let panel = chat::init(room, window, cx);
let placement = DockPlacement::Center;
this.add_panel(panel, placement, window, cx);
});
} else {
window.push_notification(
"Failed to open room. Please try again later.",
cx,
);
}
}
},
));
Self { Self {
dock, dock,
subscriptions, subscriptions,
@@ -129,12 +120,10 @@ impl ChatSpace {
}) })
} }
fn show_titlebar(&mut self, cx: &mut Context<Self>) { pub fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.titlebar = true; // Disable the titlebar
cx.notify(); self.titlebar(false, cx);
}
fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let panel = Arc::new(onboarding::init(window, cx)); let panel = Arc::new(onboarding::init(window, cx));
let center = DockItem::panel(panel); let center = DockItem::panel(panel);
@@ -144,8 +133,9 @@ impl ChatSpace {
}); });
} }
fn open_chats(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub fn open_chats(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.show_titlebar(cx); // Enable the titlebar
self.titlebar(true, cx);
let weak_dock = self.dock.downgrade(); let weak_dock = self.dock.downgrade();
let left = DockItem::panel(Arc::new(sidebar::init(window, cx))); let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
@@ -191,20 +181,28 @@ impl ChatSpace {
}); });
} }
fn titlebar(&mut self, status: bool, cx: &mut Context<Self>) {
self.titlebar = status;
cx.notify();
}
fn verify_messaging_relays(&self, cx: &App) -> Task<Result<bool, Error>> { fn verify_messaging_relays(&self, cx: &App) -> Task<Result<bool, Error>> {
cx.background_spawn(async move { cx.background_spawn(async move {
let client = get_client(); let signer = shared_state().client.signer().await?;
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::InboxRelays) .kind(Kind::InboxRelays)
.author(public_key) .author(public_key)
.limit(1); .limit(1);
let is_exist = shared_state()
.client
.database()
.query(filter)
.await?
.first()
.is_some();
let exist = client.database().query(filter).await?.first().is_some(); Ok(is_exist)
Ok(exist)
}) })
} }
@@ -298,11 +296,12 @@ impl Render for ChatSpace {
.flex() .flex()
.items_center() .items_center()
.justify_end() .justify_end()
.gap_2() .gap_1p5()
.px_2() .px_2()
.child( .child(
Button::new("appearance") Button::new("appearance")
.xsmall() .tooltip("Change the app's appearance")
.small()
.ghost() .ghost()
.map(|this| { .map(|this| {
if cx.theme().mode.is_dark() { if cx.theme().mode.is_dark() {
@@ -326,6 +325,26 @@ impl Render for ChatSpace {
); );
} }
})), })),
)
.child(
Button::new("settings")
.tooltip("Open settings")
.small()
.ghost()
.icon(IconName::Settings),
)
.child(
Button::new("logout")
.tooltip("Log out")
.small()
.ghost()
.icon(IconName::Logout)
.on_click(cx.listener(move |_, _, _window, cx| {
cx.background_spawn(async move {
shared_state().unset_signer().await;
})
.detach();
})),
), ),
), ),
) )

View File

@@ -1,17 +1,14 @@
use anyhow::{anyhow, Error}; use std::sync::Arc;
use std::time::Duration;
use anyhow::Error;
use asset::Assets; use asset::Assets;
use auto_update::AutoUpdater; use auto_update::AutoUpdater;
use chats::ChatRegistry; use chats::ChatRegistry;
use futures::{select, FutureExt};
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
use global::constants::APP_NAME; use global::constants::APP_NAME;
use global::{ use global::constants::{APP_ID, KEYRING_BUNKER, KEYRING_USER_PATH};
constants::{ use global::{shared_state, NostrSignal};
ALL_MESSAGES_SUB_ID, APP_ID, APP_PUBKEY, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT,
METADATA_BATCH_TIMEOUT, NEW_MESSAGE_SUB_ID, SEARCH_RELAYS,
},
get_client, init_global_state, profiles,
};
use gpui::{ use gpui::{
actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
WindowBounds, WindowKind, WindowOptions, WindowBounds, WindowKind, WindowOptions,
@@ -20,13 +17,7 @@ use gpui::{
use gpui::{point, SharedString, TitlebarOptions}; use gpui::{point, SharedString, TitlebarOptions};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use gpui::{WindowBackgroundAppearance, WindowDecorations}; use gpui::{WindowBackgroundAppearance, WindowDecorations};
use nostr_sdk::{ use nostr_connect::prelude::*;
async_utility::task::spawn, nips::nip01::Coordinate, pool::prelude::ReqExitPolicy, Client,
Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Metadata, PublicKey, RelayMessage,
RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId, Tag,
};
use smol::Timer;
use std::{collections::HashSet, mem, sync::Arc, time::Duration};
use theme::Theme; use theme::Theme;
use ui::Root; use ui::Root;
@@ -36,226 +27,27 @@ pub(crate) mod views;
actions!(coop, [Quit]); actions!(coop, [Quit]);
#[derive(Debug)]
enum Signal {
/// Receive event
Event(Event),
/// Receive eose
Eose,
/// Receive app updates
AppUpdates(Event),
}
fn main() { fn main() {
// Initialize logging // Initialize logging
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
// Initialize global state
init_global_state();
let (event_tx, event_rx) = smol::channel::bounded::<Signal>(2048); // Initialize the Global State and process events in a separate thread.
let (batch_tx, batch_rx) = smol::channel::bounded::<Vec<PublicKey>>(500); // Must be run under async utility runtime
nostr_sdk::async_utility::task::spawn(async move {
let client = get_client(); shared_state().start().await;
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
// Spawn a task to establish relay connections
// NOTE: Use `async_utility` instead of `smol-rs`
spawn(async move {
for relay in BOOTSTRAP_RELAYS.into_iter() {
if let Err(e) = client.add_relay(relay).await {
log::error!("Failed to add relay {}: {}", relay, e);
}
}
for relay in SEARCH_RELAYS.into_iter() {
if let Err(e) = client.add_relay(relay).await {
log::error!("Failed to add relay {}: {}", relay, e);
}
}
// Establish connection to bootstrap relays
client.connect().await;
log::info!("Connected to bootstrap relays");
log::info!("Subscribing to app updates...");
let coordinate = Coordinate {
kind: Kind::Custom(32267),
public_key: PublicKey::from_hex(APP_PUBKEY).expect("App Pubkey is invalid"),
identifier: APP_ID.into(),
};
let filter = Filter::new()
.kind(Kind::ReleaseArtifactSet)
.coordinate(&coordinate)
.limit(1);
if let Err(e) = client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await
{
log::error!("Failed to subscribe for app updates: {}", e);
}
}); });
// Spawn a task to handle metadata batching // Initialize the Application
// NOTE: Use `async_utility` instead of `smol-rs`
spawn(async move {
let mut batch: HashSet<PublicKey> = HashSet::new();
loop {
let mut timeout =
Box::pin(Timer::after(Duration::from_millis(METADATA_BATCH_TIMEOUT)).fuse());
select! {
pubkeys = batch_rx.recv().fuse() => {
match pubkeys {
Ok(keys) => {
batch.extend(keys);
if batch.len() >= METADATA_BATCH_LIMIT {
sync_metadata(mem::take(&mut batch), client, opts).await;
}
}
Err(_) => break,
}
}
_ = timeout => {
if !batch.is_empty() {
sync_metadata(mem::take(&mut batch), client, opts).await;
}
}
}
}
});
// Spawn a task to handle relay pool notification
// NOTE: Use `async_utility` instead of `smol-rs`
spawn(async move {
let keys = Keys::generate();
let all_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
let new_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
let mut notifications = client.notifications();
let mut processed_events: HashSet<EventId> = HashSet::new();
while let Ok(notification) = notifications.recv().await {
if let RelayPoolNotification::Message { message, .. } = notification {
match message {
RelayMessage::Event {
event,
subscription_id,
} => {
if processed_events.contains(&event.id) {
continue;
}
processed_events.insert(event.id);
match event.kind {
Kind::GiftWrap => {
let event = match get_unwrapped(event.id).await {
Ok(event) => event,
Err(_) => match client.unwrap_gift_wrap(&event).await {
Ok(unwrap) => match unwrap.rumor.sign_with_keys(&keys) {
Ok(unwrapped) => {
set_unwrapped(event.id, &unwrapped, &keys)
.await
.ok();
unwrapped
}
Err(_) => continue,
},
Err(_) => continue,
},
};
let mut pubkeys = vec![];
pubkeys.extend(event.tags.public_keys());
pubkeys.push(event.pubkey);
// Send all pubkeys to the batch to sync metadata
batch_tx.send(pubkeys).await.ok();
// Save the event to the database, use for query directly.
client.database().save_event(&event).await.ok();
// Send this event to the GPUI
if new_id == *subscription_id {
event_tx.send(Signal::Event(event)).await.ok();
}
}
Kind::Metadata => {
let metadata = Metadata::from_json(&event.content).ok();
profiles()
.write()
.await
.entry(event.pubkey)
.and_modify(|entry| {
if entry.is_none() {
*entry = metadata.clone();
}
})
.or_insert_with(|| metadata);
}
Kind::ContactList => {
if let Ok(signer) = client.signer().await {
if let Ok(public_key) = signer.get_public_key().await {
if public_key == event.pubkey {
let pubkeys = event
.tags
.public_keys()
.copied()
.collect::<Vec<_>>();
batch_tx.send(pubkeys).await.ok();
}
}
}
}
Kind::ReleaseArtifactSet => {
let filter = Filter::new()
.ids(event.tags.event_ids().copied())
.kind(Kind::FileMetadata);
if let Err(e) = client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await
{
log::error!("Failed to subscribe for file metadata: {}", e);
} else {
event_tx
.send(Signal::AppUpdates(event.into_owned()))
.await
.ok();
}
}
_ => {}
}
}
RelayMessage::EndOfStoredEvents(subscription_id) => {
if all_id == *subscription_id {
event_tx.send(Signal::Eose).await.ok();
}
}
_ => {}
}
}
}
});
// Initialize application
let app = Application::new() let app = Application::new()
.with_assets(Assets) .with_assets(Assets)
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new())); .with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
app.run(move |cx| { app.run(move |cx| {
// Bring the app to the foreground
cx.activate(true);
// Register the `quit` function // Register the `quit` function
cx.on_action(quit); cx.on_action(quit);
// Register the `quit` function with CMD+Q // Register the `quit` function with CMD+Q (macOS only)
#[cfg(target_os = "macos")]
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
// Set menu items // Set menu items
@@ -297,37 +89,87 @@ fn main() {
// Root Entity // Root Entity
cx.new(|cx| { cx.new(|cx| {
cx.activate(true);
// Initialize components // Initialize components
ui::init(cx); ui::init(cx);
// Initialize auto update // Initialize auto update
auto_update::init(cx); auto_update::init(cx);
// Initialize chat state // Initialize chat state
chats::init(cx); chats::init(cx);
// Initialize account state // Initialize chatspace (or workspace)
account::init(cx); let chatspace = chatspace::init(window, cx);
let async_chatspace = chatspace.downgrade();
let async_chatspace_clone = async_chatspace.clone();
// Read user's credential
let read_credential = cx.read_credentials(KEYRING_USER_PATH);
cx.spawn_in(window, async move |_, cx| {
if let Ok(Some((user, secret))) = read_credential.await {
cx.update(|window, cx| {
if let Ok(signer) = extract_credential(&user, secret) {
cx.background_spawn(async move {
if let Err(e) = shared_state().set_signer(signer).await {
log::error!("Signer error: {}", e);
}
})
.detach();
} else {
async_chatspace
.update(cx, |this, cx| {
this.open_onboarding(window, cx);
})
.ok();
}
})
.ok();
} else {
cx.update(|window, cx| {
async_chatspace
.update(cx, |this, cx| {
this.open_onboarding(window, cx);
})
.ok();
})
.ok();
}
})
.detach();
// Spawn a task to handle events from nostr channel // Spawn a task to handle events from nostr channel
cx.spawn_in(window, async move |_, cx| { cx.spawn_in(window, async move |_, cx| {
while let Ok(signal) = event_rx.recv().await { while let Ok(signal) = shared_state().global_receiver.recv().await {
cx.update(|window, cx| { cx.update(|window, cx| {
let chats = ChatRegistry::global(cx); let chats = ChatRegistry::global(cx);
let auto_updater = AutoUpdater::global(cx); let auto_updater = AutoUpdater::global(cx);
match signal { match signal {
Signal::Eose => { NostrSignal::SignerUpdated => {
async_chatspace_clone
.update(cx, |this, cx| {
this.open_chats(window, cx);
})
.ok();
}
NostrSignal::SignerUnset => {
async_chatspace_clone
.update(cx, |this, cx| {
this.open_onboarding(window, cx);
})
.ok();
}
NostrSignal::Eose => {
chats.update(cx, |this, cx| { chats.update(cx, |this, cx| {
this.load_rooms(window, cx); this.load_rooms(window, cx);
}); });
} }
Signal::Event(event) => { NostrSignal::Event(event) => {
chats.update(cx, |this, cx| { chats.update(cx, |this, cx| {
this.event_to_message(event, window, cx); this.event_to_message(event, window, cx);
}); });
} }
Signal::AppUpdates(event) => { NostrSignal::AppUpdate(event) => {
auto_updater.update(cx, |this, cx| { auto_updater.update(cx, |this, cx| {
this.update(event, cx); this.update(event, cx);
}); });
@@ -339,62 +181,26 @@ fn main() {
}) })
.detach(); .detach();
Root::new(chatspace::init(window, cx).into(), window, cx) Root::new(chatspace.into(), window, cx)
}) })
}) })
.expect("Failed to open window. Please restart the application."); .expect("Failed to open window. Please restart the application.");
}); });
} }
async fn set_unwrapped(root: EventId, event: &Event, keys: &Keys) -> Result<(), Error> { fn extract_credential(user: &str, secret: Vec<u8>) -> Result<impl NostrSigner, Error> {
let client = get_client(); if user == KEYRING_BUNKER {
let event = EventBuilder::new(Kind::Custom(9001), event.as_json()) let value = String::from_utf8(secret)?;
.tags(vec![Tag::event(root)]) let uri = NostrConnectURI::parse(value)?;
.sign(keys) // keys must be random generated let client_keys = shared_state().client_signer.clone();
.await?; let signer = NostrConnect::new(uri, client_keys, Duration::from_secs(300), None)?;
client.database().save_event(&event).await?; Ok(signer.into_nostr_signer())
Ok(())
}
async fn get_unwrapped(gift_wrap: EventId) -> Result<Event, Error> {
let client = get_client();
let filter = Filter::new()
.kind(Kind::Custom(9001))
.event(gift_wrap)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() {
let parsed = Event::from_json(event.content)?;
Ok(parsed)
} else { } else {
Err(anyhow!("Event not found")) let secret_key = SecretKey::from_slice(&secret)?;
} let keys = Keys::new(secret_key);
}
async fn sync_metadata( Ok(keys.into_nostr_signer())
buffer: HashSet<PublicKey>,
client: &Client,
opts: SubscribeAutoCloseOptions,
) {
let kinds = vec![
Kind::Metadata,
Kind::ContactList,
Kind::InboxRelays,
Kind::UserStatus,
];
let filter = Filter::new()
.authors(buffer.iter().cloned())
.limit(buffer.len() * kinds.len())
.kinds(kinds);
if let Err(e) = client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await
{
log::error!("Failed to sync metadata: {e}");
} }
} }

View File

@@ -1,19 +1,21 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use account::Account;
use async_utility::task::spawn; use async_utility::task::spawn;
use chats::{ use chats::message::Message;
message::Message, use chats::room::{Room, RoomKind, SendError};
room::{Room, RoomKind, SendError}, use common::nip96_upload;
}; use common::profile::RenderProfile;
use common::{nip96_upload, profile::RenderProfile}; use global::shared_state;
use global::get_client; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, impl_internal_actions, list, prelude::FluentBuilder, px, red, relative, rems, svg, div, img, impl_internal_actions, list, px, red, relative, rems, svg, white, AnyElement, App,
white, AnyElement, App, AppContext, ClipboardItem, Context, Div, Element, Empty, Entity, AppContext, ClipboardItem, Context, Div, Element, Empty, Entity, EventEmitter, Flatten,
EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, ListState, ObjectFit,
ListState, ObjectFit, ParentElement, PathPromptOptions, Render, RetainAllImageCache, ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString,
SharedString, StatefulInteractiveElement, Styled, StyledImage, Subscription, Window, StatefulInteractiveElement, Styled, StyledImage, Subscription, Window,
}; };
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
@@ -21,15 +23,15 @@ use serde::Deserialize;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::fs; use smol::fs;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::emoji_picker::EmojiPicker;
use ui::input::{InputEvent, InputState, TextInput};
use ui::notification::Notification;
use ui::popup_menu::PopupMenu;
use ui::text::RichText;
use ui::{ use ui::{
avatar::Avatar,
button::{Button, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
emoji_picker::EmojiPicker,
input::{InputEvent, InputState, TextInput},
notification::Notification,
popup_menu::PopupMenu,
text::RichText,
v_flex, ContextModal, Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt, v_flex, ContextModal, Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt,
}; };
@@ -217,7 +219,7 @@ impl Chat {
// TODO: find a better way to prevent duplicate messages during optimistic updates // TODO: find a better way to prevent duplicate messages during optimistic updates
fn prevent_duplicate_message(&self, new_msg: &Message, cx: &Context<Self>) -> bool { fn prevent_duplicate_message(&self, new_msg: &Message, cx: &Context<Self>) -> bool {
let Some(current_user) = Account::get_global(cx).profile_ref() else { let Some(account) = shared_state().identity() else {
return false; return false;
}; };
@@ -225,7 +227,7 @@ impl Chat {
return false; return false;
}; };
if current_user.public_key() != author.public_key() { if account.public_key() != author.public_key() {
return false; return false;
} }
@@ -238,7 +240,7 @@ impl Chat {
m.borrow() m.borrow()
.author .author
.as_ref() .as_ref()
.is_some_and(|p| p.public_key() == current_user.public_key()) .is_some_and(|p| p.public_key() == account.public_key())
}) })
.any(|existing| { .any(|existing| {
let existing = existing.borrow(); let existing = existing.borrow();
@@ -383,12 +385,11 @@ impl Chat {
}; };
if let Ok(file_data) = fs::read(path).await { if let Ok(file_data) = fs::read(path).await {
let client = get_client();
let (tx, rx) = oneshot::channel::<Option<Url>>(); let (tx, rx) = oneshot::channel::<Option<Url>>();
// Spawn task via async utility instead of GPUI context // Spawn task via async utility instead of GPUI context
spawn(async move { spawn(async move {
let url = match nip96_upload(client, file_data).await { let url = match nip96_upload(&shared_state().client, file_data).await {
Ok(url) => Some(url), Ok(url) => Some(url),
Err(e) => { Err(e) => {
log::error!("Upload error: {e}"); log::error!("Upload error: {e}");

View File

@@ -1,28 +1,25 @@
use std::{ use std::collections::{BTreeSet, HashSet};
collections::{BTreeSet, HashSet}, use std::time::Duration;
time::Duration,
};
use anyhow::Error; use anyhow::Error;
use chats::{room::Room, ChatRegistry}; use chats::room::Room;
use chats::ChatRegistry;
use common::profile::RenderProfile; use common::profile::RenderProfile;
use global::get_client; use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, impl_internal_actions, prelude::FluentBuilder, px, red, relative, uniform_list, App, div, img, impl_internal_actions, px, red, relative, uniform_list, App, AppContext, Context,
AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, Window,
Window,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::Deserialize; use serde::Deserialize;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::Timer; use smol::Timer;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{ use ui::button::{Button, ButtonVariants};
button::{Button, ButtonVariants}, use ui::input::{InputEvent, InputState, TextInput};
input::{InputEvent, InputState, TextInput}, use ui::{ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
ContextModal, Disableable, Icon, IconName, Sizable, StyledExt,
};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
cx.new(|cx| Compose::new(window, cx)) cx.new(|cx| Compose::new(window, cx))
@@ -71,10 +68,13 @@ impl Compose {
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let task: Task<Result<BTreeSet<Profile>, Error>> = cx.background_spawn(async move { let task: Task<Result<BTreeSet<Profile>, Error>> = cx.background_spawn(async move {
let client = get_client(); let signer = shared_state().client.signer().await?;
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
let profiles = client.database().contacts(public_key).await?; let profiles = shared_state()
.client
.database()
.contacts(public_key)
.await?;
Ok(profiles) Ok(profiles)
}); });
@@ -133,8 +133,7 @@ impl Compose {
let tags = Tags::from_list(tag_list); let tags = Tags::from_list(tag_list);
let event: Task<Result<Event, anyhow::Error>> = cx.background_spawn(async move { let event: Task<Result<Event, anyhow::Error>> = cx.background_spawn(async move {
let client = get_client(); let signer = shared_state().client.signer().await?;
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
// [IMPORTANT] // [IMPORTANT]
@@ -173,7 +172,6 @@ impl Compose {
} }
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let client = get_client();
let content = self.user_input.read(cx).value().to_string(); let content = self.user_input.read(cx).value().to_string();
// Show loading spinner // Show loading spinner
@@ -184,7 +182,8 @@ impl Compose {
let profile = nip05::profile(&content, None).await?; let profile = nip05::profile(&content, None).await?;
let public_key = profile.public_key; let public_key = profile.public_key;
let metadata = client let metadata = shared_state()
.client
.fetch_metadata(public_key, Duration::from_secs(2)) .fetch_metadata(public_key, Duration::from_secs(2))
.await? .await?
.unwrap_or_default(); .unwrap_or_default();
@@ -199,7 +198,8 @@ impl Compose {
}; };
cx.background_spawn(async move { cx.background_spawn(async move {
let metadata = client let metadata = shared_state()
.client
.fetch_metadata(public_key, Duration::from_secs(2)) .fetch_metadata(public_key, Duration::from_secs(2))
.await? .await?
.unwrap_or_default(); .unwrap_or_default();

View File

@@ -1,24 +1,27 @@
use std::{sync::Arc, time::Duration}; use std::sync::Arc;
use std::time::Duration;
use account::Account;
use common::string_to_qr; use common::string_to_qr;
use global::get_client_keys; use global::constants::{APP_NAME, KEYRING_BUNKER, KEYRING_USER_PATH};
use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, prelude::FluentBuilder, red, relative, AnyElement, App, AppContext, Context, Entity, div, img, red, relative, AnyElement, App, AppContext, ClipboardItem, Context, Entity,
EventEmitter, FocusHandle, Focusable, Image, IntoElement, ParentElement, Render, SharedString, EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
Styled, Subscription, Window, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
}; };
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{ use ui::button::{Button, ButtonVariants};
button::{Button, ButtonVariants}, use ui::dock_area::panel::{Panel, PanelEvent};
dock_area::panel::{Panel, PanelEvent}, use ui::input::{InputEvent, InputState, TextInput};
input::{InputEvent, InputState, TextInput}, use ui::notification::Notification;
notification::Notification, use ui::popup_menu::PopupMenu;
popup_menu::PopupMenu, use ui::{ContextModal, Disableable, Sizable, StyledExt};
ContextModal, Disableable, Sizable, StyledExt,
}; const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
const NOSTR_CONNECT_TIMEOUT: u64 = 300;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct CoopAuthUrlHandler; struct CoopAuthUrlHandler;
@@ -37,23 +40,20 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
} }
pub struct Login { pub struct Login {
// Inputs
key_input: Entity<InputState>, key_input: Entity<InputState>,
relay_input: Entity<InputState>,
connection_string: Entity<NostrConnectURI>,
qr_image: Entity<Option<Arc<Image>>>,
// Signer that created by Connection String
active_signer: Entity<Option<NostrConnect>>,
// Error for the key input
error: Entity<Option<SharedString>>, error: Entity<Option<SharedString>>,
is_logging_in: bool, is_logging_in: bool,
// Nostr Connect
qr: Entity<Option<Arc<Image>>>,
connect_relay: Entity<InputState>,
connect_client: Entity<Option<NostrConnectURI>>,
// Keep track of all signers created by nostr connect
signers: SmallVec<[NostrConnect; 3]>,
// Panel // Panel
name: SharedString, name: SharedString,
closable: bool,
zoomable: bool,
focus_handle: FocusHandle, focus_handle: FocusHandle,
#[allow(unused)] #[allow(unused)]
subscriptions: SmallVec<[Subscription; 4]>, subscriptions: SmallVec<[Subscription; 5]>,
} }
impl Login { impl Login {
@@ -62,106 +62,159 @@ impl Login {
} }
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self { fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
let connect_client: Entity<Option<NostrConnectURI>> = cx.new(|_| None); // nsec or bunker_uri (NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md)
let error = cx.new(|_| None);
let qr = cx.new(|_| None);
let key_input = let key_input =
cx.new(|cx| InputState::new(window, cx).placeholder("nsec... or bunker://...")); cx.new(|cx| InputState::new(window, cx).placeholder("nsec... or bunker://..."));
let connect_relay =
cx.new(|cx| InputState::new(window, cx).default_value("wss://relay.nsec.app"));
let signers = smallvec![]; let relay_input =
cx.new(|cx| InputState::new(window, cx).default_value(NOSTR_CONNECT_RELAY));
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
//
// Direct connection initiated by the client
let connection_string = cx.new(|_cx| {
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
let client_keys = shared_state().client_signer.clone();
NostrConnectURI::client(client_keys.public_key(), vec![relay], APP_NAME)
});
// Convert the Connection String into QR Image
let qr_image = cx.new(|_| None);
let async_qr_image = qr_image.downgrade();
// Keep track of the signer that created by Connection String
let active_signer = cx.new(|_| None);
let async_active_signer = active_signer.downgrade();
let error = cx.new(|_| None);
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
subscriptions.push(cx.subscribe_in( subscriptions.push(
&key_input, cx.subscribe_in(&key_input, window, |this, _, event, window, cx| {
window,
move |this, _, event, window, cx| {
if let InputEvent::PressEnter { .. } = event { if let InputEvent::PressEnter { .. } = event {
this.login(window, cx); this.login(window, cx);
} }
}),
);
subscriptions.push(
cx.subscribe_in(&relay_input, window, |this, _, event, window, cx| {
if let InputEvent::PressEnter { .. } = event {
this.change_relay(window, cx);
}
}),
);
subscriptions.push(cx.observe_new::<NostrConnectURI>(
move |connection_string, _window, cx| {
if let Ok(mut signer) = NostrConnect::new(
connection_string.to_owned(),
shared_state().client_signer.clone(),
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
None,
) {
// Automatically open remote signer's webpage when received auth url
signer.auth_url_handler(CoopAuthUrlHandler);
async_active_signer
.update(cx, |this, cx| {
*this = Some(signer);
cx.notify();
})
.ok();
}
// Update the QR Image with the new connection string
async_qr_image
.update(cx, |this, cx| {
*this = string_to_qr(&connection_string.to_string());
cx.notify();
})
.ok();
}, },
)); ));
subscriptions.push(cx.subscribe_in( subscriptions.push(cx.observe_in(
&connect_relay, &connection_string,
window, window,
move |this, _, event, window, cx| { |this, entity, _window, cx| {
if let InputEvent::PressEnter { .. } = event { let connection_string = entity.read(cx).clone();
this.change_relay(window, cx); let client_keys = shared_state().client_signer.clone();
// Update the QR Image with the new connection string
this.qr_image.update(cx, |this, cx| {
*this = string_to_qr(&connection_string.to_string());
cx.notify();
});
if let Ok(mut signer) = NostrConnect::new(
connection_string,
client_keys,
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
None,
) {
// Automatically open remote signer's webpage when received auth url
signer.auth_url_handler(CoopAuthUrlHandler);
this.active_signer.update(cx, |this, cx| {
*this = Some(signer);
cx.notify();
});
} }
}, },
)); ));
subscriptions.push( subscriptions.push(
cx.observe_in(&connect_client, window, |this, uri, window, cx| { cx.observe_in(&active_signer, window, |_this, entity, window, cx| {
let keys = get_client_keys().to_owned(); if let Some(signer) = entity.read(cx).clone() {
let (tx, rx) = oneshot::channel::<Option<NostrConnectURI>>();
if let Some(uri) = uri.read(cx).clone() { cx.background_spawn(async move {
if let Ok(qr) = string_to_qr(uri.to_string().as_str()) { if let Ok(bunker_uri) = signer.bunker_uri().await {
this.qr.update(cx, |this, cx| { tx.send(Some(bunker_uri)).ok();
*this = Some(qr);
cx.notify();
});
}
// Shutdown all previous nostr connect clients if let Err(e) = shared_state().set_signer(signer).await {
for client in std::mem::take(&mut this.signers).into_iter() { log::error!("{}", e);
cx.background_spawn(async move { }
client.shutdown().await; } else {
}) tx.send(None).ok();
.detach();
}
// Create a new nostr connect client
match NostrConnect::new(uri, keys, Duration::from_secs(200), None) {
Ok(mut signer) => {
// Handle auth url
signer.auth_url_handler(CoopAuthUrlHandler);
// Store this signer for further clean up
this.signers.push(signer.clone());
Account::global(cx).update(cx, |this, cx| {
this.login(signer, window, cx);
});
} }
Err(e) => { })
window.push_notification(Notification::error(e.to_string()), cx); .detach();
cx.spawn_in(window, async move |this, cx| {
if let Ok(Some(uri)) = rx.await {
this.update(cx, |this, cx| {
this.save_bunker(&uri, cx);
})
.ok();
} else {
cx.update(|window, cx| {
window.push_notification(
Notification::error("Connection failed"),
cx,
);
})
.ok();
} }
} })
.detach();
} }
}), }),
); );
cx.spawn_in(window, async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(300))
.await;
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.change_relay(window, cx);
})
.ok();
})
.ok();
})
.detach();
Self { Self {
key_input,
connect_relay,
connect_client,
subscriptions,
signers,
error,
qr,
is_logging_in: false,
name: "Login".into(), name: "Login".into(),
closable: true,
zoomable: true,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
is_logging_in: false,
key_input,
relay_input,
connection_string,
qr_image,
error,
active_signer,
subscriptions,
} }
} }
@@ -169,63 +222,169 @@ impl Login {
if self.is_logging_in { if self.is_logging_in {
return; return;
}; };
self.set_logging_in(true, cx); self.set_logging_in(true, cx);
let client_keys = shared_state().client_signer.clone();
let content = self.key_input.read(cx).value(); let content = self.key_input.read(cx).value();
let account = Account::global(cx);
if content.starts_with("nsec1") { if content.starts_with("nsec1") {
match SecretKey::parse(content.as_ref()) { let Ok(keys) = SecretKey::parse(content.as_ref()).map(Keys::new) else {
Ok(secret) => { self.set_error("Secret key is not valid", cx);
let keys = Keys::new(secret); return;
};
account.update(cx, |this, cx| { // Active signer is no longer needed
this.login(keys, window, cx); self.shutdown_active_signer(cx);
});
// Save these keys to the OS storage for further logins
self.save_keys(&keys, cx);
// Set signer with this keys in the background
cx.background_spawn(async move {
if let Err(e) = shared_state().set_signer(keys).await {
log::error!("{}", e);
}
})
.detach();
} else if content.starts_with("bunker://") {
let Ok(uri) = NostrConnectURI::parse(content.as_ref()) else {
self.set_error("Bunker URL is not valid", cx);
return;
};
// Active signer is no longer needed
self.shutdown_active_signer(cx);
match NostrConnect::new(
uri.clone(),
client_keys,
Duration::from_secs(NOSTR_CONNECT_TIMEOUT / 2),
None,
) {
Ok(signer) => {
let (tx, rx) = oneshot::channel::<Option<NostrConnectURI>>();
// Set signer with this remote signer in the background
cx.background_spawn(async move {
if let Ok(bunker_uri) = signer.bunker_uri().await {
tx.send(Some(bunker_uri)).ok();
if let Err(e) = shared_state().set_signer(signer).await {
log::error!("{}", e);
}
} else {
tx.send(None).ok();
}
})
.detach();
// Handle error
cx.spawn_in(window, async move |this, cx| {
if let Ok(Some(uri)) = rx.await {
this.update(cx, |this, cx| {
this.save_bunker(&uri, cx);
})
.ok();
} else {
this.update(cx, |this, cx| {
this.set_error(
"Connection to the Remote Signer failed or timed out",
cx,
);
})
.ok();
}
})
.detach();
} }
Err(e) => { Err(e) => {
self.set_error(e.to_string(), cx); self.set_error(e.to_string(), cx);
} }
} }
} else if content.starts_with("bunker://") {
let Ok(uri) = NostrConnectURI::parse(content.as_ref()) else {
self.set_error("Bunker URL is not valid".to_owned(), cx);
return;
};
self.connect_client.update(cx, |this, cx| {
*this = Some(uri);
cx.notify();
});
} else { } else {
self.set_error("You must provide a valid Private Key or Bunker.".into(), cx); self.set_error("You must provide a valid Private Key or Bunker.", cx);
}; };
} }
fn change_relay(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn change_relay(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Ok(relay_url) = let Ok(relay_url) = RelayUrl::parse(self.relay_input.read(cx).value().to_string().as_str())
RelayUrl::parse(self.connect_relay.read(cx).value().to_string().as_str())
else { else {
window.push_notification(Notification::error("Relay URL is not valid."), cx); window.push_notification(Notification::error("Relay URL is not valid."), cx);
return; return;
}; };
let client_pubkey = get_client_keys().public_key(); let client_keys = shared_state().client_signer.clone();
let uri = NostrConnectURI::client(client_pubkey, vec![relay_url], "Coop"); let uri = NostrConnectURI::client(client_keys.public_key(), vec![relay_url], "Coop");
self.connect_client.update(cx, |this, cx| { self.connection_string.update(cx, |this, cx| {
*this = Some(uri); *this = uri;
cx.notify(); cx.notify();
}); });
} }
fn set_error(&mut self, message: String, cx: &mut Context<Self>) { fn save_keys(&self, keys: &Keys, cx: &mut Context<Self>) {
let save_credential = cx.write_credentials(
KEYRING_USER_PATH,
keys.public_key().to_hex().as_str(),
keys.secret_key().as_secret_bytes(),
);
cx.background_spawn(async move {
if let Err(e) = save_credential.await {
log::error!("Failed to save keys: {}", e)
}
})
.detach();
}
fn save_bunker(&self, uri: &NostrConnectURI, cx: &mut Context<Self>) {
let mut value = uri.to_string();
// Remove the secret param if it exists
if let Some(secret) = uri.secret() {
value = value.replace(secret, "");
}
let save_credential =
cx.write_credentials(KEYRING_USER_PATH, KEYRING_BUNKER, value.as_bytes());
cx.background_spawn(async move {
if let Err(e) = save_credential.await {
log::error!("Failed to save the Bunker URI: {}", e)
}
})
.detach();
}
fn shutdown_active_signer(&self, cx: &Context<Self>) {
if let Some(signer) = self.active_signer.read(cx).clone() {
cx.background_spawn(async move {
signer.shutdown().await;
})
.detach();
}
}
fn set_error(&mut self, message: impl Into<SharedString>, cx: &mut Context<Self>) {
self.set_logging_in(false, cx); self.set_logging_in(false, cx);
self.error.update(cx, |this, cx| { self.error.update(cx, |this, cx| {
*this = Some(SharedString::new(message)); *this = Some(message.into());
cx.notify(); cx.notify();
}); });
// Clear the error message after 3 secs
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(3)).await;
this.update(cx, |this, cx| {
this.error.update(cx, |this, cx| {
*this = None;
cx.notify();
});
})
.ok();
})
.detach();
} }
fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) { fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) {
@@ -243,14 +402,6 @@ impl Panel for Login {
self.name.clone().into_any_element() self.name.clone().into_any_element()
} }
fn closable(&self, _cx: &App) -> bool {
self.closable
}
fn zoomable(&self, _cx: &App) -> bool {
self.zoomable
}
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu { fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
menu.track_focus(&self.focus_handle) menu.track_focus(&self.focus_handle)
} }
@@ -365,9 +516,10 @@ impl Render for Login {
.child("Use Nostr Connect apps to scan the code"), .child("Use Nostr Connect apps to scan the code"),
), ),
) )
.when_some(self.qr.read(cx).clone(), |this, qr| { .when_some(self.qr_image.read(cx).clone(), |this, qr| {
this.child( this.child(
div() div()
.id("")
.mb_2() .mb_2()
.p_2() .p_2()
.size_72() .size_72()
@@ -384,7 +536,27 @@ impl Render for Login {
.border_color(cx.theme().border) .border_color(cx.theme().border)
}) })
.bg(cx.theme().background) .bg(cx.theme().background)
.child(img(qr).h_64()), .child(img(qr).h_64())
.on_click(cx.listener(move |this, _, window, cx| {
#[cfg(any(
target_os = "linux",
target_os = "freebsd"
))]
cx.write_to_clipboard(ClipboardItem::new_string(
this.connection_string.read(cx).to_string(),
));
#[cfg(any(
target_os = "macos",
target_os = "windows"
))]
cx.write_to_clipboard(ClipboardItem::new_string(
this.connection_string.read(cx).to_string(),
));
window.push_notification(
"Connection String has been copied",
cx,
);
})),
) )
}) })
.child( .child(
@@ -394,7 +566,7 @@ impl Render for Login {
.items_center() .items_center()
.justify_center() .justify_center()
.gap_1() .gap_1()
.child(TextInput::new(&self.connect_relay).xsmall()) .child(TextInput::new(&self.relay_input).xsmall())
.child( .child(
Button::new("change") Button::new("change")
.label("Change") .label("Change")

View File

@@ -6,5 +6,6 @@ pub mod onboarding;
pub mod profile; pub mod profile;
pub mod relays; pub mod relays;
pub mod sidebar; pub mod sidebar;
pub mod startup;
pub mod subject; pub mod subject;
pub mod welcome; pub mod welcome;

View File

@@ -1,24 +1,21 @@
use std::str::FromStr;
use account::Account;
use async_utility::task::spawn; use async_utility::task::spawn;
use common::nip96_upload; use common::nip96_upload;
use global::get_client; use global::constants::KEYRING_USER_PATH;
use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, prelude::FluentBuilder, relative, AnyElement, App, AppContext, Context, Entity, div, img, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten,
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString,
Render, SharedString, Styled, Window, Styled, Window,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smol::fs; use smol::fs;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{ use ui::button::{Button, ButtonVariants};
button::{Button, ButtonVariants}, use ui::dock_area::panel::{Panel, PanelEvent};
dock_area::panel::{Panel, PanelEvent}, use ui::input::{InputState, TextInput};
input::{InputState, TextInput}, use ui::popup_menu::PopupMenu;
popup_menu::PopupMenu, use ui::{Disableable, Icon, IconName, Sizable, StyledExt};
Disableable, Icon, IconName, Sizable, StyledExt,
};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
NewAccount::new(window, cx) NewAccount::new(window, cx)
@@ -44,8 +41,10 @@ impl NewAccount {
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self { fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice")); let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
let avatar_input = let avatar_input =
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg")); cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg"));
let bio_input = cx.new(|cx| { let bio_input = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
.multi_line() .multi_line()
@@ -65,22 +64,33 @@ impl NewAccount {
} }
} }
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn submit(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.set_submitting(true, cx); self.set_submitting(true, cx);
let avatar = self.avatar_input.read(cx).value().to_string(); let avatar = self.avatar_input.read(cx).value().to_string();
let name = self.name_input.read(cx).value().to_string(); let name = self.name_input.read(cx).value().to_string();
let bio = self.bio_input.read(cx).value().to_string(); let bio = self.bio_input.read(cx).value().to_string();
let keys = Keys::generate();
let mut metadata = Metadata::new().display_name(name).about(bio); let mut metadata = Metadata::new().display_name(name).about(bio);
if let Ok(url) = Url::from_str(&avatar) { if let Ok(url) = Url::parse(&avatar) {
metadata = metadata.picture(url); metadata = metadata.picture(url);
}; };
Account::global(cx).update(cx, |this, cx| { let save_credential = cx.write_credentials(
this.new_account(metadata, window, cx); KEYRING_USER_PATH,
}); keys.public_key().to_hex().as_str(),
keys.secret_key().as_secret_bytes(),
);
cx.background_spawn(async move {
if let Err(e) = save_credential.await {
log::error!("Failed to save keys: {}", e)
};
shared_state().new_account(keys, metadata).await;
})
.detach();
} }
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -109,11 +119,10 @@ impl NewAccount {
}; };
if let Ok(file_data) = fs::read(path).await { if let Ok(file_data) = fs::read(path).await {
let client = get_client();
let (tx, rx) = oneshot::channel::<Url>(); let (tx, rx) = oneshot::channel::<Url>();
spawn(async move { spawn(async move {
if let Ok(url) = nip96_upload(client, file_data).await { if let Ok(url) = nip96_upload(&shared_state().client, file_data).await {
_ = tx.send(url); _ = tx.send(url);
} }
}); });

View File

@@ -3,12 +3,10 @@ use gpui::{
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{ use ui::button::{Button, ButtonVariants};
button::{Button, ButtonVariants}, use ui::dock_area::panel::{Panel, PanelEvent};
dock_area::panel::{Panel, PanelEvent}, use ui::popup_menu::PopupMenu;
popup_menu::PopupMenu, use ui::{Icon, IconName, StyledExt};
Icon, IconName, StyledExt,
};
use crate::chatspace; use crate::chatspace;

View File

@@ -1,19 +1,20 @@
use std::str::FromStr;
use std::time::Duration;
use async_utility::task::spawn; use async_utility::task::spawn;
use common::nip96_upload; use common::nip96_upload;
use global::get_client; use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, prelude::FluentBuilder, App, AppContext, Context, Entity, Flatten, IntoElement, div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
ParentElement, PathPromptOptions, Render, Styled, Task, Window, PathPromptOptions, Render, Styled, Task, Window,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smol::fs; use smol::fs;
use std::{str::FromStr, time::Duration};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{ use ui::button::{Button, ButtonVariants};
button::{Button, ButtonVariants}, use ui::input::{InputState, TextInput};
input::{InputState, TextInput}, use ui::{ContextModal, Disableable, IconName, Sizable};
ContextModal, Disableable, IconName, Sizable,
};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Profile> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<Profile> {
Profile::new(window, cx) Profile::new(window, cx)
@@ -54,10 +55,10 @@ impl Profile {
}; };
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move { let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
let client = get_client(); let signer = shared_state().client.signer().await?;
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
let metadata = client let metadata = shared_state()
.client
.fetch_metadata(public_key, Duration::from_secs(2)) .fetch_metadata(public_key, Duration::from_secs(2))
.await?; .await?;
@@ -122,8 +123,7 @@ impl Profile {
let (tx, rx) = oneshot::channel::<Url>(); let (tx, rx) = oneshot::channel::<Url>();
spawn(async move { spawn(async move {
let client = get_client(); if let Ok(url) = nip96_upload(&shared_state().client, file_data).await {
if let Ok(url) = nip96_upload(client, file_data).await {
_ = tx.send(url); _ = tx.send(url);
} }
}); });
@@ -189,9 +189,7 @@ impl Profile {
} }
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = get_client(); let _ = shared_state().client.set_metadata(&new_metadata).await?;
_ = client.set_metadata(&new_metadata).await?;
Ok(()) Ok(())
}); });

View File

@@ -1,18 +1,17 @@
use anyhow::Error; use anyhow::Error;
use global::{constants::NEW_MESSAGE_SUB_ID, get_client}; use global::constants::NEW_MESSAGE_SUB_ID;
use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, prelude::FluentBuilder, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, div, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, TextAlign, IntoElement, ParentElement, Render, Styled, Subscription, Task, TextAlign, UniformList, Window,
UniformList, Window,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{ use ui::button::{Button, ButtonVariants};
button::{Button, ButtonVariants}, use ui::input::{InputEvent, InputState, TextInput};
input::{InputEvent, InputState, TextInput}, use ui::{ContextModal, Disableable, IconName, Sizable};
ContextModal, Disableable, IconName, Sizable,
};
const MIN_HEIGHT: f32 = 200.0; const MIN_HEIGHT: f32 = 200.0;
const MESSAGE: &str = "In order to receive messages from others, you need to setup at least one Messaging Relay. You can use the recommend relays or add more."; const MESSAGE: &str = "In order to receive messages from others, you need to setup at least one Messaging Relay. You can use the recommend relays or add more.";
@@ -36,16 +35,20 @@ impl Relays {
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com")); let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
let relays = cx.new(|cx| { let relays = cx.new(|cx| {
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move { let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
let client = get_client(); let signer = shared_state().client.signer().await?;
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::InboxRelays) .kind(Kind::InboxRelays)
.author(public_key) .author(public_key)
.limit(1); .limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() { if let Some(event) = shared_state()
.client
.database()
.query(filter)
.await?
.first_owned()
{
let relays = event let relays = event
.tags .tags
.filter(TagKind::Relay) .filter(TagKind::Relay)
@@ -108,18 +111,23 @@ impl Relays {
let relays = self.relays.read(cx).clone(); let relays = self.relays.read(cx).clone();
let task: Task<Result<EventId, Error>> = cx.background_spawn(async move { let task: Task<Result<EventId, Error>> = cx.background_spawn(async move {
let client = get_client(); let signer = shared_state().client.signer().await?;
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
// If user didn't have any NIP-65 relays, add default ones // If user didn't have any NIP-65 relays, add default ones
if client.database().relay_list(public_key).await?.is_empty() { if shared_state()
.client
.database()
.relay_list(public_key)
.await?
.is_empty()
{
let builder = EventBuilder::relay_list(vec![ let builder = EventBuilder::relay_list(vec![
(RelayUrl::parse("wss://relay.damus.io/").unwrap(), None), (RelayUrl::parse("wss://relay.damus.io/").unwrap(), None),
(RelayUrl::parse("wss://relay.primal.net/").unwrap(), None), (RelayUrl::parse("wss://relay.primal.net/").unwrap(), None),
]); ]);
if let Err(e) = client.send_event_builder(builder).await { if let Err(e) = shared_state().client.send_event_builder(builder).await {
log::error!("Failed to send relay list event: {}", e); log::error!("Failed to send relay list event: {}", e);
} }
} }
@@ -130,21 +138,22 @@ impl Relays {
.collect(); .collect();
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags); let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
let output = client.send_event_builder(builder).await?; let output = shared_state().client.send_event_builder(builder).await?;
// Connect to messaging relays // Connect to messaging relays
for relay in relays.into_iter() { for relay in relays.into_iter() {
_ = client.add_relay(&relay).await; _ = shared_state().client.add_relay(&relay).await;
_ = client.connect_relay(&relay).await; _ = shared_state().client.connect_relay(&relay).await;
} }
let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID); let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
// Close old subscription // Close old subscription
client.unsubscribe(&sub_id).await; shared_state().client.unsubscribe(&sub_id).await;
// Subscribe to new messages // Subscribe to new messages
if let Err(e) = client if let Err(e) = shared_state()
.client
.subscribe_with_id( .subscribe_with_id(
sub_id, sub_id,
Filter::new() Filter::new()

View File

@@ -1,11 +1,13 @@
use std::rc::Rc; use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, prelude::FluentBuilder, rems, App, ClickEvent, Div, InteractiveElement, IntoElement, div, img, rems, App, ClickEvent, Div, InteractiveElement, IntoElement, ParentElement as _,
ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{avatar::Avatar, StyledExt}; use ui::avatar::Avatar;
use ui::StyledExt;
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct DisplayRoom { pub struct DisplayRoom {

View File

@@ -1,33 +1,32 @@
use std::{collections::BTreeSet, ops::Range, time::Duration}; use std::collections::BTreeSet;
use std::ops::Range;
use std::time::Duration;
use account::Account;
use async_utility::task::spawn; use async_utility::task::spawn;
use chats::{ use chats::room::{Room, RoomKind};
room::{Room, RoomKind}, use chats::{ChatRegistry, RoomEmitter};
ChatRegistry, RoomEmitter, use common::debounced_delay::DebouncedDelay;
}; use common::profile::RenderProfile;
use common::{debounced_delay::DebouncedDelay, profile::RenderProfile};
use element::DisplayRoom; use element::DisplayRoom;
use global::{constants::SEARCH_RELAYS, get_client}; use global::constants::SEARCH_RELAYS;
use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, prelude::FluentBuilder, rems, uniform_list, AnyElement, App, AppContext, Context, Entity, div, rems, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter,
EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString,
SharedString, Styled, Subscription, Task, Window, Styled, Subscription, Task, Window,
}; };
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{ use ui::avatar::Avatar;
avatar::Avatar, use ui::button::{Button, ButtonRounded, ButtonVariants};
button::{Button, ButtonRounded, ButtonVariants}, use ui::dock_area::panel::{Panel, PanelEvent};
dock_area::panel::{Panel, PanelEvent}, use ui::input::{InputEvent, InputState, TextInput};
input::{InputEvent, InputState, TextInput}, use ui::popup_menu::{PopupMenu, PopupMenuExt};
popup_menu::{PopupMenu, PopupMenuExt}, use ui::skeleton::Skeleton;
skeleton::Skeleton, use ui::{ContextModal, IconName, Selectable, Sizable, StyledExt};
ContextModal, IconName, Selectable, Sizable, StyledExt,
};
use crate::chatspace::{ModalKind, ToggleModal}; use crate::chatspace::{ModalKind, ToggleModal};
@@ -142,14 +141,13 @@ impl Sidebar {
let query = self.find_input.read(cx).value().clone(); let query = self.find_input.read(cx).value().clone();
cx.background_spawn(async move { cx.background_spawn(async move {
let client = get_client();
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::Metadata) .kind(Kind::Metadata)
.search(query.to_lowercase()) .search(query.to_lowercase())
.limit(FIND_LIMIT); .limit(FIND_LIMIT);
let events = client let events = shared_state()
.client
.fetch_events_from(SEARCH_RELAYS, filter, Duration::from_secs(3)) .fetch_events_from(SEARCH_RELAYS, filter, Duration::from_secs(3))
.await? .await?
.into_iter() .into_iter()
@@ -160,8 +158,11 @@ impl Sidebar {
let (tx, rx) = smol::channel::bounded::<Room>(10); let (tx, rx) = smol::channel::bounded::<Room>(10);
spawn(async move { spawn(async move {
let client = get_client(); let signer = shared_state()
let signer = client.signer().await.expect("signer is required"); .client
.signer()
.await
.expect("signer is required");
let public_key = signer.get_public_key().await.expect("error"); let public_key = signer.get_public_key().await.expect("error");
for event in events.into_iter() { for event in events.into_iter() {
@@ -492,9 +493,10 @@ impl Render for Sidebar {
.flex_col() .flex_col()
.gap_3() .gap_3()
// Account // Account
.when_some(Account::get_global(cx).profile_ref(), |this, profile| { .when_some(
this.child(self.render_account(profile, cx)) shared_state().identity.read_blocking().as_ref(),
}) |this, profile| this.child(self.render_account(profile, cx)),
)
// Search Input // Search Input
.child( .child(
div().px_3().w_full().h_7().flex_none().child( div().px_3().w_full().h_7().flex_none().child(

View File

@@ -0,0 +1,88 @@
use gpui::{
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Window,
};
use theme::ActiveTheme;
use ui::button::Button;
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::indicator::Indicator;
use ui::popup_menu::PopupMenu;
use ui::Sizable;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Startup> {
Startup::new(window, cx)
}
pub struct Startup {
name: SharedString,
focus_handle: FocusHandle,
}
impl Startup {
fn new(_window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self {
name: "Welcome".into(),
focus_handle: cx.focus_handle(),
})
}
}
impl Panel for Startup {
fn panel_id(&self) -> SharedString {
self.name.clone()
}
fn title(&self, _cx: &App) -> AnyElement {
"Startup".into_any_element()
}
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
menu.track_focus(&self.focus_handle)
}
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
vec![]
}
}
impl EventEmitter<PanelEvent> for Startup {}
impl Focusable for Startup {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Startup {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.flex()
.items_center()
.justify_center()
.child(
div()
.flex()
.flex_col()
.items_center()
.gap_6()
.child(
svg()
.path("brand/coop.svg")
.size_12()
.text_color(cx.theme().elevated_surface_background),
)
.child(
div()
.flex()
.items_center()
.gap_1p5()
.text_xs()
.text_center()
.text_color(cx.theme().text_muted)
.child("Connection in progress")
.child(Indicator::new().small()),
),
)
}
}

View File

@@ -4,11 +4,9 @@ use gpui::{
ParentElement, Render, Styled, Window, ParentElement, Render, Styled, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{ use ui::button::{Button, ButtonVariants};
button::{Button, ButtonVariants}, use ui::input::{InputState, TextInput};
input::{InputState, TextInput}, use ui::{ContextModal, Sizable};
ContextModal, Sizable,
};
pub fn init( pub fn init(
id: u64, id: u64,

View File

@@ -3,12 +3,10 @@ use gpui::{
IntoElement, ParentElement, Render, SharedString, Styled, Window, IntoElement, ParentElement, Render, SharedString, Styled, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{ use ui::button::Button;
button::Button, use ui::dock_area::panel::{Panel, PanelEvent};
dock_area::panel::{Panel, PanelEvent}, use ui::popup_menu::PopupMenu;
popup_menu::PopupMenu, use ui::StyledExt;
StyledExt,
};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Welcome> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<Welcome> {
Welcome::new(window, cx) Welcome::new(window, cx)

View File

@@ -5,9 +5,14 @@ edition.workspace = true
publish.workspace = true publish.workspace = true
[dependencies] [dependencies]
nostr-keyring.workspace = true
nostr-connect.workspace = true
nostr-sdk.workspace = true nostr-sdk.workspace = true
dirs.workspace = true dirs.workspace = true
smol.workspace = true smol.workspace = true
futures.workspace = true
log.workspace = true
anyhow.workspace = true
whoami = "1.5.2" whoami = "1.5.2"
rustls = "0.23.23" rustls = "0.23.23"

View File

@@ -1,6 +1,9 @@
pub const APP_NAME: &str = "Coop"; pub const APP_NAME: &str = "Coop";
pub const APP_ID: &str = "su.reya.coop"; pub const APP_ID: &str = "su.reya.coop";
pub const APP_PUBKEY: &str = "b1813fb01274b32cc5db6d1198e7c79dda0fb430899f63c7064f651a41d44f2b"; pub const APP_PUBKEY: &str = "b1813fb01274b32cc5db6d1198e7c79dda0fb430899f63c7064f651a41d44f2b";
pub const KEYRING_PATH: &str = "Coop Safe Storage";
pub const KEYRING_USER_PATH: &str = "coop";
pub const KEYRING_BUNKER: &str = "bunker";
/// Bootstrap Relays. /// Bootstrap Relays.
pub const BOOTSTRAP_RELAYS: [&str; 4] = [ pub const BOOTSTRAP_RELAYS: [&str; 4] = [
@@ -32,3 +35,13 @@ pub const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";
/// NIP96 Media Server. /// NIP96 Media Server.
pub const NIP96_SERVER: &str = "https://nostrmedia.com"; pub const NIP96_SERVER: &str = "https://nostrmedia.com";
pub(crate) const GLOBAL_CHANNEL_LIMIT: usize = 2048;
pub(crate) const BATCH_CHANNEL_LIMIT: usize = 1024;
pub(crate) const NIP17_RELAYS: [&str; 2] = ["wss://auth.nostr1.com", "wss://relay.0xchat.com"];
pub(crate) const NIP65_RELAYS: [&str; 4] = [
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://relay.nostr.net",
"wss://nos.lol",
];

View File

@@ -1,110 +1,598 @@
//! Global state management for the Nostr client application.
//!
//! This module provides a singleton global state that manages:
//! - Nostr client connections and event handling
//! - User identity and profile management
//! - Batched metadata fetching for performance
//! - Cross-component communication via channels
use std::collections::{BTreeMap, BTreeSet};
use std::mem;
use std::sync::OnceLock;
use std::time::Duration;
use anyhow::{anyhow, Error};
use constants::{
ALL_MESSAGES_SUB_ID, APP_ID, APP_PUBKEY, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT,
METADATA_BATCH_TIMEOUT, NEW_MESSAGE_SUB_ID, SEARCH_RELAYS,
};
use nostr_keyring::prelude::*;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use paths::nostr_file; use paths::nostr_file;
use smol::lock::RwLock; use smol::lock::RwLock;
use std::{collections::BTreeMap, sync::OnceLock, time::Duration}; use crate::constants::{
BATCH_CHANNEL_LIMIT, GLOBAL_CHANNEL_LIMIT, KEYRING_PATH, NIP17_RELAYS, NIP65_RELAYS,
};
pub mod constants; pub mod constants;
pub mod paths; pub mod paths;
/// Represents the global state of the Nostr client, including: /// Global singleton instance for application state
/// - The Nostr client instance static GLOBALS: OnceLock<Globals> = OnceLock::new();
/// - Client keys
/// - A cache of user profiles (metadata) /// Signals sent through the global event channel to notify UI components
pub struct NostrState { #[derive(Debug)]
keys: Keys, pub enum NostrSignal {
client: Client, /// User's signing keys have been updated
cache_profiles: RwLock<BTreeMap<PublicKey, Option<Metadata>>>, SignerUpdated,
/// User's signing keys have been unset
SignerUnset,
/// New Nostr event received
Event(Event),
/// Application update event received
AppUpdate(Event),
/// End of stored events received from relay
Eose,
} }
/// Global singleton instance of NostrState /// Global application state containing Nostr client and shared resources
static GLOBAL_STATE: OnceLock<NostrState> = OnceLock::new(); pub struct Globals {
/// The Nostr SDK client
pub client: Client,
/// Cryptographic keys for signing Nostr events
pub client_signer: Keys,
/// Current user's profile information (pubkey and metadata)
pub identity: RwLock<Option<Profile>>,
/// Auto-close options for subscriptions to prevent memory leaks
pub auto_close: Option<SubscribeAutoCloseOptions>,
/// Channel sender for broadcasting global Nostr events to UI
pub global_sender: smol::channel::Sender<NostrSignal>,
/// Channel receiver for handling global Nostr events
pub global_receiver: smol::channel::Receiver<NostrSignal>,
/// Channel sender for batching public keys for metadata fetching
pub batch_sender: smol::channel::Sender<Vec<PublicKey>>,
/// Channel receiver for processing batched public key requests
pub batch_receiver: smol::channel::Receiver<Vec<PublicKey>>,
/// Cache of user profiles mapped by their public keys
pub persons: RwLock<BTreeMap<PublicKey, Option<Metadata>>>,
}
/// Initializes and returns a new NostrState instance with: /// Returns the global singleton instance, initializing it if necessary
/// - LMDB database backend pub fn shared_state() -> &'static Globals {
/// - Default client options (gossip enabled, 800ms max avg latency) GLOBALS.get_or_init(|| {
/// - Newly generated keys // rustls uses the `aws_lc_rs` provider by default
/// - Empty profile cache // This only errors if the default provider has already
pub fn init_global_state() -> NostrState { // been installed. We can ignore this `Result`.
// rustls uses the `aws_lc_rs` provider by default rustls::crypto::aws_lc_rs::default_provider()
// This only errors if the default provider has already .install_default()
// been installed. We can ignore this `Result`. .ok();
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.ok();
// Setup database let keyring = NostrKeyring::new(KEYRING_PATH);
let db_path = nostr_file(); // Get the client signer or generate a new one if it doesn't exist
let lmdb = NostrLMDB::open(db_path).expect("Database is NOT initialized"); let client_signer = if let Ok(keys) = keyring.get("client") {
keys
} else {
let keys = Keys::generate();
if let Err(e) = keyring.set("client", &keys) {
log::error!("Failed to save client keys: {}", e);
}
keys
};
// Client options let opts = Options::new().gossip(true);
let opts = Options::new() let lmdb = NostrLMDB::open(nostr_file()).expect("Database is NOT initialized");
.gossip(true)
.max_avg_latency(Duration::from_millis(800));
NostrState { let (global_sender, global_receiver) =
client: ClientBuilder::default().database(lmdb).opts(opts).build(), smol::channel::bounded::<NostrSignal>(GLOBAL_CHANNEL_LIMIT);
keys: Keys::generate(),
cache_profiles: RwLock::new(BTreeMap::new()), let (batch_sender, batch_receiver) =
smol::channel::bounded::<Vec<PublicKey>>(BATCH_CHANNEL_LIMIT);
Globals {
client: ClientBuilder::default().database(lmdb).opts(opts).build(),
identity: RwLock::new(None),
persons: RwLock::new(BTreeMap::new()),
auto_close: Some(
SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE),
),
client_signer,
global_sender,
global_receiver,
batch_sender,
batch_receiver,
}
})
}
impl Globals {
/// Starts the global event processing system and metadata batching
pub async fn start(&self) {
self.connect().await;
self.subscribe_for_app_updates().await;
nostr_sdk::async_utility::task::spawn(async move {
let mut batch: BTreeSet<PublicKey> = BTreeSet::new();
let timeout_duration = Duration::from_millis(METADATA_BATCH_TIMEOUT);
loop {
let timeout = smol::Timer::after(timeout_duration);
/// Internal events for the metadata batching system
enum BatchEvent {
/// New public keys to add to the batch
NewKeys(Vec<PublicKey>),
/// Timeout reached, process current batch
Timeout,
/// Channel was closed, shutdown gracefully
ChannelClosed,
}
let event = smol::future::or(
async {
match shared_state().batch_receiver.recv().await {
Ok(public_keys) => BatchEvent::NewKeys(public_keys),
Err(_) => BatchEvent::ChannelClosed,
}
},
async {
timeout.await;
BatchEvent::Timeout
},
)
.await;
match event {
BatchEvent::NewKeys(public_keys) => {
batch.extend(public_keys);
// Process immediately if batch limit reached
if batch.len() >= METADATA_BATCH_LIMIT {
shared_state()
.sync_data_for_pubkeys(mem::take(&mut batch))
.await;
}
}
BatchEvent::Timeout => {
// Process current batch if not empty
if !batch.is_empty() {
shared_state()
.sync_data_for_pubkeys(mem::take(&mut batch))
.await;
}
}
BatchEvent::ChannelClosed => {
// Process remaining batch and exit
if !batch.is_empty() {
shared_state().sync_data_for_pubkeys(batch).await;
}
break;
}
}
}
});
let mut notifications = self.client.notifications();
let mut processed_events: BTreeSet<EventId> = BTreeSet::new();
while let Ok(notification) = notifications.recv().await {
if let RelayPoolNotification::Message { message, .. } = notification {
match message {
RelayMessage::Event {
event,
subscription_id,
} => {
if processed_events.contains(&event.id) {
continue;
}
// Skip events that have already been processed
processed_events.insert(event.id);
match event.kind {
Kind::GiftWrap => {
self.unwrap_event(&subscription_id, &event).await;
}
Kind::Metadata => {
self.insert_person(&event).await;
}
Kind::ContactList => {
self.extract_pubkeys_and_sync(&event).await;
}
Kind::ReleaseArtifactSet => {
self.notify_update(&event).await;
}
_ => {}
}
}
RelayMessage::EndOfStoredEvents(subscription_id) => {
if *subscription_id == SubscriptionId::new(ALL_MESSAGES_SUB_ID) {
self.global_sender.send(NostrSignal::Eose).await.ok();
}
}
_ => {}
}
}
}
}
/// Sets a new signer for the client and updates user identity
pub async fn set_signer<S>(&self, signer: S) -> Result<(), Error>
where
S: NostrSigner + 'static,
{
let public_key = signer.get_public_key().await?;
// Update signer
self.client.set_signer(signer).await;
// Fetch user's metadata
let metadata = shared_state()
.client
.fetch_metadata(public_key, Duration::from_secs(2))
.await?
.unwrap_or_default();
let profile = Profile::new(public_key, metadata);
let mut identity_guard = self.identity.write().await;
// Update the identity
*identity_guard = Some(profile);
// Subscribe for user's data
nostr_sdk::async_utility::task::spawn(async move {
shared_state().subscribe_for_user_data().await;
});
// Notify GPUi via the global channel
self.global_sender.send(NostrSignal::SignerUpdated).await?;
Ok(())
}
pub async fn unset_signer(&self) {
self.client.reset().await;
if let Err(e) = self.global_sender.send(NostrSignal::SignerUnset).await {
log::error!("Failed to send signal to global channel: {}", e);
}
}
/// Creates a new account with the given keys and metadata
pub async fn new_account(&self, keys: Keys, metadata: Metadata) {
let profile = Profile::new(keys.public_key(), metadata.clone());
// Update signer
self.client.set_signer(keys).await;
// Set metadata
self.client.set_metadata(&metadata).await.ok();
// Create relay list
let builder = EventBuilder::new(Kind::RelayList, "").tags(
NIP65_RELAYS.into_iter().filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay_metadata(url, None))
} else {
None
}
}),
);
if let Err(e) = self.client.send_event_builder(builder).await {
log::error!("Failed to send relay list event: {}", e);
};
// Create messaging relay list
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(
NIP17_RELAYS.into_iter().filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay(url))
} else {
None
}
}),
);
if let Err(e) = self.client.send_event_builder(builder).await {
log::error!("Failed to send messaging relay list event: {}", e);
};
let mut guard = self.identity.write().await;
// Update the identity
*guard = Some(profile);
// Notify GPUi via the global channel
self.global_sender
.send(NostrSignal::SignerUpdated)
.await
.ok();
// Subscribe
self.subscribe_for_user_data().await;
}
/// Returns the current user's profile (blocking)
pub fn identity(&self) -> Option<Profile> {
self.identity.read_blocking().as_ref().cloned()
}
/// Returns the current user's profile (async)
pub async fn async_identity(&self) -> Option<Profile> {
self.identity.read().await.as_ref().cloned()
}
/// Gets a person's profile from cache or creates default (blocking)
pub fn person(&self, public_key: &PublicKey) -> Profile {
let metadata = if let Some(metadata) = self.persons.read_blocking().get(public_key) {
metadata.clone().unwrap_or_default()
} else {
Metadata::default()
};
Profile::new(*public_key, metadata)
}
/// Gets a person's profile from cache or creates default (async)
pub async fn async_person(&self, public_key: &PublicKey) -> Profile {
let metadata = if let Some(metadata) = self.persons.read().await.get(public_key) {
metadata.clone().unwrap_or_default()
} else {
Metadata::default()
};
Profile::new(*public_key, metadata)
}
/// Connects to bootstrap and configured relays
async fn connect(&self) {
for relay in BOOTSTRAP_RELAYS.into_iter() {
if let Err(e) = self.client.add_relay(relay).await {
log::error!("Failed to add relay {}: {}", relay, e);
}
}
for relay in SEARCH_RELAYS.into_iter() {
if let Err(e) = self.client.add_relay(relay).await {
log::error!("Failed to add relay {}: {}", relay, e);
}
}
// Establish connection to relays
self.client.connect().await;
log::info!("Connected to bootstrap relays");
}
/// Subscribes to user-specific data feeds (DMs, mentions, etc.)
async fn subscribe_for_user_data(&self) {
let Some(profile) = self.identity.read().await.clone() else {
return;
};
let public_key = profile.public_key();
let metadata = Filter::new()
.kinds(vec![
Kind::Metadata,
Kind::ContactList,
Kind::InboxRelays,
Kind::MuteList,
Kind::SimpleGroups,
])
.author(public_key)
.limit(10);
let data = Filter::new()
.author(public_key)
.kinds(vec![
Kind::Metadata,
Kind::ContactList,
Kind::MuteList,
Kind::SimpleGroups,
Kind::InboxRelays,
Kind::RelayList,
])
.since(Timestamp::now());
let msg = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let new_msg = Filter::new()
.kind(Kind::GiftWrap)
.pubkey(public_key)
.limit(0);
let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
let new_messages_sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
let opts = shared_state().auto_close;
self.client.subscribe(data, None).await.ok();
self.client
.subscribe(metadata, shared_state().auto_close)
.await
.ok();
self.client
.subscribe_with_id(all_messages_sub_id, msg, opts)
.await
.ok();
self.client
.subscribe_with_id(new_messages_sub_id, new_msg, None)
.await
.ok();
log::info!("Subscribing to user's metadata...");
}
/// Subscribes to application update notifications
async fn subscribe_for_app_updates(&self) {
let coordinate = Coordinate {
kind: Kind::Custom(32267),
public_key: PublicKey::from_hex(APP_PUBKEY).expect("App Pubkey is invalid"),
identifier: APP_ID.into(),
};
let filter = Filter::new()
.kind(Kind::ReleaseArtifactSet)
.coordinate(&coordinate)
.limit(1);
if let Err(e) = self
.client
.subscribe_to(BOOTSTRAP_RELAYS, filter, shared_state().auto_close)
.await
{
log::error!("Failed to subscribe for app updates: {}", e);
}
log::info!("Subscribing to app updates...");
}
/// Stores an unwrapped event in local database with reference to original
async fn set_unwrapped(&self, root: EventId, event: &Event, keys: &Keys) -> Result<(), Error> {
// Must be use the random generated keys to sign this event
let event = EventBuilder::new(Kind::Custom(30078), event.as_json())
.tags(vec![Tag::identifier(root), Tag::event(root)])
.sign(keys)
.await?;
// Only save this event into the local database
self.client.database().save_event(&event).await?;
Ok(())
}
/// Retrieves a previously unwrapped event from local database
async fn get_unwrapped(&self, target: EventId) -> Result<Event, Error> {
let filter = Filter::new()
.kind(Kind::Custom(30078))
.event(target)
.limit(1);
if let Some(event) = self.client.database().query(filter).await?.first_owned() {
Ok(Event::from_json(event.content)?)
} else {
Err(anyhow!("Event not found"))
}
}
/// Unwraps a gift-wrapped event and processes its contents
async fn unwrap_event(&self, subscription_id: &SubscriptionId, event: &Event) {
let new_messages_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
let random_keys = Keys::generate();
let event = match self.get_unwrapped(event.id).await {
Ok(event) => event,
Err(_) => match self.client.unwrap_gift_wrap(event).await {
Ok(unwrap) => match unwrap.rumor.sign_with_keys(&random_keys) {
Ok(unwrapped) => {
self.set_unwrapped(event.id, &unwrapped, &random_keys)
.await
.ok();
unwrapped
}
Err(_) => return,
},
Err(_) => return,
},
};
let mut pubkeys = vec![];
pubkeys.extend(event.tags.public_keys());
pubkeys.push(event.pubkey);
// Send all pubkeys to the batch to sync metadata
self.batch_sender.send(pubkeys).await.ok();
// Save the event to the database, use for query directly.
self.client.database().save_event(&event).await.ok();
// Send this event to the GPUI
if subscription_id == &new_messages_id {
self.global_sender
.send(NostrSignal::Event(event))
.await
.ok();
}
}
/// Extracts public keys from contact list and queues metadata sync
async fn extract_pubkeys_and_sync(&self, event: &Event) {
if let Ok(signer) = self.client.signer().await {
if let Ok(public_key) = signer.get_public_key().await {
if public_key == event.pubkey {
let pubkeys = event.tags.public_keys().copied().collect::<Vec<_>>();
self.batch_sender.send(pubkeys).await.ok();
}
}
}
}
/// Fetches metadata for a batch of public keys
async fn sync_data_for_pubkeys(&self, public_keys: BTreeSet<PublicKey>) {
let kinds = vec![
Kind::Metadata,
Kind::ContactList,
Kind::InboxRelays,
Kind::UserStatus,
];
let filter = Filter::new()
.limit(public_keys.len() * kinds.len())
.authors(public_keys)
.kinds(kinds);
if let Err(e) = shared_state()
.client
.subscribe_to(BOOTSTRAP_RELAYS, filter, shared_state().auto_close)
.await
{
log::error!("Failed to sync metadata: {e}");
}
}
/// Inserts or updates a person's metadata from a Kind::Metadata event
async fn insert_person(&self, event: &Event) {
let metadata = Metadata::from_json(&event.content).ok();
self.persons
.write()
.await
.entry(event.pubkey)
.and_modify(|entry| {
if entry.is_none() {
*entry = metadata.clone();
}
})
.or_insert_with(|| metadata);
}
/// Notifies UI of application updates via global channel
async fn notify_update(&self, event: &Event) {
let filter = Filter::new()
.ids(event.tags.event_ids().copied())
.kind(Kind::FileMetadata);
if let Err(e) = self
.client
.subscribe_to(BOOTSTRAP_RELAYS, filter, self.auto_close)
.await
{
log::error!("Failed to subscribe for file metadata: {}", e);
} else {
self.global_sender
.send(NostrSignal::AppUpdate(event.to_owned()))
.await
.ok();
}
} }
} }
/// Returns a reference to the global Nostr client instance.
///
/// Initializes the global state if it hasn't been initialized yet.
pub fn get_client() -> &'static Client {
&GLOBAL_STATE.get_or_init(init_global_state).client
}
/// Returns a reference to the client's cryptographic keys.
///
/// Initializes the global state if it hasn't been initialized yet.
pub fn get_client_keys() -> &'static Keys {
&GLOBAL_STATE.get_or_init(init_global_state).keys
}
/// Returns a reference to the global profile cache (thread-safe).
///
/// Initializes the global state if it hasn't been initialized yet.
pub fn profiles() -> &'static RwLock<BTreeMap<PublicKey, Option<Metadata>>> {
&GLOBAL_STATE.get_or_init(init_global_state).cache_profiles
}
/// Synchronously gets a profile from the cache by public key.
///
/// Returns default metadata if the profile is not cached.
pub fn get_cache_profile(key: &PublicKey) -> Profile {
let metadata = if let Some(metadata) = profiles().read_blocking().get(key) {
metadata.clone().unwrap_or_default()
} else {
Metadata::default()
};
Profile::new(*key, metadata)
}
/// Asynchronously gets a profile from the cache by public key.
///
/// Returns default metadata if the profile isn't cached.
pub async fn async_cache_profile(key: &PublicKey) -> Profile {
let metadata = if let Some(metadata) = profiles().read().await.get(key) {
metadata.clone().unwrap_or_default()
} else {
Metadata::default()
};
Profile::new(*key, metadata)
}
/// Synchronously inserts or updates a profile in the cache.
pub fn insert_cache_profile(key: PublicKey, metadata: Option<Metadata>) {
profiles()
.write_blocking()
.entry(key)
.and_modify(|entry| {
if entry.is_none() {
*entry = metadata.clone();
}
})
.or_insert_with(|| metadata);
}

View File

@@ -9,19 +9,6 @@ use gpui::{Hsla, SharedString};
pub struct ColorScaleStep(usize); pub struct ColorScaleStep(usize);
impl ColorScaleStep { impl ColorScaleStep {
pub const ONE: Self = Self(1);
pub const TWO: Self = Self(2);
pub const THREE: Self = Self(3);
pub const FOUR: Self = Self(4);
pub const FIVE: Self = Self(5);
pub const SIX: Self = Self(6);
pub const SEVEN: Self = Self(7);
pub const EIGHT: Self = Self(8);
pub const NINE: Self = Self(9);
pub const TEN: Self = Self(10);
pub const ELEVEN: Self = Self(11);
pub const TWELVE: Self = Self(12);
/// All of the steps in a [`ColorScale`]. /// All of the steps in a [`ColorScale`].
pub const ALL: [ColorScaleStep; 12] = [ pub const ALL: [ColorScaleStep; 12] = [
Self::ONE, Self::ONE,
@@ -37,6 +24,18 @@ impl ColorScaleStep {
Self::ELEVEN, Self::ELEVEN,
Self::TWELVE, Self::TWELVE,
]; ];
pub const EIGHT: Self = Self(8);
pub const ELEVEN: Self = Self(11);
pub const FIVE: Self = Self(5);
pub const FOUR: Self = Self(4);
pub const NINE: Self = Self(9);
pub const ONE: Self = Self(1);
pub const SEVEN: Self = Self(7);
pub const SIX: Self = Self(6);
pub const TEN: Self = Self(10);
pub const THREE: Self = Self(3);
pub const TWELVE: Self = Self(12);
pub const TWO: Self = Self(2);
} }
/// A scale of colors for a given [`ColorScaleSet`]. /// A scale of colors for a given [`ColorScaleSet`].
@@ -191,8 +190,8 @@ pub struct ColorScales {
} }
impl IntoIterator for ColorScales { impl IntoIterator for ColorScales {
type Item = ColorScaleSet;
type IntoIter = std::vec::IntoIter<Self::Item>; type IntoIter = std::vec::IntoIter<Self::Item>;
type Item = ColorScaleSet;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
vec![ vec![

View File

@@ -1,6 +1,7 @@
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, prelude::FluentBuilder, px, rems, AbsoluteLength, App, Hsla, ImageSource, Img, div, img, px, rems, AbsoluteLength, App, Hsla, ImageSource, Img, IntoElement, ParentElement,
IntoElement, ParentElement, RenderOnce, Styled, StyledImage, Window, RenderOnce, Styled, StyledImage, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;

View File

@@ -1,13 +1,14 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, relative, AnyElement, App, ClickEvent, Div, ElementId, Hsla, div, relative, AnyElement, App, ClickEvent, Div, ElementId, Hsla, InteractiveElement,
InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString,
StatefulInteractiveElement as _, Styled, Window, StatefulInteractiveElement as _, Styled, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::{ use crate::indicator::Indicator;
indicator::Indicator, tooltip::Tooltip, Disableable, Icon, Selectable, Sizable, Size, StyledExt, use crate::tooltip::Tooltip;
}; use crate::{Disableable, Icon, Selectable, Sizable, Size, StyledExt};
pub enum ButtonRounded { pub enum ButtonRounded {
Normal, Normal,

View File

@@ -1,7 +1,7 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, relative, svg, App, ElementId, InteractiveElement, div, relative, svg, App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, SharedString, StatefulInteractiveElement as _, Styled as _, Window,
Styled as _, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;

View File

@@ -1,11 +1,15 @@
use crate::popup_menu::PopupMenu; use std::cell::RefCell;
use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
anchored, deferred, div, prelude::FluentBuilder, px, relative, AnyElement, App, Context, anchored, deferred, div, px, relative, AnyElement, App, Context, Corner, DismissEvent,
Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, Focusable, FocusableWrapper, DispatchPhase, Element, ElementId, Entity, Focusable, FocusableWrapper, GlobalElementId,
GlobalElementId, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
Pixels, Point, Position, Size, Stateful, Style, Window, Position, Size, Stateful, Style, Window,
}; };
use std::{cell::RefCell, rc::Rc};
use crate::popup_menu::PopupMenu;
pub trait ContextMenuExt: ParentElement + Sized { pub trait ContextMenuExt: ParentElement + Sized {
fn context_menu( fn context_menu(
@@ -92,8 +96,8 @@ impl Default for ContextMenuState {
} }
impl Element for ContextMenu { impl Element for ContextMenu {
type RequestLayoutState = ContextMenuState;
type PrepaintState = (); type PrepaintState = ();
type RequestLayoutState = ContextMenuState;
fn id(&self) -> Option<ElementId> { fn id(&self) -> Option<ElementId> {
Some(self.id.clone()) Some(self.id.clone())

View File

@@ -1,6 +1,6 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, Axis, Div, Hsla, IntoElement, ParentElement, RenderOnce, div, px, Axis, Div, Hsla, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
SharedString, Styled,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;

View File

@@ -1,19 +1,19 @@
use std::sync::Arc; use std::sync::Arc;
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, App, AppContext, Axis, Context, Element, Entity, div, px, App, AppContext, Axis, Context, Element, Entity, InteractiveElement as _, IntoElement,
InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
Point, Render, StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window, StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use theme::ActiveTheme; use theme::ActiveTheme;
use super::{DockArea, DockItem}; use super::{DockArea, DockItem};
use crate::{ use crate::dock_area::panel::PanelView;
dock_area::{panel::PanelView, tab_panel::TabPanel}, use crate::dock_area::tab_panel::TabPanel;
resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE}, use crate::resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE};
AxisExt as _, StyledExt, use crate::{AxisExt as _, StyledExt};
};
#[derive(Clone, Render)] #[derive(Clone, Render)]
struct ResizePanel; struct ResizePanel;
@@ -396,8 +396,8 @@ impl IntoElement for DockElement {
} }
impl Element for DockElement { impl Element for DockElement {
type RequestLayoutState = ();
type PrepaintState = (); type PrepaintState = ();
type RequestLayoutState = ();
fn id(&self) -> Option<gpui::ElementId> { fn id(&self) -> Option<gpui::ElementId> {
None None

View File

@@ -1,16 +1,17 @@
use crate::dock_area::{
dock::{Dock, DockPlacement},
panel::{Panel, PanelEvent, PanelStyle, PanelView},
stack_panel::StackPanel,
tab_panel::TabPanel,
};
use gpui::{
actions, canvas, div, prelude::FluentBuilder, px, AnyElement, AnyView, App, AppContext, Axis,
Bounds, Context, Edges, Entity, EntityId, EventEmitter, InteractiveElement as _, IntoElement,
ParentElement as _, Pixels, Render, Styled, Subscription, WeakEntity, Window,
};
use std::sync::Arc; use std::sync::Arc;
use gpui::prelude::FluentBuilder;
use gpui::{
actions, canvas, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges,
Entity, EntityId, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _,
Pixels, Render, Styled, Subscription, WeakEntity, Window,
};
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;
pub mod dock; pub mod dock;
pub mod panel; pub mod panel;
pub mod stack_panel; pub mod stack_panel;

View File

@@ -1,9 +1,11 @@
use crate::{button::Button, popup_menu::PopupMenu};
use gpui::{ use gpui::{
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Render, AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Render,
SharedString, Window, SharedString, Window,
}; };
use crate::button::Button;
use crate::popup_menu::PopupMenu;
pub enum PanelEvent { pub enum PanelEvent {
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
@@ -118,6 +120,7 @@ impl<T: Panel> PanelView for Entity<T> {
this.set_active(active, cx); this.set_active(active, cx);
}) })
} }
fn set_zoomed(&self, zoomed: bool, cx: &mut App) { fn set_zoomed(&self, zoomed: bool, cx: &mut App) {
self.update(cx, |this, cx| { self.update(cx, |this, cx| {
this.set_zoomed(zoomed, cx); this.set_zoomed(zoomed, cx);

View File

@@ -1,23 +1,21 @@
use super::{DockArea, PanelEvent}; use std::sync::Arc;
use crate::{
dock_area::{ use gpui::prelude::FluentBuilder;
panel::{Panel, PanelView},
tab_panel::TabPanel,
},
h_flex,
resizable::{
h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent,
ResizablePanelGroup,
},
AxisExt as _, Placement,
};
use gpui::{ use gpui::{
prelude::FluentBuilder, App, AppContext, Axis, Context, DismissEvent, Entity, EventEmitter, App, AppContext, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, SharedString, Styled, IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Subscription, WeakEntity,
Subscription, WeakEntity, Window, Window,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use std::sync::Arc;
use super::{DockArea, PanelEvent};
use crate::dock_area::panel::{Panel, PanelView};
use crate::dock_area::tab_panel::TabPanel;
use crate::resizable::{
h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent,
ResizablePanelGroup,
};
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

@@ -1,24 +1,24 @@
use gpui::{
div, prelude::FluentBuilder, px, rems, App, AppContext, Context, Corner, DefiniteLength,
DismissEvent, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, Render, ScrollHandle,
SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
};
use std::sync::Arc; use std::sync::Arc;
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent,
Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement,
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
StatefulInteractiveElement, Styled, WeakEntity, Window,
};
use theme::ActiveTheme; use theme::ActiveTheme;
use super::{ use super::panel::PanelView;
panel::PanelView, stack_panel::StackPanel, ClosePanel, DockArea, PanelEvent, PanelStyle, use super::stack_panel::StackPanel;
ToggleZoom, use super::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
}; use crate::button::{Button, ButtonVariants as _};
use crate::{ use crate::dock_area::dock::DockPlacement;
button::{Button, ButtonVariants as _}, use crate::dock_area::panel::Panel;
dock_area::{dock::DockPlacement, panel::Panel}, use crate::popup_menu::{PopupMenu, PopupMenuExt};
h_flex, use crate::tab::tab_bar::TabBar;
popup_menu::{PopupMenu, PopupMenuExt}, use crate::tab::Tab;
tab::{tab_bar::TabBar, Tab}, use crate::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt,
};
#[derive(Clone)] #[derive(Clone)]
struct TabState { struct TabState {

View File

@@ -1,19 +1,16 @@
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
anchored, canvas, deferred, div, prelude::FluentBuilder, px, rems, AnyElement, App, AppContext, anchored, canvas, deferred, div, px, rems, AnyElement, App, AppContext, Bounds, ClickEvent,
Bounds, ClickEvent, Context, DismissEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Context, DismissEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable,
Focusable, InteractiveElement, IntoElement, KeyBinding, Length, ParentElement, Pixels, Render, InteractiveElement, IntoElement, KeyBinding, Length, ParentElement, Pixels, Render, RenderOnce,
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window,
Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::{ use crate::actions::{Cancel, Confirm, SelectNext, SelectPrev};
actions::{Cancel, Confirm, SelectNext, SelectPrev}, use crate::input::clear_button::clear_button;
h_flex, use crate::list::{List, ListDelegate, ListItem};
input::clear_button::clear_button, use crate::{h_flex, v_flex, Disableable as _, Icon, IconName, Sizable, Size, StyleSized};
list::{List, ListDelegate, ListItem},
v_flex, Disableable as _, Icon, IconName, Sizable, Size, StyleSized,
};
#[derive(Clone)] #[derive(Clone)]
pub enum ListEvent { pub enum ListEvent {

View File

@@ -1,19 +1,18 @@
use std::rc::Rc; use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Corner, Element, div, impl_internal_actions, px, App, AppContext, Corner, Element, InteractiveElement,
InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
StatefulInteractiveElement, Styled, WeakEntity, Window, WeakEntity, Window,
}; };
use serde::Deserialize; use serde::Deserialize;
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::{ use crate::button::{Button, ButtonVariants};
button::{Button, ButtonVariants}, use crate::input::InputState;
input::InputState, use crate::popover::{Popover, PopoverContent};
popover::{Popover, PopoverContent}, use crate::Icon;
Icon,
};
#[derive(PartialEq, Clone, Debug, Deserialize)] #[derive(PartialEq, Clone, Debug, Deserialize)]
pub struct EmitEmoji(pub SharedString); pub struct EmitEmoji(pub SharedString);

View File

@@ -1,7 +1,5 @@
use std::{ use std::fmt::Debug;
fmt::Debug, use std::time::{Duration, Instant};
time::{Duration, Instant},
};
pub trait HistoryItem: Clone { pub trait HistoryItem: Clone {
fn version(&self) -> usize; fn version(&self) -> usize;
@@ -156,6 +154,7 @@ mod tests {
fn version(&self) -> usize { fn version(&self) -> usize {
self.version self.version
} }
fn set_version(&mut self, version: usize) { fn set_version(&mut self, version: usize) {
self.version = version; self.version = version;
} }

View File

@@ -1,7 +1,7 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
prelude::FluentBuilder as _, svg, AnyElement, App, AppContext, Entity, Hsla, IntoElement, svg, AnyElement, App, AppContext, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
Radians, Render, RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation, SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -40,6 +40,7 @@ pub enum IconName {
Inbox, Inbox,
Info, Info,
Loader, Loader,
Logout,
Moon, Moon,
PanelBottom, PanelBottom,
PanelBottomOpen, PanelBottomOpen,
@@ -108,6 +109,7 @@ impl IconName {
Self::Inbox => "icons/inbox.svg", Self::Inbox => "icons/inbox.svg",
Self::Info => "icons/info.svg", Self::Info => "icons/info.svg",
Self::Loader => "icons/loader.svg", Self::Loader => "icons/loader.svg",
Self::Logout => "icons/logout.svg",
Self::Moon => "icons/moon.svg", Self::Moon => "icons/moon.svg",
Self::PanelBottom => "icons/panel-bottom.svg", Self::PanelBottom => "icons/panel-bottom.svg",
Self::PanelBottomOpen => "icons/panel-bottom-open.svg", Self::PanelBottomOpen => "icons/panel-bottom-open.svg",

View File

@@ -1,10 +1,13 @@
use crate::{Icon, IconName, Sizable, Size};
use gpui::{
div, ease_in_out, percentage, prelude::FluentBuilder as _, Animation, AnimationExt as _, App,
Hsla, IntoElement, ParentElement, RenderOnce, Styled as _, Transformation, Window,
};
use std::time::Duration; use std::time::Duration;
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, ease_in_out, percentage, Animation, AnimationExt as _, App, Hsla, IntoElement,
ParentElement, RenderOnce, Styled as _, Transformation, Window,
};
use crate::{Icon, IconName, Sizable, Size};
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Indicator { pub struct Indicator {
size: Size, size: Size,

View File

@@ -1,4 +1,5 @@
use std::{fmt::Debug, ops::Range}; use std::fmt::Debug;
use std::ops::Range;
use crate::history::HistoryItem; use crate::history::HistoryItem;

View File

@@ -1,10 +1,8 @@
use gpui::{App, Styled}; use gpui::{App, Styled};
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::{ use crate::button::{Button, ButtonVariants as _};
button::{Button, ButtonVariants as _}, use crate::{Icon, IconName, Sizable as _};
Icon, IconName, Sizable as _,
};
#[inline] #[inline]
pub(crate) fn clear_button(cx: &App) -> Button { pub(crate) fn clear_button(cx: &App) -> Button {

View File

@@ -348,8 +348,8 @@ fn print_points_as_svg_path(line_corners: &Vec<Corners<Point<Pixels>>>, points:
} }
impl Element for TextElement { impl Element for TextElement {
type RequestLayoutState = ();
type PrepaintState = PrepaintState; type PrepaintState = PrepaintState;
type RequestLayoutState = ();
fn id(&self) -> Option<ElementId> { fn id(&self) -> Option<ElementId> {
None None

View File

@@ -1,25 +1,28 @@
use std::cell::Cell;
use std::ops::Range;
use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
actions, div, impl_internal_actions, point, px, App, AppContext, Bounds, ClipboardItem,
Context, DefiniteLength, Entity, EntityInputHandler, EventEmitter, FocusHandle, Focusable,
InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, ScrollHandle,
ScrollWheelEvent, SharedString, Styled as _, Subscription, UTF16Selection, Window, WrappedLine,
};
use serde::Deserialize; use serde::Deserialize;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{cell::Cell, ops::Range, rc::Rc};
use unicode_segmentation::*; use unicode_segmentation::*;
use gpui::{
actions, div, impl_internal_actions, point, prelude::FluentBuilder as _, px, App, AppContext,
Bounds, ClipboardItem, Context, DefiniteLength, Entity, EntityInputHandler, EventEmitter,
FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point,
Render, ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription,
UTF16Selection, Window, WrappedLine,
};
// TODO: // TODO:
// - Move cursor to skip line eof empty chars. // - Move cursor to skip line eof empty chars.
use super::{ use super::{
blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern, blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern,
text_wrapper::TextWrapper, text_wrapper::TextWrapper,
}; };
use crate::{history::History, scroll::ScrollbarState, Root}; use crate::history::History;
use crate::scroll::ScrollbarState;
use crate::Root;
#[derive(Clone, PartialEq, Eq, Deserialize)] #[derive(Clone, PartialEq, Eq, Deserialize)]
pub struct Enter { pub struct Enter {

View File

@@ -1,19 +1,16 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, relative, AnyElement, App, DefiniteLength, Entity, div, px, relative, AnyElement, App, DefiniteLength, Entity, InteractiveElement as _,
InteractiveElement as _, IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, Styled, Window,
Styled, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use super::InputState; use super::InputState;
use crate::{ use crate::button::{Button, ButtonVariants as _};
button::{Button, ButtonVariants as _}, use crate::indicator::Indicator;
h_flex, use crate::input::clear_button::clear_button;
indicator::Indicator, use crate::scroll::{Scrollbar, ScrollbarAxis};
input::clear_button::clear_button, use crate::{h_flex, IconName, Sizable, Size, StyleSized};
scroll::{Scrollbar, ScrollbarAxis},
IconName, Sizable, Size, StyleSized,
};
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct TextInput { pub struct TextInput {

View File

@@ -1,21 +1,22 @@
use std::{cell::Cell, rc::Rc, time::Duration}; use std::cell::Cell;
use std::rc::Rc;
use std::time::Duration;
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, prelude::FluentBuilder, uniform_list, AnyElement, AppContext, Entity, FocusHandle, div, px, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
Focusable, InteractiveElement, IntoElement, KeyBinding, Length, ListSizingBehavior, Focusable, InteractiveElement, IntoElement, KeyBinding, Length, ListSizingBehavior,
MouseButton, ParentElement, Render, Styled, Task, UniformListScrollHandle, Window, MouseButton, MouseDownEvent, ParentElement, Render, ScrollStrategy, Styled, Subscription, Task,
UniformListScrollHandle, Window,
}; };
use gpui::{px, App, Context, EventEmitter, MouseDownEvent, ScrollStrategy, Subscription};
use smol::Timer; use smol::Timer;
use theme::ActiveTheme; use theme::ActiveTheme;
use super::loading::Loading; use super::loading::Loading;
use crate::{ use crate::actions::{Cancel, Confirm, SelectNext, SelectPrev};
actions::{Cancel, Confirm, SelectNext, SelectPrev}, use crate::input::{InputEvent, InputState, TextInput};
input::{InputEvent, InputState, TextInput}, use crate::scroll::{Scrollbar, ScrollbarState};
scroll::{Scrollbar, ScrollbarState}, use crate::{v_flex, Icon, IconName, Sizable as _, Size};
v_flex, Icon, IconName, Sizable as _, Size,
};
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
let context: Option<&str> = Some("List"); let context: Option<&str> = Some("List");

View File

@@ -1,7 +1,8 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, AnyElement, App, ClickEvent, Div, ElementId, div, AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement, MouseButton,
InteractiveElement, IntoElement, MouseButton, MouseMoveEvent, ParentElement, RenderOnce, MouseMoveEvent, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement as _, Styled,
Stateful, StatefulInteractiveElement as _, Styled, Window, Window,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use theme::ActiveTheme; use theme::ActiveTheme;

View File

@@ -1,7 +1,8 @@
use gpui::{IntoElement, ParentElement as _, RenderOnce, Styled}; use gpui::{IntoElement, ParentElement as _, RenderOnce, Styled};
use super::ListItem; use super::ListItem;
use crate::{skeleton::Skeleton, v_flex}; use crate::skeleton::Skeleton;
use crate::v_flex;
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Loading; pub struct Loading;

View File

@@ -1,18 +1,17 @@
use std::{rc::Rc, time::Duration}; use std::rc::Rc;
use std::time::Duration;
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
actions, anchored, div, point, prelude::FluentBuilder, px, relative, Animation, actions, anchored, div, point, px, relative, Animation, AnimationExt as _, AnyElement, App,
AnimationExt as _, AnyElement, App, Bounds, ClickEvent, Div, FocusHandle, InteractiveElement, Bounds, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding, MouseButton,
IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, ParentElement, Pixels, Point, RenderOnce, SharedString, Styled, Window,
Styled, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::{ use crate::animation::cubic_bezier;
animation::cubic_bezier, use crate::button::{Button, ButtonCustomVariant, ButtonVariants as _};
button::{Button, ButtonCustomVariant, ButtonVariants as _}, use crate::{v_flex, ContextModal, IconName, StyledExt};
v_flex, ContextModal, IconName, StyledExt,
};
actions!(modal, [Escape]); actions!(modal, [Escape]);

View File

@@ -1,24 +1,21 @@
use std::{ use std::any::TypeId;
any::TypeId, use std::collections::{HashMap, VecDeque};
collections::{HashMap, VecDeque}, use std::sync::Arc;
sync::Arc, use std::time::Duration;
time::Duration,
};
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
blue, div, green, prelude::FluentBuilder, px, red, yellow, Animation, AnimationExt, App, blue, div, green, px, red, yellow, Animation, AnimationExt, App, AppContext, ClickEvent,
AppContext, ClickEvent, Context, DismissEvent, ElementId, Entity, EventEmitter, Context, DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString, ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
StatefulInteractiveElement, Styled, Subscription, Window, Window,
}; };
use smol::Timer; use smol::Timer;
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::{ use crate::animation::cubic_bezier;
animation::cubic_bezier, use crate::button::{Button, ButtonVariants as _};
button::{Button, ButtonVariants as _}, use crate::{h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt};
h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt,
};
pub enum NotificationType { pub enum NotificationType {
Info, Info,

View File

@@ -1,12 +1,13 @@
use std::{cell::RefCell, rc::Rc}; use std::cell::RefCell;
use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
actions, anchored, deferred, div, prelude::FluentBuilder as _, px, AnyElement, App, Bounds, actions, anchored, deferred, div, px, AnyElement, App, Bounds, Context, Corner, DismissEvent,
Context, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, EventEmitter, DispatchPhase, Element, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
FocusHandle, Focusable, GlobalElementId, Hitbox, HitboxBehavior, InteractiveElement as _, GlobalElementId, Hitbox, HitboxBehavior, InteractiveElement as _, IntoElement, KeyBinding,
IntoElement, KeyBinding, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render,
Pixels, Point, Render, ScrollHandle, StatefulInteractiveElement, Style, StyleRefinement, ScrollHandle, StatefulInteractiveElement, Style, StyleRefinement, Styled, Window,
Styled, Window,
}; };
use crate::{Selectable, StyledExt as _}; use crate::{Selectable, StyledExt as _};
@@ -146,6 +147,7 @@ where
self.trigger_style = Some(style); self.trigger_style = Some(style);
self self
} }
/// Set the content of the popover. /// Set the content of the popover.
/// ///
/// The `content` is a closure that returns an `AnyElement`. /// The `content` is a closure that returns an `AnyElement`.
@@ -244,8 +246,8 @@ pub struct PrepaintState {
} }
impl<M: ManagedView> Element for Popover<M> { impl<M: ManagedView> Element for Popover<M> {
type RequestLayoutState = PopoverElementState<M>;
type PrepaintState = PrepaintState; type PrepaintState = PrepaintState;
type RequestLayoutState = PopoverElementState<M>;
fn id(&self) -> Option<ElementId> { fn id(&self) -> Option<ElementId> {
Some(self.id.clone()) Some(self.id.clone())

View File

@@ -1,22 +1,21 @@
use std::{cell::Cell, ops::Deref, rc::Rc}; use std::cell::Cell;
use std::ops::Deref;
use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
actions, anchored, canvas, div, prelude::FluentBuilder, px, rems, Action, AnyElement, App, actions, anchored, canvas, div, px, rems, Action, AnyElement, App, AppContext, Bounds, Context,
AppContext, Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
Focusable, InteractiveElement, IntoElement, KeyBinding, Keystroke, ParentElement, Pixels, IntoElement, KeyBinding, Keystroke, ParentElement, Pixels, Render, ScrollHandle, SharedString,
Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Subscription, StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window,
WeakEntity, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::{ use crate::button::Button;
button::Button, use crate::list::ListItem;
h_flex, use crate::popover::Popover;
list::ListItem, use crate::scroll::{Scrollbar, ScrollbarState};
popover::Popover, use crate::{h_flex, v_flex, Icon, IconName, Selectable, Sizable as _, StyledExt};
scroll::{Scrollbar, ScrollbarState},
v_flex, Icon, IconName, Selectable, Sizable as _, StyledExt,
};
actions!(menu, [Confirm, Dismiss, SelectNext, SelectPrev]); actions!(menu, [Confirm, Dismiss, SelectNext, SelectPrev]);
@@ -531,9 +530,7 @@ impl Render for PopupMenu {
.on_action(cx.listener(Self::select_prev)) .on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::dismiss)) .on_action(cx.listener(Self::dismiss))
.on_mouse_down_out( .on_mouse_down_out(cx.listener(|this, _, window, cx| this.dismiss(&Dismiss, window, cx)))
cx.listener(|this, _, window, cx| this.dismiss(&Dismiss, window, cx)),
)
.popover_style(cx) .popover_style(cx)
.relative() .relative()
.p_1() .p_1()
@@ -564,9 +561,7 @@ impl Render for PopupMenu {
.iter_mut() .iter_mut()
.enumerate() .enumerate()
// Skip last separator // Skip last separator
.filter(|(ix, item)| { .filter(|(ix, item)| !(*ix == items_count - 1 && item.is_separator()))
!(*ix == items_count - 1 && item.is_separator())
})
.map(|(ix, item)| { .map(|(ix, item)| {
let this = ListItem::new(("menu-item", ix)) let this = ListItem::new(("menu-item", ix))
.relative() .relative()
@@ -575,68 +570,51 @@ impl Render for PopupMenu {
.px_2() .px_2()
.rounded_md() .rounded_md()
.text_sm() .text_sm()
.on_mouse_enter(cx.listener( .on_mouse_enter(cx.listener(move |this, _, _window, cx| {
move |this, _, _window, cx| { this.hovered_menu_ix = Some(ix);
this.hovered_menu_ix = Some(ix); cx.notify();
cx.notify(); }));
},
));
match item { match item {
PopupMenuItem::Separator => { PopupMenuItem::Separator => this.h_auto().p_0().disabled(true).child(
this.h_auto().p_0().disabled(true).child( div()
div() .rounded_none()
.rounded_none() .h(px(1.))
.h(px(1.)) .mx_neg_1()
.mx_neg_1() .my_0p5()
.my_0p5() .bg(cx.theme().border_disabled),
.bg(cx.theme().border_disabled), ),
)
}
PopupMenuItem::ElementItem { render, .. } => this PopupMenuItem::ElementItem { render, .. } => this
.on_click(cx.listener( .on_click(
move |this, _, window, cx| { cx.listener(move |this, _, window, cx| {
this.on_click(ix, window, cx) this.on_click(ix, window, cx)
}, }),
)) )
.child( .child(
h_flex() h_flex()
.min_h(ITEM_HEIGHT) .min_h(ITEM_HEIGHT)
.items_center() .items_center()
.gap_x_1() .gap_x_1()
.children(Self::render_icon( .children(Self::render_icon(has_icon, None, window, cx))
has_icon, None, window, cx,
))
.child((render)(window, cx)), .child((render)(window, cx)),
), ),
PopupMenuItem::Item { PopupMenuItem::Item {
icon, icon, label, action, ..
label,
action,
..
} => { } => {
let action = action let action = action.as_ref().map(|action| action.boxed_clone());
.as_ref() let key = Self::render_keybinding(action, window, cx);
.map(|action| action.boxed_clone());
let key =
Self::render_keybinding(action, window, cx);
this.on_click(cx.listener( this.on_click(
move |this, _, window, cx| { cx.listener(move |this, _, window, cx| {
this.on_click(ix, window, cx) this.on_click(ix, window, cx)
}, }),
)) )
.child( .child(
h_flex() h_flex()
.h(ITEM_HEIGHT) .h(ITEM_HEIGHT)
.items_center() .items_center()
.gap_x_1p5() .gap_x_1p5()
.children(Self::render_icon( .children(Self::render_icon(has_icon, icon.clone(), window, cx))
has_icon,
icon.clone(),
window,
cx,
))
.child( .child(
h_flex() h_flex()
.flex_1() .flex_1()
@@ -649,9 +627,7 @@ impl Render for PopupMenu {
) )
} }
PopupMenuItem::Submenu { icon, label, menu } => this PopupMenuItem::Submenu { icon, label, menu } => this
.when(self.hovered_menu_ix == Some(ix), |this| { .when(self.hovered_menu_ix == Some(ix), |this| this.selected(true))
this.selected(true)
})
.child( .child(
h_flex() h_flex()
.items_start() .items_start()
@@ -673,57 +649,44 @@ impl Render for PopupMenu {
.items_center() .items_center()
.justify_between() .justify_between()
.child(label.clone()) .child(label.clone())
.child( .child(IconName::CaretRight),
IconName::CaretRight,
),
), ),
) )
.when_some( .when_some(self.hovered_menu_ix, |this, hovered_ix| {
self.hovered_menu_ix, let (anchor, left) = if window.bounds().size.width
|this, hovered_ix| { - bounds.origin.x
let (anchor, left) = < max_width
if window.bounds().size.width {
- bounds.origin.x (Corner::TopRight, -px(15.))
< max_width } else {
{ (Corner::TopLeft, bounds.size.width - px(10.))
(Corner::TopRight, -px(15.)) };
} else {
(
Corner::TopLeft,
bounds.size.width
- px(10.),
)
};
let top = if bounds.origin.y let top = if bounds.origin.y + bounds.size.height
+ bounds.size.height > window.bounds().size.height
> window.bounds().size.height {
{ px(32.)
px(32.) } else {
} else { -px(10.)
-px(10.) };
};
if hovered_ix == ix { if hovered_ix == ix {
this.child( this.child(
anchored() anchored()
.anchor(anchor) .anchor(anchor)
.child( .child(
div() div()
.occlude() .occlude()
.top(top) .top(top)
.left(left) .left(left)
.child(menu.clone()), .child(menu.clone()),
) )
.snap_to_window_with_margin( .snap_to_window_with_margin(Edges::all(px(8.))),
Edges::all(px(8.)), )
), } else {
) this
} else { }
this }),
}
},
),
), ),
} }
}), }),

View File

@@ -1,11 +1,13 @@
use gpui::{
canvas, div, prelude::FluentBuilder, px, relative, Along, AnyElement, AnyView, App, AppContext,
Axis, Bounds, Context, Element, Entity, EntityId, EventEmitter, IntoElement, IsZero,
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Render, StatefulInteractiveElement as _,
Style, Styled, WeakEntity, Window,
};
use std::rc::Rc; use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
canvas, div, px, relative, Along, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context,
Element, Entity, EntityId, EventEmitter, IntoElement, IsZero, MouseMoveEvent, MouseUpEvent,
ParentElement, Pixels, Render, StatefulInteractiveElement as _, Style, Styled, WeakEntity,
Window,
};
use super::resize_handle; use super::resize_handle;
use crate::{h_flex, v_flex, AxisExt}; use crate::{h_flex, v_flex, AxisExt};
@@ -470,8 +472,8 @@ impl IntoElement for ResizePanelGroupElement {
} }
impl Element for ResizePanelGroupElement { impl Element for ResizePanelGroupElement {
type RequestLayoutState = ();
type PrepaintState = (); type PrepaintState = ();
type RequestLayoutState = ();
fn id(&self) -> Option<gpui::ElementId> { fn id(&self) -> Option<gpui::ElementId> {
None None

View File

@@ -1,7 +1,7 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, App, Axis, Div, ElementId, InteractiveElement, div, px, App, Axis, Div, ElementId, InteractiveElement, IntoElement, ParentElement as _,
IntoElement, ParentElement as _, Pixels, RenderOnce, Stateful, StatefulInteractiveElement, Pixels, RenderOnce, Stateful, StatefulInteractiveElement, Styled as _, Window,
Styled as _, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;

View File

@@ -6,12 +6,10 @@ use gpui::{
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::{ use crate::input::InputState;
input::InputState, use crate::modal::Modal;
modal::Modal, use crate::notification::{Notification, NotificationList};
notification::{Notification, NotificationList}, use crate::window_border;
window_border,
};
/// Extension trait for [`WindowContext`] and [`ViewContext`] to add drawer functionality. /// Extension trait for [`WindowContext`] and [`ViewContext`] to add drawer functionality.
pub trait ContextModal: Sized { pub trait ContextModal: Sized {

View File

@@ -1,9 +1,11 @@
use std::cell::Cell;
use std::rc::Rc;
use gpui::{ use gpui::{
canvas, div, relative, AnyElement, App, Div, Element, ElementId, EntityId, GlobalElementId, canvas, div, relative, AnyElement, App, Div, Element, ElementId, EntityId, GlobalElementId,
InteractiveElement, IntoElement, ParentElement, Pixels, Position, ScrollHandle, SharedString, InteractiveElement, IntoElement, ParentElement, Pixels, Position, ScrollHandle, SharedString,
Size, Stateful, StatefulInteractiveElement, Style, StyleRefinement, Styled, Window, Size, Stateful, StatefulInteractiveElement, Style, StyleRefinement, Styled, Window,
}; };
use std::{cell::Cell, rc::Rc};
use super::{Scrollbar, ScrollbarAxis, ScrollbarState}; use super::{Scrollbar, ScrollbarAxis, ScrollbarState};
@@ -143,8 +145,8 @@ impl<E> Element for Scrollable<E>
where where
E: Element, E: Element,
{ {
type RequestLayoutState = AnyElement;
type PrepaintState = ScrollViewState; type PrepaintState = ScrollViewState;
type RequestLayoutState = AnyElement;
fn id(&self) -> Option<gpui::ElementId> { fn id(&self) -> Option<gpui::ElementId> {
Some(self.id.clone()) Some(self.id.clone())

View File

@@ -46,8 +46,8 @@ impl IntoElement for ScrollableMask {
} }
impl Element for ScrollableMask { impl Element for ScrollableMask {
type RequestLayoutState = ();
type PrepaintState = Hitbox; type PrepaintState = Hitbox;
type RequestLayoutState = ();
fn id(&self) -> Option<ElementId> { fn id(&self) -> Option<ElementId> {
None None

View File

@@ -1,8 +1,6 @@
use std::{ use std::cell::Cell;
cell::Cell, use std::rc::Rc;
rc::Rc, use std::time::{Duration, Instant};
time::{Duration, Instant},
};
use gpui::{ use gpui::{
fill, point, px, relative, App, BorderStyle, Bounds, ContentMask, CursorStyle, Edges, Element, fill, point, px, relative, App, BorderStyle, Bounds, ContentMask, CursorStyle, Edges, Element,
@@ -389,9 +387,8 @@ pub struct AxisPrepaintState {
} }
impl Element for Scrollbar { impl Element for Scrollbar {
type RequestLayoutState = ();
type PrepaintState = PrepaintState; type PrepaintState = PrepaintState;
type RequestLayoutState = ();
fn id(&self) -> Option<gpui::ElementId> { fn id(&self) -> Option<gpui::ElementId> {
None None
@@ -631,7 +628,7 @@ impl Element for Scrollbar {
let margin_end = state.margin_end; let margin_end = state.margin_end;
let is_vertical = axis.is_vertical(); let is_vertical = axis.is_vertical();
window.set_cursor_style(CursorStyle::default(), Some(&state.bar_hitbox)); window.set_cursor_style(CursorStyle::default(), &state.bar_hitbox);
window.paint_layer(hitbox_bounds, |cx| { window.paint_layer(hitbox_bounds, |cx| {
cx.paint_quad(fill(state.bounds, state.bg)); cx.paint_quad(fill(state.bounds, state.bg));

View File

@@ -1,9 +1,12 @@
use std::{cell::RefCell, rc::Rc, time::Duration}; use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, AnyElement, App, Element, div, px, Animation, AnimationExt as _, AnyElement, App, Element, ElementId, GlobalElementId,
ElementId, GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, Styled as _,
SharedString, Styled as _, Window, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -87,9 +90,8 @@ pub struct SwitchState {
} }
impl Element for Switch { impl Element for Switch {
type RequestLayoutState = AnyElement;
type PrepaintState = (); type PrepaintState = ();
type RequestLayoutState = AnyElement;
fn id(&self) -> Option<ElementId> { fn id(&self) -> Option<ElementId> {
Some(self.id.clone()) Some(self.id.clone())

View File

@@ -1,6 +1,7 @@
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, prelude::FluentBuilder, px, AnyElement, App, Div, ElementId, InteractiveElement, div, px, AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
IntoElement, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Window, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;

View File

@@ -1,11 +1,12 @@
use crate::h_flex; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, AnyElement, App, Div, ElementId, InteractiveElement, div, px, AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
IntoElement, ParentElement, RenderOnce, ScrollHandle, StatefulInteractiveElement as _, Styled, RenderOnce, ScrollHandle, StatefulInteractiveElement as _, Styled, Window,
Window,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::h_flex;
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct TabBar { pub struct TabBar {
base: Div, base: Div,

View File

@@ -1,3 +1,7 @@
use std::collections::HashMap;
use std::ops::Range;
use std::sync::Arc;
use common::profile::RenderProfile; use common::profile::RenderProfile;
use gpui::{ use gpui::{
AnyElement, AnyView, App, ElementId, FontWeight, HighlightStyle, InteractiveText, IntoElement, AnyElement, AnyView, App, ElementId, FontWeight, HighlightStyle, InteractiveText, IntoElement,
@@ -7,7 +11,6 @@ use linkify::{LinkFinder, LinkKind};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::{collections::HashMap, ops::Range, sync::Arc};
use theme::ActiveTheme; use theme::ActiveTheme;
static NOSTR_URI_REGEX: Lazy<Regex> = static NOSTR_URI_REGEX: Lazy<Regex> =

View File

@@ -1,9 +1,10 @@
use std::rc::Rc; use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
black, div, prelude::FluentBuilder as _, px, relative, white, AnyElement, App, ClickEvent, Div, black, div, px, relative, white, AnyElement, App, ClickEvent, Div, Element, Hsla,
Element, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, Rgba,
RenderOnce, Rgba, Stateful, StatefulInteractiveElement as _, Style, Styled, Window, Stateful, StatefulInteractiveElement as _, Style, Styled, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -291,8 +292,8 @@ impl IntoElement for TitleBarElement {
} }
impl Element for TitleBarElement { impl Element for TitleBarElement {
type RequestLayoutState = ();
type PrepaintState = (); type PrepaintState = ();
type RequestLayoutState = ();
fn id(&self) -> Option<gpui::ElementId> { fn id(&self) -> Option<gpui::ElementId> {
None None

View File

@@ -1,7 +1,8 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
canvas, div, point, prelude::FluentBuilder as _, px, AnyElement, App, Bounds, CursorStyle, canvas, div, point, px, AnyElement, App, Bounds, CursorStyle, Decorations, Edges,
Decorations, Edges, HitboxBehavior, Hsla, InteractiveElement as _, IntoElement, MouseButton, HitboxBehavior, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels,
ParentElement, Pixels, Point, RenderOnce, ResizeEdge, Size, Styled as _, Window, Point, RenderOnce, ResizeEdge, Size, Styled as _, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -104,7 +105,7 @@ impl RenderOnce for WindowBorder {
CursorStyle::ResizeUpRightDownLeft CursorStyle::ResizeUpRightDownLeft
} }
}, },
Some(&hitbox), &hitbox,
); );
}, },
) )

9
rustfmt.toml Normal file
View File

@@ -0,0 +1,9 @@
tab_spaces = 4
newline_style = "Auto"
reorder_imports = true
reorder_modules = true
reorder_impl_items = true
indent_style = "Block"
normalize_comments = false
imports_granularity = "Module"
group_imports = "StdExternalCrate"