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:
189
Cargo.lock
generated
189
Cargo.lock
generated
@@ -2,21 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "account"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"common",
|
||||
"global",
|
||||
"gpui",
|
||||
"log",
|
||||
"nostr-sdk",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"ui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.2"
|
||||
@@ -417,7 +402,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"log",
|
||||
"nostr-sdk",
|
||||
"reqwest 0.12.18",
|
||||
"reqwest 0.12.19",
|
||||
"smol",
|
||||
"tempfile",
|
||||
]
|
||||
@@ -497,9 +482,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.7.3"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
|
||||
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||
|
||||
[[package]]
|
||||
name = "bech32"
|
||||
@@ -754,9 +739,9 @@ checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.17.0"
|
||||
version = "3.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
@@ -851,9 +836,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.24"
|
||||
version = "1.2.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
|
||||
checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
@@ -934,7 +919,6 @@ dependencies = [
|
||||
name = "chats"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"account",
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"common",
|
||||
@@ -1078,7 +1062,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collections"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b"
|
||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"rustc-hash 2.1.1",
|
||||
@@ -1156,7 +1140,6 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
name = "coop"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"account",
|
||||
"anyhow",
|
||||
"auto_update",
|
||||
"chats",
|
||||
@@ -1459,7 +1442,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "derive_refineable"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b"
|
||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1802,9 +1785,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.1"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
||||
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
@@ -2199,7 +2182,12 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
name = "global"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dirs 5.0.1",
|
||||
"futures",
|
||||
"log",
|
||||
"nostr-connect",
|
||||
"nostr-keyring",
|
||||
"nostr-sdk",
|
||||
"rustls",
|
||||
"smol",
|
||||
@@ -2276,7 +2264,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b"
|
||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"as-raw-xcb-connection",
|
||||
@@ -2313,6 +2301,7 @@ dependencies = [
|
||||
"image",
|
||||
"inventory",
|
||||
"itertools 0.14.0",
|
||||
"libc",
|
||||
"log",
|
||||
"lyon",
|
||||
"media",
|
||||
@@ -2368,7 +2357,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_macros"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b"
|
||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -2474,12 +2463,6 @@ dependencies = [
|
||||
"heed-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.1"
|
||||
@@ -2597,7 +2580,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "http_client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b"
|
||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -2614,7 +2597,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "http_client_tls"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b"
|
||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls-platform-verifier",
|
||||
@@ -2649,9 +2632,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.6"
|
||||
version = "0.27.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d"
|
||||
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
@@ -2683,9 +2666,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.13"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
|
||||
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
@@ -2704,7 +2687,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry 0.4.0",
|
||||
"windows-registry 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3064,6 +3047,20 @@ dependencies = [
|
||||
"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]]
|
||||
name = "khronos-egl"
|
||||
version = "6.0.0"
|
||||
@@ -3171,6 +3168,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@@ -3331,7 +3338,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "media"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b"
|
||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bindgen 0.71.1",
|
||||
@@ -3533,7 +3540,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
[[package]]
|
||||
name = "nostr"
|
||||
version = "0.42.1"
|
||||
source = "git+https://github.com/rust-nostr/nostr#08b421634cee639d50d0f592db42350e337d3bde"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64",
|
||||
@@ -3545,8 +3552,7 @@ dependencies = [
|
||||
"chacha20poly1305",
|
||||
"getrandom 0.2.16",
|
||||
"instant",
|
||||
"regex",
|
||||
"reqwest 0.12.18",
|
||||
"reqwest 0.12.19",
|
||||
"scrypt",
|
||||
"secp256k1",
|
||||
"serde",
|
||||
@@ -3558,7 +3564,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-connect"
|
||||
version = "0.42.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#08b421634cee639d50d0f592db42350e337d3bde"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"nostr",
|
||||
@@ -3570,7 +3576,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-database"
|
||||
version = "0.42.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#08b421634cee639d50d0f592db42350e337d3bde"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
|
||||
dependencies = [
|
||||
"flatbuffers",
|
||||
"lru",
|
||||
@@ -3578,10 +3584,19 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nostr-keyring"
|
||||
version = "0.42.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
|
||||
dependencies = [
|
||||
"keyring",
|
||||
"nostr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nostr-lmdb"
|
||||
version = "0.42.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#08b421634cee639d50d0f592db42350e337d3bde"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"heed",
|
||||
@@ -3594,7 +3609,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-relay-pool"
|
||||
version = "0.42.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#08b421634cee639d50d0f592db42350e337d3bde"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"async-wsocket",
|
||||
@@ -3610,7 +3625,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-sdk"
|
||||
version = "0.42.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#08b421634cee639d50d0f592db42350e337d3bde"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4096b9da00f18c3089f734b27ce1388616f3cb13"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"nostr",
|
||||
@@ -3745,11 +3760,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.16.0"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.9",
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -4234,7 +4249,7 @@ checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi 0.5.1",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix 1.0.7",
|
||||
"tracing",
|
||||
@@ -4295,9 +4310,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.32"
|
||||
version = "0.2.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6"
|
||||
checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.101",
|
||||
@@ -4615,9 +4630,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.29.2"
|
||||
version = "0.29.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f96bfbb7df43d34a2b7b8582fcbcb676ba02a763265cb90bc8aabfd62b57d64"
|
||||
checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"font-types",
|
||||
@@ -4646,7 +4661,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "refineable"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b"
|
||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
||||
dependencies = [
|
||||
"derive_refineable",
|
||||
"workspace-hack",
|
||||
@@ -4731,9 +4746,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.18"
|
||||
version = "0.12.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5"
|
||||
checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
@@ -4783,7 +4798,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "reqwest_client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b"
|
||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -5254,7 +5269,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
|
||||
[[package]]
|
||||
name = "semantic_version"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b"
|
||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -5346,9 +5361,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.8"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -5477,9 +5492,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.0"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "smol"
|
||||
@@ -5606,7 +5621,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
[[package]]
|
||||
name = "sum_tree"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b"
|
||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"log",
|
||||
@@ -6117,9 +6132,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.22"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@@ -6129,18 +6144,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.9"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.26"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -6152,9 +6167,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
@@ -6173,9 +6188,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.4"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e"
|
||||
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bytes",
|
||||
@@ -6214,9 +6229,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.28"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
||||
checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6225,9 +6240,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.33"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
|
||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
@@ -6521,7 +6536,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
[[package]]
|
||||
name = "util"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#a387bf5f54edce7558dec5c3804b03b51cbbfe9b"
|
||||
source = "git+https://github.com/zed-industries/zed#46773ebbd8c2da7c0239a52c4b4ce303111a6798"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-fs",
|
||||
@@ -7864,9 +7879,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.4.14"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
|
||||
checksum = "3e4a518c0ea2576f4da876349d7f67a7be489297cd77c2cf9e04c2e05fcd3974"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
@@ -16,7 +16,7 @@ gpui = { git = "https://github.com/zed-industries/zed" }
|
||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||
|
||||
# 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-connect = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr-keyring = { git = "https://github.com/rust-nostr/nostr" }
|
||||
|
||||
3
assets/icons/logout.svg
Normal file
3
assets/icons/logout.svg
Normal 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 |
4
assets/icons/settings.svg
Normal file
4
assets/icons/settings.svg
Normal 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 |
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,15 @@
|
||||
use std::{
|
||||
env::{self, consts::OS},
|
||||
ffi::OsString,
|
||||
path::PathBuf,
|
||||
};
|
||||
use std::env::consts::OS;
|
||||
use std::env::{self};
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Error};
|
||||
use global::get_client;
|
||||
use global::shared_state;
|
||||
use gpui::{App, AppContext, Context, Entity, Global, SemanticVersion, Task};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smol::{
|
||||
fs::{self, File},
|
||||
io::AsyncWriteExt,
|
||||
process::Command,
|
||||
};
|
||||
use smol::fs::{self, File};
|
||||
use smol::io::AsyncWriteExt;
|
||||
use smol::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
struct GlobalAutoUpdate(Entity<AutoUpdater>);
|
||||
@@ -129,10 +126,9 @@ impl AutoUpdater {
|
||||
self.set_status(AutoUpdateStatus::Downloading, cx);
|
||||
|
||||
let task: Task<Result<(TempDir, PathBuf), Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let ids = event.tags.event_ids().copied();
|
||||
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) {
|
||||
let tag = event.tags.find(TagKind::Url).context("url not found")?;
|
||||
|
||||
@@ -5,7 +5,6 @@ edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
account = { path = "../account" }
|
||||
common = { path = "../common" }
|
||||
global = { path = "../global" }
|
||||
|
||||
|
||||
@@ -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 common::room_hash;
|
||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||
use global::get_client;
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use global::shared_state;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window,
|
||||
};
|
||||
@@ -67,7 +68,7 @@ impl ChatRegistry {
|
||||
}
|
||||
|
||||
/// 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));
|
||||
}
|
||||
|
||||
@@ -160,14 +161,11 @@ impl ChatRegistry {
|
||||
/// 3. Determines each room's type based on message frequency and trust status
|
||||
/// 4. Creates Room entities for each unique room
|
||||
pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// If the user is not logged in, do nothing
|
||||
let Some(current_user) = Account::get_global(cx).profile_ref() else {
|
||||
let client = &shared_state().client;
|
||||
let Some(public_key) = shared_state().identity().map(|i| i.public_key()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let client = get_client();
|
||||
let public_key = current_user.public_key();
|
||||
|
||||
let task: Task<Result<BTreeSet<Room>, Error>> = cx.background_spawn(async move {
|
||||
// Get messages sent by the user
|
||||
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>) {
|
||||
let id = room_hash(&event);
|
||||
let author = event.pubkey;
|
||||
|
||||
let Some(profile) = Account::get_global(cx).profile.to_owned() else {
|
||||
let Some(public_key) = shared_state().identity().map(|i| i.public_key()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -301,7 +298,7 @@ impl ChatRegistry {
|
||||
this.created_at(event.created_at, cx);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::cell::RefCell;
|
||||
use std::iter::IntoIterator;
|
||||
use std::rc::Rc;
|
||||
|
||||
use chrono::{Local, TimeZone};
|
||||
use gpui::SharedString;
|
||||
use nostr_sdk::prelude::*;
|
||||
use std::{cell::RefCell, iter::IntoIterator, rc::Rc};
|
||||
|
||||
use crate::room::SendError;
|
||||
|
||||
|
||||
@@ -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 chrono::{Local, TimeZone};
|
||||
use common::{compare, profile::RenderProfile, room_hash};
|
||||
use global::{async_cache_profile, get_cache_profile, get_client, profiles};
|
||||
use common::profile::RenderProfile;
|
||||
use common::{compare, room_hash};
|
||||
use global::shared_state;
|
||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
use crate::{
|
||||
constants::{DAYS_IN_MONTH, HOURS_IN_DAY, MINUTES_IN_HOUR, NOW, SECONDS_IN_MINUTE},
|
||||
message::Message,
|
||||
};
|
||||
use crate::constants::{DAYS_IN_MONTH, HOURS_IN_DAY, MINUTES_IN_HOUR, NOW, SECONDS_IN_MINUTE};
|
||||
use crate::message::Message;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Incoming(pub Message);
|
||||
@@ -165,22 +164,21 @@ impl Room {
|
||||
/// # Returns
|
||||
///
|
||||
/// The Profile of the first member in the room
|
||||
pub fn first_member(&self, cx: &App) -> Profile {
|
||||
let account = Account::global(cx).read(cx);
|
||||
let Some(profile) = account.profile.clone() else {
|
||||
return get_cache_profile(&self.members[0]);
|
||||
pub fn first_member(&self, _cx: &App) -> Profile {
|
||||
let Some(account) = shared_state().identity() else {
|
||||
return shared_state().person(&self.members[0]);
|
||||
};
|
||||
|
||||
if let Some(public_key) = self
|
||||
.members
|
||||
.iter()
|
||||
.filter(|&pubkey| pubkey != &profile.public_key())
|
||||
.filter(|&pubkey| pubkey != &account.public_key())
|
||||
.collect::<Vec<_>>()
|
||||
.first()
|
||||
{
|
||||
get_cache_profile(public_key)
|
||||
shared_state().person(public_key)
|
||||
} else {
|
||||
profile
|
||||
account
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +198,7 @@ impl Room {
|
||||
let profiles = self
|
||||
.members
|
||||
.iter()
|
||||
.map(get_cache_profile)
|
||||
.map(|public_key| shared_state().person(public_key))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut name = profiles
|
||||
@@ -325,14 +323,18 @@ impl Room {
|
||||
/// A Task that resolves to Result<Vec<(PublicKey, Option<Metadata>)>, Error>
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn load_metadata(&self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
|
||||
let client = get_client();
|
||||
let public_keys = Arc::clone(&self.members);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
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()
|
||||
.await
|
||||
.entry(*public_key)
|
||||
@@ -359,7 +361,6 @@ impl Room {
|
||||
/// A Task that resolves to Result<Vec<(PublicKey, bool)>, Error> where
|
||||
/// the boolean indicates if the member has inbox relays configured
|
||||
pub fn messaging_relays(&self, cx: &App) -> Task<Result<Vec<(PublicKey, bool)>, Error>> {
|
||||
let client = get_client();
|
||||
let pubkeys = Arc::clone(&self.members);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
@@ -370,8 +371,13 @@ impl Room {
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(*pubkey)
|
||||
.limit(1);
|
||||
|
||||
let is_ready = client.database().query(filter).await?.first().is_some();
|
||||
let is_ready = shared_state()
|
||||
.client
|
||||
.database()
|
||||
.query(filter)
|
||||
.await?
|
||||
.first()
|
||||
.is_some();
|
||||
|
||||
result.push((*pubkey, is_ready));
|
||||
}
|
||||
@@ -391,9 +397,7 @@ impl Room {
|
||||
/// A Task that resolves to Result<Vec<RoomMessage>, Error> containing
|
||||
/// all messages for this room
|
||||
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<Message>, Error>> {
|
||||
let client = get_client();
|
||||
let pubkeys = Arc::clone(&self.members);
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::PrivateDirectMessage)
|
||||
.authors(pubkeys.to_vec())
|
||||
@@ -404,7 +408,8 @@ impl Room {
|
||||
let parser = NostrParser::new();
|
||||
|
||||
// Get all events from database
|
||||
let events = client
|
||||
let events = shared_state()
|
||||
.client
|
||||
.database()
|
||||
.query(filter)
|
||||
.await?
|
||||
@@ -452,10 +457,10 @@ impl Room {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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()
|
||||
.id(event.id)
|
||||
@@ -486,7 +491,7 @@ impl Room {
|
||||
///
|
||||
/// 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>) {
|
||||
let author = get_cache_profile(&event.pubkey);
|
||||
let author = shared_state().person(&event.pubkey);
|
||||
|
||||
// Extract all mentions from content
|
||||
let mentions = extract_mentions(&event.content);
|
||||
@@ -542,9 +547,9 @@ impl Room {
|
||||
&self,
|
||||
content: &str,
|
||||
replies: Option<&Vec<Message>>,
|
||||
cx: &App,
|
||||
_cx: &App,
|
||||
) -> Option<Message> {
|
||||
let author = Account::get_global(cx).profile.clone()?;
|
||||
let author = shared_state().identity()?;
|
||||
let public_key = author.public_key();
|
||||
let builder = EventBuilder::private_msg_rumor(public_key, content);
|
||||
|
||||
@@ -627,11 +632,10 @@ impl Room {
|
||||
let public_keys = Arc::clone(&self.members);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = shared_state().client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let mut reports = vec![];
|
||||
|
||||
let mut reports = vec![];
|
||||
let mut tags: Vec<Tag> = public_keys
|
||||
.iter()
|
||||
.filter_map(|pubkey| {
|
||||
@@ -671,11 +675,13 @@ impl Room {
|
||||
};
|
||||
|
||||
for receiver in receivers.iter() {
|
||||
if let Err(e) = client
|
||||
if let Err(e) = shared_state()
|
||||
.client
|
||||
.send_private_msg(*receiver, &content, tags.clone())
|
||||
.await
|
||||
{
|
||||
let metadata = client
|
||||
let metadata = shared_state()
|
||||
.client
|
||||
.database()
|
||||
.metadata(*receiver)
|
||||
.await?
|
||||
@@ -692,11 +698,13 @@ impl Room {
|
||||
|
||||
// Only send a backup message to current user if there are no issues when sending to others
|
||||
if reports.is_empty() {
|
||||
if let Err(e) = client
|
||||
if let Err(e) = shared_state()
|
||||
.client
|
||||
.send_private_msg(*current_user, &content, tags.clone())
|
||||
.await
|
||||
{
|
||||
let metadata = client
|
||||
let metadata = shared_state()
|
||||
.client
|
||||
.database()
|
||||
.metadata(*current_user)
|
||||
.await?
|
||||
@@ -732,7 +740,7 @@ pub fn extract_mentions(content: &str) -> Vec<Profile> {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for pubkey in pubkey_tokens.into_iter() {
|
||||
mentions.push(get_cache_profile(&pubkey));
|
||||
mentions.push(shared_state().person(&pubkey));
|
||||
}
|
||||
|
||||
mentions
|
||||
|
||||
@@ -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 std::{marker::PhantomData, time::Duration};
|
||||
|
||||
pub struct DebouncedDelay<E: 'static> {
|
||||
task: Option<Task<()>>,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
use global::constants::NIP96_SERVER;
|
||||
use gpui::{Image, ImageFormat};
|
||||
@@ -42,11 +40,12 @@ pub fn room_hash(event: &Event) -> u64 {
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
pub fn string_to_qr(data: &str) -> Result<Arc<Image>, anyhow::Error> {
|
||||
let bytes = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256)?;
|
||||
let img = Arc::new(Image::from_bytes(ImageFormat::Png, bytes));
|
||||
pub fn string_to_qr(data: &str) -> Option<Arc<Image>> {
|
||||
let Ok(bytes) = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Ok(img)
|
||||
Some(Arc::new(Image::from_bytes(ImageFormat::Png, bytes)))
|
||||
}
|
||||
|
||||
pub fn compare<T>(a: &[T], b: &[T]) -> bool
|
||||
|
||||
@@ -14,7 +14,6 @@ theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
global = { path = "../global" }
|
||||
chats = { path = "../chats" }
|
||||
account = { path = "../account" }
|
||||
auto_update = { path = "../auto_update" }
|
||||
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use account::Account;
|
||||
use anyhow::Error;
|
||||
use chats::{ChatRegistry, RoomEmitter};
|
||||
use global::{
|
||||
constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH},
|
||||
get_client,
|
||||
};
|
||||
use global::constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH};
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis, Context, Entity,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, Window,
|
||||
div, impl_internal_actions, px, App, AppContext, Axis, Context, Entity, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, Styled, Subscription, Task, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use nostr_connect::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::{ActiveTheme, Theme, ThemeMode};
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::{dock::DockPlacement, panel::PanelView, DockArea, DockItem},
|
||||
ContextModal, IconName, Root, Sizable, TitleBar,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::dock::DockPlacement;
|
||||
use ui::dock_area::panel::PanelView;
|
||||
use ui::dock_area::{DockArea, DockItem};
|
||||
use ui::{ContextModal, IconName, Root, Sizable, TitleBar};
|
||||
|
||||
use crate::views::chat::{self, Chat};
|
||||
use crate::views::{
|
||||
chat::{self, Chat},
|
||||
compose, login, new_account, onboarding, profile, relays, sidebar, welcome,
|
||||
compose, login, new_account, onboarding, profile, relays, sidebar, startup, welcome,
|
||||
};
|
||||
|
||||
impl_internal_actions!(dock, [ToggleModal]);
|
||||
@@ -63,16 +61,16 @@ pub struct ToggleModal {
|
||||
}
|
||||
|
||||
pub struct ChatSpace {
|
||||
titlebar: bool,
|
||||
dock: Entity<DockArea>,
|
||||
titlebar: bool,
|
||||
#[allow(unused)]
|
||||
subscriptions: SmallVec<[Subscription; 3]>,
|
||||
subscriptions: SmallVec<[Subscription; 2]>,
|
||||
}
|
||||
|
||||
impl ChatSpace {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
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 mut dock = DockArea::new(window, cx);
|
||||
// Initialize the dock area with the center panel
|
||||
@@ -81,46 +79,39 @@ impl ChatSpace {
|
||||
});
|
||||
|
||||
cx.new(|cx| {
|
||||
let account = Account::global(cx);
|
||||
let chats = ChatRegistry::global(cx);
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(cx.observe_in(
|
||||
&account,
|
||||
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| {
|
||||
// Automatically load messages when chat panel opens
|
||||
subscriptions.push(cx.observe_new::<Chat>(|this: &mut Chat, window, cx| {
|
||||
if let Some(window) = window {
|
||||
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 {
|
||||
dock,
|
||||
subscriptions,
|
||||
@@ -129,12 +120,10 @@ impl ChatSpace {
|
||||
})
|
||||
}
|
||||
|
||||
fn show_titlebar(&mut self, cx: &mut Context<Self>) {
|
||||
self.titlebar = true;
|
||||
cx.notify();
|
||||
}
|
||||
pub fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Disable the titlebar
|
||||
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 center = DockItem::panel(panel);
|
||||
|
||||
@@ -144,8 +133,9 @@ impl ChatSpace {
|
||||
});
|
||||
}
|
||||
|
||||
fn open_chats(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.show_titlebar(cx);
|
||||
pub fn open_chats(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Enable the titlebar
|
||||
self.titlebar(true, cx);
|
||||
|
||||
let weak_dock = self.dock.downgrade();
|
||||
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>> {
|
||||
cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = shared_state().client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(public_key)
|
||||
.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(exist)
|
||||
Ok(is_exist)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -298,11 +296,12 @@ impl Render for ChatSpace {
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_end()
|
||||
.gap_2()
|
||||
.gap_1p5()
|
||||
.px_2()
|
||||
.child(
|
||||
Button::new("appearance")
|
||||
.xsmall()
|
||||
.tooltip("Change the app's appearance")
|
||||
.small()
|
||||
.ghost()
|
||||
.map(|this| {
|
||||
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();
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
use anyhow::{anyhow, Error};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use asset::Assets;
|
||||
use auto_update::AutoUpdater;
|
||||
use chats::ChatRegistry;
|
||||
use futures::{select, FutureExt};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use global::constants::APP_NAME;
|
||||
use global::{
|
||||
constants::{
|
||||
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 global::constants::{APP_ID, KEYRING_BUNKER, KEYRING_USER_PATH};
|
||||
use global::{shared_state, NostrSignal};
|
||||
use gpui::{
|
||||
actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
|
||||
WindowBounds, WindowKind, WindowOptions,
|
||||
@@ -20,13 +17,7 @@ use gpui::{
|
||||
use gpui::{point, SharedString, TitlebarOptions};
|
||||
#[cfg(target_os = "linux")]
|
||||
use gpui::{WindowBackgroundAppearance, WindowDecorations};
|
||||
use nostr_sdk::{
|
||||
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 nostr_connect::prelude::*;
|
||||
use theme::Theme;
|
||||
use ui::Root;
|
||||
|
||||
@@ -36,226 +27,27 @@ pub(crate) mod views;
|
||||
|
||||
actions!(coop, [Quit]);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Signal {
|
||||
/// Receive event
|
||||
Event(Event),
|
||||
/// Receive eose
|
||||
Eose,
|
||||
/// Receive app updates
|
||||
AppUpdates(Event),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Initialize logging
|
||||
tracing_subscriber::fmt::init();
|
||||
// Initialize global state
|
||||
init_global_state();
|
||||
|
||||
let (event_tx, event_rx) = smol::channel::bounded::<Signal>(2048);
|
||||
let (batch_tx, batch_rx) = smol::channel::bounded::<Vec<PublicKey>>(500);
|
||||
|
||||
let client = get_client();
|
||||
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);
|
||||
}
|
||||
// Initialize the Global State and process events in a separate thread.
|
||||
// Must be run under async utility runtime
|
||||
nostr_sdk::async_utility::task::spawn(async move {
|
||||
shared_state().start().await;
|
||||
});
|
||||
|
||||
// Spawn a task to handle metadata batching
|
||||
// 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
|
||||
// Initialize the Application
|
||||
let app = Application::new()
|
||||
.with_assets(Assets)
|
||||
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
|
||||
|
||||
app.run(move |cx| {
|
||||
// Bring the app to the foreground
|
||||
cx.activate(true);
|
||||
|
||||
// Register the `quit` function
|
||||
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)]);
|
||||
|
||||
// Set menu items
|
||||
@@ -297,37 +89,87 @@ fn main() {
|
||||
|
||||
// Root Entity
|
||||
cx.new(|cx| {
|
||||
cx.activate(true);
|
||||
// Initialize components
|
||||
ui::init(cx);
|
||||
|
||||
// Initialize auto update
|
||||
auto_update::init(cx);
|
||||
|
||||
// Initialize chat state
|
||||
chats::init(cx);
|
||||
|
||||
// Initialize account state
|
||||
account::init(cx);
|
||||
// Initialize chatspace (or workspace)
|
||||
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
|
||||
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| {
|
||||
let chats = ChatRegistry::global(cx);
|
||||
let auto_updater = AutoUpdater::global(cx);
|
||||
|
||||
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| {
|
||||
this.load_rooms(window, cx);
|
||||
});
|
||||
}
|
||||
Signal::Event(event) => {
|
||||
NostrSignal::Event(event) => {
|
||||
chats.update(cx, |this, cx| {
|
||||
this.event_to_message(event, window, cx);
|
||||
});
|
||||
}
|
||||
Signal::AppUpdates(event) => {
|
||||
NostrSignal::AppUpdate(event) => {
|
||||
auto_updater.update(cx, |this, cx| {
|
||||
this.update(event, cx);
|
||||
});
|
||||
@@ -339,62 +181,26 @@ fn main() {
|
||||
})
|
||||
.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.");
|
||||
});
|
||||
}
|
||||
|
||||
async fn set_unwrapped(root: EventId, event: &Event, keys: &Keys) -> Result<(), Error> {
|
||||
let client = get_client();
|
||||
let event = EventBuilder::new(Kind::Custom(9001), event.as_json())
|
||||
.tags(vec![Tag::event(root)])
|
||||
.sign(keys) // keys must be random generated
|
||||
.await?;
|
||||
fn extract_credential(user: &str, secret: Vec<u8>) -> Result<impl NostrSigner, Error> {
|
||||
if user == KEYRING_BUNKER {
|
||||
let value = String::from_utf8(secret)?;
|
||||
let uri = NostrConnectURI::parse(value)?;
|
||||
let client_keys = shared_state().client_signer.clone();
|
||||
let signer = NostrConnect::new(uri, client_keys, Duration::from_secs(300), None)?;
|
||||
|
||||
client.database().save_event(&event).await?;
|
||||
|
||||
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)
|
||||
Ok(signer.into_nostr_signer())
|
||||
} else {
|
||||
Err(anyhow!("Event not found"))
|
||||
}
|
||||
}
|
||||
let secret_key = SecretKey::from_slice(&secret)?;
|
||||
let keys = Keys::new(secret_key);
|
||||
|
||||
async fn sync_metadata(
|
||||
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}");
|
||||
Ok(keys.into_nostr_signer())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 chats::{
|
||||
message::Message,
|
||||
room::{Room, RoomKind, SendError},
|
||||
};
|
||||
use common::{nip96_upload, profile::RenderProfile};
|
||||
use global::get_client;
|
||||
use chats::message::Message;
|
||||
use chats::room::{Room, RoomKind, SendError};
|
||||
use common::nip96_upload;
|
||||
use common::profile::RenderProfile;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, impl_internal_actions, list, prelude::FluentBuilder, px, red, relative, rems, svg,
|
||||
white, AnyElement, App, AppContext, ClipboardItem, Context, Div, Element, Empty, Entity,
|
||||
EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment,
|
||||
ListState, ObjectFit, ParentElement, PathPromptOptions, Render, RetainAllImageCache,
|
||||
SharedString, StatefulInteractiveElement, Styled, StyledImage, Subscription, Window,
|
||||
div, img, impl_internal_actions, list, px, red, relative, rems, svg, white, AnyElement, App,
|
||||
AppContext, ClipboardItem, Context, Div, Element, Empty, Entity, EventEmitter, Flatten,
|
||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, ListState, ObjectFit,
|
||||
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString,
|
||||
StatefulInteractiveElement, Styled, StyledImage, Subscription, Window,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
@@ -21,15 +23,15 @@ use serde::Deserialize;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smol::fs;
|
||||
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::{
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -217,7 +219,7 @@ impl Chat {
|
||||
|
||||
// TODO: find a better way to prevent duplicate messages during optimistic updates
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -225,7 +227,7 @@ impl Chat {
|
||||
return false;
|
||||
};
|
||||
|
||||
if current_user.public_key() != author.public_key() {
|
||||
if account.public_key() != author.public_key() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -238,7 +240,7 @@ impl Chat {
|
||||
m.borrow()
|
||||
.author
|
||||
.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| {
|
||||
let existing = existing.borrow();
|
||||
@@ -383,12 +385,11 @@ impl Chat {
|
||||
};
|
||||
|
||||
if let Ok(file_data) = fs::read(path).await {
|
||||
let client = get_client();
|
||||
let (tx, rx) = oneshot::channel::<Option<Url>>();
|
||||
|
||||
// Spawn task via async utility instead of GPUI context
|
||||
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),
|
||||
Err(e) => {
|
||||
log::error!("Upload error: {e}");
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
use std::{
|
||||
collections::{BTreeSet, HashSet},
|
||||
time::Duration,
|
||||
};
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use chats::{room::Room, ChatRegistry};
|
||||
use chats::room::Room;
|
||||
use chats::ChatRegistry;
|
||||
use common::profile::RenderProfile;
|
||||
use global::get_client;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, impl_internal_actions, prelude::FluentBuilder, px, red, relative, uniform_list, App,
|
||||
AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextAlign,
|
||||
Window,
|
||||
div, img, impl_internal_actions, px, red, relative, uniform_list, App, AppContext, Context,
|
||||
Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smol::Timer;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
input::{InputEvent, InputState, TextInput},
|
||||
ContextModal, Disableable, Icon, IconName, Sizable, StyledExt,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::{ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
|
||||
cx.new(|cx| Compose::new(window, cx))
|
||||
@@ -71,10 +68,13 @@ impl Compose {
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let task: Task<Result<BTreeSet<Profile>, Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = shared_state().client.signer().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)
|
||||
});
|
||||
@@ -133,8 +133,7 @@ impl Compose {
|
||||
let tags = Tags::from_list(tag_list);
|
||||
|
||||
let event: Task<Result<Event, anyhow::Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = shared_state().client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
// [IMPORTANT]
|
||||
@@ -173,7 +172,6 @@ impl Compose {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// Show loading spinner
|
||||
@@ -184,7 +182,8 @@ impl Compose {
|
||||
let profile = nip05::profile(&content, None).await?;
|
||||
let public_key = profile.public_key;
|
||||
|
||||
let metadata = client
|
||||
let metadata = shared_state()
|
||||
.client
|
||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
@@ -199,7 +198,8 @@ impl Compose {
|
||||
};
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let metadata = client
|
||||
let metadata = shared_state()
|
||||
.client
|
||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -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 global::get_client_keys;
|
||||
use global::constants::{APP_NAME, KEYRING_BUNKER, KEYRING_USER_PATH};
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder, red, relative, AnyElement, App, AppContext, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Image, IntoElement, ParentElement, Render, SharedString,
|
||||
Styled, Subscription, Window,
|
||||
div, img, red, relative, AnyElement, App, AppContext, ClipboardItem, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
||||
};
|
||||
use nostr_connect::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
input::{InputEvent, InputState, TextInput},
|
||||
notification::Notification,
|
||||
popup_menu::PopupMenu,
|
||||
ContextModal, Disableable, Sizable, StyledExt,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{ContextModal, Disableable, Sizable, StyledExt};
|
||||
|
||||
const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
||||
const NOSTR_CONNECT_TIMEOUT: u64 = 300;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CoopAuthUrlHandler;
|
||||
@@ -37,23 +40,20 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
|
||||
}
|
||||
|
||||
pub struct Login {
|
||||
// Inputs
|
||||
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>>,
|
||||
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
|
||||
name: SharedString,
|
||||
closable: bool,
|
||||
zoomable: bool,
|
||||
focus_handle: FocusHandle,
|
||||
#[allow(unused)]
|
||||
subscriptions: SmallVec<[Subscription; 4]>,
|
||||
subscriptions: SmallVec<[Subscription; 5]>,
|
||||
}
|
||||
|
||||
impl Login {
|
||||
@@ -62,106 +62,159 @@ impl Login {
|
||||
}
|
||||
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let connect_client: Entity<Option<NostrConnectURI>> = cx.new(|_| None);
|
||||
let error = cx.new(|_| None);
|
||||
let qr = cx.new(|_| None);
|
||||
|
||||
// nsec or bunker_uri (NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md)
|
||||
let key_input =
|
||||
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![];
|
||||
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&key_input,
|
||||
window,
|
||||
move |this, _, event, window, cx| {
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&key_input, window, |this, _, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
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(
|
||||
&connect_relay,
|
||||
subscriptions.push(cx.observe_in(
|
||||
&connection_string,
|
||||
window,
|
||||
move |this, _, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
this.change_relay(window, cx);
|
||||
|this, entity, _window, cx| {
|
||||
let connection_string = entity.read(cx).clone();
|
||||
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(
|
||||
cx.observe_in(&connect_client, window, |this, uri, window, cx| {
|
||||
let keys = get_client_keys().to_owned();
|
||||
cx.observe_in(&active_signer, window, |_this, entity, window, cx| {
|
||||
if let Some(signer) = entity.read(cx).clone() {
|
||||
let (tx, rx) = oneshot::channel::<Option<NostrConnectURI>>();
|
||||
|
||||
if let Some(uri) = uri.read(cx).clone() {
|
||||
if let Ok(qr) = string_to_qr(uri.to_string().as_str()) {
|
||||
this.qr.update(cx, |this, cx| {
|
||||
*this = Some(qr);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(bunker_uri) = signer.bunker_uri().await {
|
||||
tx.send(Some(bunker_uri)).ok();
|
||||
|
||||
// Shutdown all previous nostr connect clients
|
||||
for client in std::mem::take(&mut this.signers).into_iter() {
|
||||
cx.background_spawn(async move {
|
||||
client.shutdown().await;
|
||||
})
|
||||
.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);
|
||||
});
|
||||
if let Err(e) = shared_state().set_signer(signer).await {
|
||||
log::error!("{}", e);
|
||||
}
|
||||
} else {
|
||||
tx.send(None).ok();
|
||||
}
|
||||
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 {
|
||||
key_input,
|
||||
connect_relay,
|
||||
connect_client,
|
||||
subscriptions,
|
||||
signers,
|
||||
error,
|
||||
qr,
|
||||
is_logging_in: false,
|
||||
name: "Login".into(),
|
||||
closable: true,
|
||||
zoomable: true,
|
||||
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 {
|
||||
return;
|
||||
};
|
||||
|
||||
self.set_logging_in(true, cx);
|
||||
|
||||
let client_keys = shared_state().client_signer.clone();
|
||||
let content = self.key_input.read(cx).value();
|
||||
let account = Account::global(cx);
|
||||
|
||||
if content.starts_with("nsec1") {
|
||||
match SecretKey::parse(content.as_ref()) {
|
||||
Ok(secret) => {
|
||||
let keys = Keys::new(secret);
|
||||
let Ok(keys) = SecretKey::parse(content.as_ref()).map(Keys::new) else {
|
||||
self.set_error("Secret key is not valid", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
account.update(cx, |this, cx| {
|
||||
this.login(keys, window, cx);
|
||||
});
|
||||
// Active signer is no longer needed
|
||||
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) => {
|
||||
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 {
|
||||
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>) {
|
||||
let Ok(relay_url) =
|
||||
RelayUrl::parse(self.connect_relay.read(cx).value().to_string().as_str())
|
||||
let Ok(relay_url) = RelayUrl::parse(self.relay_input.read(cx).value().to_string().as_str())
|
||||
else {
|
||||
window.push_notification(Notification::error("Relay URL is not valid."), cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let client_pubkey = get_client_keys().public_key();
|
||||
let uri = NostrConnectURI::client(client_pubkey, vec![relay_url], "Coop");
|
||||
let client_keys = shared_state().client_signer.clone();
|
||||
let uri = NostrConnectURI::client(client_keys.public_key(), vec![relay_url], "Coop");
|
||||
|
||||
self.connect_client.update(cx, |this, cx| {
|
||||
*this = Some(uri);
|
||||
self.connection_string.update(cx, |this, cx| {
|
||||
*this = uri;
|
||||
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.error.update(cx, |this, cx| {
|
||||
*this = Some(SharedString::new(message));
|
||||
*this = Some(message.into());
|
||||
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>) {
|
||||
@@ -243,14 +402,6 @@ impl Panel for Login {
|
||||
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 {
|
||||
menu.track_focus(&self.focus_handle)
|
||||
}
|
||||
@@ -365,9 +516,10 @@ impl Render for Login {
|
||||
.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(
|
||||
div()
|
||||
.id("")
|
||||
.mb_2()
|
||||
.p_2()
|
||||
.size_72()
|
||||
@@ -384,7 +536,27 @@ impl Render for Login {
|
||||
.border_color(cx.theme().border)
|
||||
})
|
||||
.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(
|
||||
@@ -394,7 +566,7 @@ impl Render for Login {
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_1()
|
||||
.child(TextInput::new(&self.connect_relay).xsmall())
|
||||
.child(TextInput::new(&self.relay_input).xsmall())
|
||||
.child(
|
||||
Button::new("change")
|
||||
.label("Change")
|
||||
|
||||
@@ -6,5 +6,6 @@ pub mod onboarding;
|
||||
pub mod profile;
|
||||
pub mod relays;
|
||||
pub mod sidebar;
|
||||
pub mod startup;
|
||||
pub mod subject;
|
||||
pub mod welcome;
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use account::Account;
|
||||
use async_utility::task::spawn;
|
||||
use common::nip96_upload;
|
||||
use global::get_client;
|
||||
use global::constants::KEYRING_USER_PATH;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder, relative, AnyElement, App, AppContext, Context, Entity,
|
||||
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
|
||||
Render, SharedString, Styled, Window,
|
||||
div, img, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten,
|
||||
FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString,
|
||||
Styled, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smol::fs;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
input::{InputState, TextInput},
|
||||
popup_menu::PopupMenu,
|
||||
Disableable, Icon, IconName, Sizable, StyledExt,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
||||
NewAccount::new(window, cx)
|
||||
@@ -44,8 +41,10 @@ impl NewAccount {
|
||||
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
|
||||
|
||||
let avatar_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg"));
|
||||
|
||||
let bio_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.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);
|
||||
|
||||
let avatar = self.avatar_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 keys = Keys::generate();
|
||||
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);
|
||||
};
|
||||
|
||||
Account::global(cx).update(cx, |this, cx| {
|
||||
this.new_account(metadata, window, cx);
|
||||
});
|
||||
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)
|
||||
};
|
||||
shared_state().new_account(keys, metadata).await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
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 {
|
||||
let client = get_client();
|
||||
let (tx, rx) = oneshot::channel::<Url>();
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,12 +3,10 @@ use gpui::{
|
||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
popup_menu::PopupMenu,
|
||||
Icon, IconName, StyledExt,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{Icon, IconName, StyledExt};
|
||||
|
||||
use crate::chatspace;
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_utility::task::spawn;
|
||||
use common::nip96_upload;
|
||||
use global::get_client;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder, App, AppContext, Context, Entity, Flatten, IntoElement,
|
||||
ParentElement, PathPromptOptions, Render, Styled, Task, Window,
|
||||
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
|
||||
PathPromptOptions, Render, Styled, Task, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smol::fs;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
input::{InputState, TextInput},
|
||||
ContextModal, Disableable, IconName, Sizable,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{ContextModal, Disableable, IconName, Sizable};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Profile> {
|
||||
Profile::new(window, cx)
|
||||
@@ -54,10 +55,10 @@ impl Profile {
|
||||
};
|
||||
|
||||
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = shared_state().client.signer().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))
|
||||
.await?;
|
||||
|
||||
@@ -122,8 +123,7 @@ impl Profile {
|
||||
let (tx, rx) = oneshot::channel::<Url>();
|
||||
|
||||
spawn(async move {
|
||||
let client = get_client();
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -189,9 +189,7 @@ impl Profile {
|
||||
}
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
_ = client.set_metadata(&new_metadata).await?;
|
||||
|
||||
let _ = shared_state().client.set_metadata(&new_metadata).await?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
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::{
|
||||
div, prelude::FluentBuilder, px, uniform_list, App, AppContext, Context, Entity, FocusHandle,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, TextAlign,
|
||||
UniformList, Window,
|
||||
div, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, Styled, Subscription, Task, TextAlign, UniformList, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
input::{InputEvent, InputState, TextInput},
|
||||
ContextModal, Disableable, IconName, Sizable,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::{ContextModal, Disableable, IconName, Sizable};
|
||||
|
||||
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.";
|
||||
@@ -36,16 +35,20 @@ impl Relays {
|
||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
|
||||
let relays = cx.new(|cx| {
|
||||
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = shared_state().client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(public_key)
|
||||
.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
|
||||
.tags
|
||||
.filter(TagKind::Relay)
|
||||
@@ -108,18 +111,23 @@ impl Relays {
|
||||
|
||||
let relays = self.relays.read(cx).clone();
|
||||
let task: Task<Result<EventId, Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = shared_state().client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
// 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![
|
||||
(RelayUrl::parse("wss://relay.damus.io/").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);
|
||||
}
|
||||
}
|
||||
@@ -130,21 +138,22 @@ impl Relays {
|
||||
.collect();
|
||||
|
||||
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
|
||||
for relay in relays.into_iter() {
|
||||
_ = client.add_relay(&relay).await;
|
||||
_ = client.connect_relay(&relay).await;
|
||||
_ = shared_state().client.add_relay(&relay).await;
|
||||
_ = shared_state().client.connect_relay(&relay).await;
|
||||
}
|
||||
|
||||
let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||
|
||||
// Close old subscription
|
||||
client.unsubscribe(&sub_id).await;
|
||||
shared_state().client.unsubscribe(&sub_id).await;
|
||||
|
||||
// Subscribe to new messages
|
||||
if let Err(e) = client
|
||||
if let Err(e) = shared_state()
|
||||
.client
|
||||
.subscribe_with_id(
|
||||
sub_id,
|
||||
Filter::new()
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder, rems, App, ClickEvent, Div, InteractiveElement, IntoElement,
|
||||
ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
div, img, rems, App, ClickEvent, Div, InteractiveElement, IntoElement, ParentElement as _,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{avatar::Avatar, StyledExt};
|
||||
use ui::avatar::Avatar;
|
||||
use ui::StyledExt;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct DisplayRoom {
|
||||
|
||||
@@ -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 chats::{
|
||||
room::{Room, RoomKind},
|
||||
ChatRegistry, RoomEmitter,
|
||||
};
|
||||
|
||||
use common::{debounced_delay::DebouncedDelay, profile::RenderProfile};
|
||||
use chats::room::{Room, RoomKind};
|
||||
use chats::{ChatRegistry, RoomEmitter};
|
||||
use common::debounced_delay::DebouncedDelay;
|
||||
use common::profile::RenderProfile;
|
||||
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::{
|
||||
div, prelude::FluentBuilder, rems, uniform_list, AnyElement, App, AppContext, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache,
|
||||
SharedString, Styled, Subscription, Task, Window,
|
||||
div, rems, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString,
|
||||
Styled, Subscription, Task, Window,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
avatar::Avatar,
|
||||
button::{Button, ButtonRounded, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
input::{InputEvent, InputState, TextInput},
|
||||
popup_menu::{PopupMenu, PopupMenuExt},
|
||||
skeleton::Skeleton,
|
||||
ContextModal, IconName, Selectable, Sizable, StyledExt,
|
||||
};
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::popup_menu::{PopupMenu, PopupMenuExt};
|
||||
use ui::skeleton::Skeleton;
|
||||
use ui::{ContextModal, IconName, Selectable, Sizable, StyledExt};
|
||||
|
||||
use crate::chatspace::{ModalKind, ToggleModal};
|
||||
|
||||
@@ -142,14 +141,13 @@ impl Sidebar {
|
||||
let query = self.find_input.read(cx).value().clone();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Metadata)
|
||||
.search(query.to_lowercase())
|
||||
.limit(FIND_LIMIT);
|
||||
|
||||
let events = client
|
||||
let events = shared_state()
|
||||
.client
|
||||
.fetch_events_from(SEARCH_RELAYS, filter, Duration::from_secs(3))
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -160,8 +158,11 @@ impl Sidebar {
|
||||
let (tx, rx) = smol::channel::bounded::<Room>(10);
|
||||
|
||||
spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await.expect("signer is required");
|
||||
let signer = shared_state()
|
||||
.client
|
||||
.signer()
|
||||
.await
|
||||
.expect("signer is required");
|
||||
let public_key = signer.get_public_key().await.expect("error");
|
||||
|
||||
for event in events.into_iter() {
|
||||
@@ -492,9 +493,10 @@ impl Render for Sidebar {
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
// Account
|
||||
.when_some(Account::get_global(cx).profile_ref(), |this, profile| {
|
||||
this.child(self.render_account(profile, cx))
|
||||
})
|
||||
.when_some(
|
||||
shared_state().identity.read_blocking().as_ref(),
|
||||
|this, profile| this.child(self.render_account(profile, cx)),
|
||||
)
|
||||
// Search Input
|
||||
.child(
|
||||
div().px_3().w_full().h_7().flex_none().child(
|
||||
|
||||
88
crates/coop/src/views/startup.rs
Normal file
88
crates/coop/src/views/startup.rs
Normal 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()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,9 @@ use gpui::{
|
||||
ParentElement, Render, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
input::{InputState, TextInput},
|
||||
ContextModal, Sizable,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{ContextModal, Sizable};
|
||||
|
||||
pub fn init(
|
||||
id: u64,
|
||||
|
||||
@@ -3,12 +3,10 @@ use gpui::{
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::Button,
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
popup_menu::PopupMenu,
|
||||
StyledExt,
|
||||
};
|
||||
use ui::button::Button;
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::StyledExt;
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Welcome> {
|
||||
Welcome::new(window, cx)
|
||||
|
||||
@@ -5,9 +5,14 @@ edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
nostr-keyring.workspace = true
|
||||
nostr-connect.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
dirs.workspace = true
|
||||
smol.workspace = true
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
whoami = "1.5.2"
|
||||
rustls = "0.23.23"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
pub const APP_NAME: &str = "Coop";
|
||||
pub const APP_ID: &str = "su.reya.coop";
|
||||
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.
|
||||
pub const BOOTSTRAP_RELAYS: [&str; 4] = [
|
||||
@@ -32,3 +35,13 @@ pub const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";
|
||||
|
||||
/// NIP96 Media Server.
|
||||
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",
|
||||
];
|
||||
|
||||
@@ -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 paths::nostr_file;
|
||||
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 paths;
|
||||
|
||||
/// Represents the global state of the Nostr client, including:
|
||||
/// - The Nostr client instance
|
||||
/// - Client keys
|
||||
/// - A cache of user profiles (metadata)
|
||||
pub struct NostrState {
|
||||
keys: Keys,
|
||||
client: Client,
|
||||
cache_profiles: RwLock<BTreeMap<PublicKey, Option<Metadata>>>,
|
||||
/// Global singleton instance for application state
|
||||
static GLOBALS: OnceLock<Globals> = OnceLock::new();
|
||||
|
||||
/// Signals sent through the global event channel to notify UI components
|
||||
#[derive(Debug)]
|
||||
pub enum NostrSignal {
|
||||
/// User's signing keys have been updated
|
||||
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
|
||||
static GLOBAL_STATE: OnceLock<NostrState> = OnceLock::new();
|
||||
/// Global application state containing Nostr client and shared resources
|
||||
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:
|
||||
/// - LMDB database backend
|
||||
/// - Default client options (gossip enabled, 800ms max avg latency)
|
||||
/// - Newly generated keys
|
||||
/// - Empty profile cache
|
||||
pub fn init_global_state() -> NostrState {
|
||||
// rustls uses the `aws_lc_rs` provider by default
|
||||
// This only errors if the default provider has already
|
||||
// been installed. We can ignore this `Result`.
|
||||
rustls::crypto::aws_lc_rs::default_provider()
|
||||
.install_default()
|
||||
.ok();
|
||||
/// Returns the global singleton instance, initializing it if necessary
|
||||
pub fn shared_state() -> &'static Globals {
|
||||
GLOBALS.get_or_init(|| {
|
||||
// rustls uses the `aws_lc_rs` provider by default
|
||||
// This only errors if the default provider has already
|
||||
// been installed. We can ignore this `Result`.
|
||||
rustls::crypto::aws_lc_rs::default_provider()
|
||||
.install_default()
|
||||
.ok();
|
||||
|
||||
// Setup database
|
||||
let db_path = nostr_file();
|
||||
let lmdb = NostrLMDB::open(db_path).expect("Database is NOT initialized");
|
||||
let keyring = NostrKeyring::new(KEYRING_PATH);
|
||||
// Get the client signer or generate a new one if it doesn't exist
|
||||
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)
|
||||
.max_avg_latency(Duration::from_millis(800));
|
||||
let opts = Options::new().gossip(true);
|
||||
let lmdb = NostrLMDB::open(nostr_file()).expect("Database is NOT initialized");
|
||||
|
||||
NostrState {
|
||||
client: ClientBuilder::default().database(lmdb).opts(opts).build(),
|
||||
keys: Keys::generate(),
|
||||
cache_profiles: RwLock::new(BTreeMap::new()),
|
||||
let (global_sender, global_receiver) =
|
||||
smol::channel::bounded::<NostrSignal>(GLOBAL_CHANNEL_LIMIT);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -9,19 +9,6 @@ use gpui::{Hsla, SharedString};
|
||||
pub struct ColorScaleStep(usize);
|
||||
|
||||
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`].
|
||||
pub const ALL: [ColorScaleStep; 12] = [
|
||||
Self::ONE,
|
||||
@@ -37,6 +24,18 @@ impl ColorScaleStep {
|
||||
Self::ELEVEN,
|
||||
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`].
|
||||
@@ -191,8 +190,8 @@ pub struct ColorScales {
|
||||
}
|
||||
|
||||
impl IntoIterator for ColorScales {
|
||||
type Item = ColorScaleSet;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
type Item = ColorScaleSet;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
vec![
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder, px, rems, AbsoluteLength, App, Hsla, ImageSource, Img,
|
||||
IntoElement, ParentElement, RenderOnce, Styled, StyledImage, Window,
|
||||
div, img, px, rems, AbsoluteLength, App, Hsla, ImageSource, Img, IntoElement, ParentElement,
|
||||
RenderOnce, Styled, StyledImage, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, relative, AnyElement, App, ClickEvent, Div, ElementId, Hsla,
|
||||
InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString,
|
||||
div, relative, AnyElement, App, ClickEvent, Div, ElementId, Hsla, InteractiveElement,
|
||||
IntoElement, MouseButton, ParentElement, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement as _, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{
|
||||
indicator::Indicator, tooltip::Tooltip, Disableable, Icon, Selectable, Sizable, Size, StyledExt,
|
||||
};
|
||||
use crate::indicator::Indicator;
|
||||
use crate::tooltip::Tooltip;
|
||||
use crate::{Disableable, Icon, Selectable, Sizable, Size, StyledExt};
|
||||
|
||||
pub enum ButtonRounded {
|
||||
Normal,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, relative, svg, App, ElementId, InteractiveElement,
|
||||
IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _,
|
||||
Styled as _, Window,
|
||||
div, relative, svg, App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
SharedString, StatefulInteractiveElement as _, Styled as _, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use crate::popup_menu::PopupMenu;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
anchored, deferred, div, prelude::FluentBuilder, px, relative, AnyElement, App, Context,
|
||||
Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, Focusable, FocusableWrapper,
|
||||
GlobalElementId, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement,
|
||||
Pixels, Point, Position, Size, Stateful, Style, Window,
|
||||
anchored, deferred, div, px, relative, AnyElement, App, Context, Corner, DismissEvent,
|
||||
DispatchPhase, Element, ElementId, Entity, Focusable, FocusableWrapper, GlobalElementId,
|
||||
InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
|
||||
Position, Size, Stateful, Style, Window,
|
||||
};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crate::popup_menu::PopupMenu;
|
||||
|
||||
pub trait ContextMenuExt: ParentElement + Sized {
|
||||
fn context_menu(
|
||||
@@ -92,8 +96,8 @@ impl Default for ContextMenuState {
|
||||
}
|
||||
|
||||
impl Element for ContextMenu {
|
||||
type RequestLayoutState = ContextMenuState;
|
||||
type PrepaintState = ();
|
||||
type RequestLayoutState = ContextMenuState;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, Axis, Div, Hsla, IntoElement, ParentElement, RenderOnce,
|
||||
SharedString, Styled,
|
||||
div, px, Axis, Div, Hsla, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, App, AppContext, Axis, Context, Element, Entity,
|
||||
InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels,
|
||||
Point, Render, StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window,
|
||||
div, px, App, AppContext, Axis, Context, Element, Entity, InteractiveElement as _, IntoElement,
|
||||
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
|
||||
StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use super::{DockArea, DockItem};
|
||||
use crate::{
|
||||
dock_area::{panel::PanelView, tab_panel::TabPanel},
|
||||
resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE},
|
||||
AxisExt as _, StyledExt,
|
||||
};
|
||||
use crate::dock_area::panel::PanelView;
|
||||
use crate::dock_area::tab_panel::TabPanel;
|
||||
use crate::resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE};
|
||||
use crate::{AxisExt as _, StyledExt};
|
||||
|
||||
#[derive(Clone, Render)]
|
||||
struct ResizePanel;
|
||||
@@ -396,8 +396,8 @@ impl IntoElement for DockElement {
|
||||
}
|
||||
|
||||
impl Element for DockElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
type RequestLayoutState = ();
|
||||
|
||||
fn id(&self) -> Option<gpui::ElementId> {
|
||||
None
|
||||
|
||||
@@ -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 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 panel;
|
||||
pub mod stack_panel;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::{button::Button, popup_menu::PopupMenu};
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Render,
|
||||
SharedString, Window,
|
||||
};
|
||||
|
||||
use crate::button::Button;
|
||||
use crate::popup_menu::PopupMenu;
|
||||
|
||||
pub enum PanelEvent {
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
@@ -118,6 +120,7 @@ impl<T: Panel> PanelView for Entity<T> {
|
||||
this.set_active(active, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn set_zoomed(&self, zoomed: bool, cx: &mut App) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.set_zoomed(zoomed, cx);
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
use super::{DockArea, PanelEvent};
|
||||
use crate::{
|
||||
dock_area::{
|
||||
panel::{Panel, PanelView},
|
||||
tab_panel::TabPanel,
|
||||
},
|
||||
h_flex,
|
||||
resizable::{
|
||||
h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent,
|
||||
ResizablePanelGroup,
|
||||
},
|
||||
AxisExt as _, Placement,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
prelude::FluentBuilder, App, AppContext, Axis, Context, DismissEvent, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, SharedString, Styled,
|
||||
Subscription, WeakEntity, Window,
|
||||
App, AppContext, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Subscription, WeakEntity,
|
||||
Window,
|
||||
};
|
||||
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(super) parent: Option<WeakEntity<StackPanel>>,
|
||||
|
||||
@@ -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 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 super::{
|
||||
panel::PanelView, stack_panel::StackPanel, ClosePanel, DockArea, PanelEvent, PanelStyle,
|
||||
ToggleZoom,
|
||||
};
|
||||
use crate::{
|
||||
button::{Button, ButtonVariants as _},
|
||||
dock_area::{dock::DockPlacement, panel::Panel},
|
||||
h_flex,
|
||||
popup_menu::{PopupMenu, PopupMenuExt},
|
||||
tab::{tab_bar::TabBar, Tab},
|
||||
v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt,
|
||||
};
|
||||
use super::panel::PanelView;
|
||||
use super::stack_panel::StackPanel;
|
||||
use super::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::dock_area::dock::DockPlacement;
|
||||
use crate::dock_area::panel::Panel;
|
||||
use crate::popup_menu::{PopupMenu, PopupMenuExt};
|
||||
use crate::tab::tab_bar::TabBar;
|
||||
use crate::tab::Tab;
|
||||
use crate::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TabState {
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
anchored, canvas, deferred, div, prelude::FluentBuilder, px, rems, AnyElement, App, AppContext,
|
||||
Bounds, ClickEvent, Context, DismissEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, InteractiveElement, IntoElement, KeyBinding, Length, ParentElement, Pixels, Render,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity,
|
||||
Window,
|
||||
anchored, canvas, deferred, div, px, rems, AnyElement, App, AppContext, Bounds, ClickEvent,
|
||||
Context, DismissEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, KeyBinding, Length, ParentElement, Pixels, Render, RenderOnce,
|
||||
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{
|
||||
actions::{Cancel, Confirm, SelectNext, SelectPrev},
|
||||
h_flex,
|
||||
input::clear_button::clear_button,
|
||||
list::{List, ListDelegate, ListItem},
|
||||
v_flex, Disableable as _, Icon, IconName, Sizable, Size, StyleSized,
|
||||
};
|
||||
use crate::actions::{Cancel, Confirm, SelectNext, SelectPrev};
|
||||
use crate::input::clear_button::clear_button;
|
||||
use crate::list::{List, ListDelegate, ListItem};
|
||||
use crate::{h_flex, v_flex, Disableable as _, Icon, IconName, Sizable, Size, StyleSized};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ListEvent {
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Corner, Element,
|
||||
InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement, Styled, WeakEntity, Window,
|
||||
div, impl_internal_actions, px, App, AppContext, Corner, Element, InteractiveElement,
|
||||
IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
|
||||
WeakEntity, Window,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{
|
||||
button::{Button, ButtonVariants},
|
||||
input::InputState,
|
||||
popover::{Popover, PopoverContent},
|
||||
Icon,
|
||||
};
|
||||
use crate::button::{Button, ButtonVariants};
|
||||
use crate::input::InputState;
|
||||
use crate::popover::{Popover, PopoverContent};
|
||||
use crate::Icon;
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Deserialize)]
|
||||
pub struct EmitEmoji(pub SharedString);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub trait HistoryItem: Clone {
|
||||
fn version(&self) -> usize;
|
||||
@@ -156,6 +154,7 @@ mod tests {
|
||||
fn version(&self) -> usize {
|
||||
self.version
|
||||
}
|
||||
|
||||
fn set_version(&mut self, version: usize) {
|
||||
self.version = version;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
prelude::FluentBuilder as _, svg, AnyElement, App, AppContext, Entity, Hsla, IntoElement,
|
||||
Radians, Render, RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation,
|
||||
Window,
|
||||
svg, AnyElement, App, AppContext, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
|
||||
SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -40,6 +40,7 @@ pub enum IconName {
|
||||
Inbox,
|
||||
Info,
|
||||
Loader,
|
||||
Logout,
|
||||
Moon,
|
||||
PanelBottom,
|
||||
PanelBottomOpen,
|
||||
@@ -108,6 +109,7 @@ impl IconName {
|
||||
Self::Inbox => "icons/inbox.svg",
|
||||
Self::Info => "icons/info.svg",
|
||||
Self::Loader => "icons/loader.svg",
|
||||
Self::Logout => "icons/logout.svg",
|
||||
Self::Moon => "icons/moon.svg",
|
||||
Self::PanelBottom => "icons/panel-bottom.svg",
|
||||
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
|
||||
|
||||
@@ -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 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)]
|
||||
pub struct Indicator {
|
||||
size: Size,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{fmt::Debug, ops::Range};
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::history::HistoryItem;
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use gpui::{App, Styled};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{
|
||||
button::{Button, ButtonVariants as _},
|
||||
Icon, IconName, Sizable as _,
|
||||
};
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::{Icon, IconName, Sizable as _};
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn clear_button(cx: &App) -> Button {
|
||||
|
||||
@@ -348,8 +348,8 @@ fn print_points_as_svg_path(line_corners: &Vec<Corners<Point<Pixels>>>, points:
|
||||
}
|
||||
|
||||
impl Element for TextElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = PrepaintState;
|
||||
type RequestLayoutState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
|
||||
@@ -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 smallvec::SmallVec;
|
||||
use std::{cell::Cell, ops::Range, rc::Rc};
|
||||
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:
|
||||
// - Move cursor to skip line eof empty chars.
|
||||
|
||||
use super::{
|
||||
blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern,
|
||||
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)]
|
||||
pub struct Enter {
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, relative, AnyElement, App, DefiniteLength, Entity,
|
||||
InteractiveElement as _, IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce,
|
||||
Styled, Window,
|
||||
div, px, relative, AnyElement, App, DefiniteLength, Entity, InteractiveElement as _,
|
||||
IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use super::InputState;
|
||||
use crate::{
|
||||
button::{Button, ButtonVariants as _},
|
||||
h_flex,
|
||||
indicator::Indicator,
|
||||
input::clear_button::clear_button,
|
||||
scroll::{Scrollbar, ScrollbarAxis},
|
||||
IconName, Sizable, Size, StyleSized,
|
||||
};
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::indicator::Indicator;
|
||||
use crate::input::clear_button::clear_button;
|
||||
use crate::scroll::{Scrollbar, ScrollbarAxis};
|
||||
use crate::{h_flex, IconName, Sizable, Size, StyleSized};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct TextInput {
|
||||
|
||||
@@ -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::{
|
||||
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,
|
||||
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 theme::ActiveTheme;
|
||||
|
||||
use super::loading::Loading;
|
||||
use crate::{
|
||||
actions::{Cancel, Confirm, SelectNext, SelectPrev},
|
||||
input::{InputEvent, InputState, TextInput},
|
||||
scroll::{Scrollbar, ScrollbarState},
|
||||
v_flex, Icon, IconName, Sizable as _, Size,
|
||||
};
|
||||
use crate::actions::{Cancel, Confirm, SelectNext, SelectPrev};
|
||||
use crate::input::{InputEvent, InputState, TextInput};
|
||||
use crate::scroll::{Scrollbar, ScrollbarState};
|
||||
use crate::{v_flex, Icon, IconName, Sizable as _, Size};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let context: Option<&str> = Some("List");
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, AnyElement, App, ClickEvent, Div, ElementId,
|
||||
InteractiveElement, IntoElement, MouseButton, MouseMoveEvent, ParentElement, RenderOnce,
|
||||
Stateful, StatefulInteractiveElement as _, Styled, Window,
|
||||
div, AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement, MouseButton,
|
||||
MouseMoveEvent, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement as _, Styled,
|
||||
Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use gpui::{IntoElement, ParentElement as _, RenderOnce, Styled};
|
||||
|
||||
use super::ListItem;
|
||||
use crate::{skeleton::Skeleton, v_flex};
|
||||
use crate::skeleton::Skeleton;
|
||||
use crate::v_flex;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Loading;
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
use std::{rc::Rc, time::Duration};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
actions, anchored, div, point, prelude::FluentBuilder, px, relative, Animation,
|
||||
AnimationExt as _, AnyElement, App, Bounds, ClickEvent, Div, FocusHandle, InteractiveElement,
|
||||
IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString,
|
||||
Styled, Window,
|
||||
actions, anchored, div, point, px, relative, Animation, AnimationExt as _, AnyElement, App,
|
||||
Bounds, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding, MouseButton,
|
||||
ParentElement, Pixels, Point, RenderOnce, SharedString, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{
|
||||
animation::cubic_bezier,
|
||||
button::{Button, ButtonCustomVariant, ButtonVariants as _},
|
||||
v_flex, ContextModal, IconName, StyledExt,
|
||||
};
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonCustomVariant, ButtonVariants as _};
|
||||
use crate::{v_flex, ContextModal, IconName, StyledExt};
|
||||
|
||||
actions!(modal, [Escape]);
|
||||
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use std::any::TypeId;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
blue, div, green, prelude::FluentBuilder, px, red, yellow, Animation, AnimationExt, App,
|
||||
AppContext, ClickEvent, Context, DismissEvent, ElementId, Entity, EventEmitter,
|
||||
InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Subscription, Window,
|
||||
blue, div, green, px, red, yellow, Animation, AnimationExt, App, AppContext, ClickEvent,
|
||||
Context, DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
||||
Window,
|
||||
};
|
||||
use smol::Timer;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{
|
||||
animation::cubic_bezier,
|
||||
button::{Button, ButtonVariants as _},
|
||||
h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt,
|
||||
};
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::{h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt};
|
||||
|
||||
pub enum NotificationType {
|
||||
Info,
|
||||
|
||||
@@ -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::{
|
||||
actions, anchored, deferred, div, prelude::FluentBuilder as _, px, AnyElement, App, Bounds,
|
||||
Context, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, GlobalElementId, Hitbox, HitboxBehavior, InteractiveElement as _,
|
||||
IntoElement, KeyBinding, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement,
|
||||
Pixels, Point, Render, ScrollHandle, StatefulInteractiveElement, Style, StyleRefinement,
|
||||
Styled, Window,
|
||||
actions, anchored, deferred, div, px, AnyElement, App, Bounds, Context, Corner, DismissEvent,
|
||||
DispatchPhase, Element, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
GlobalElementId, Hitbox, HitboxBehavior, InteractiveElement as _, IntoElement, KeyBinding,
|
||||
LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render,
|
||||
ScrollHandle, StatefulInteractiveElement, Style, StyleRefinement, Styled, Window,
|
||||
};
|
||||
|
||||
use crate::{Selectable, StyledExt as _};
|
||||
@@ -146,6 +147,7 @@ where
|
||||
self.trigger_style = Some(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the content of the popover.
|
||||
///
|
||||
/// The `content` is a closure that returns an `AnyElement`.
|
||||
@@ -244,8 +246,8 @@ pub struct PrepaintState {
|
||||
}
|
||||
|
||||
impl<M: ManagedView> Element for Popover<M> {
|
||||
type RequestLayoutState = PopoverElementState<M>;
|
||||
type PrepaintState = PrepaintState;
|
||||
type RequestLayoutState = PopoverElementState<M>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
|
||||
@@ -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::{
|
||||
actions, anchored, canvas, div, prelude::FluentBuilder, px, rems, Action, AnyElement, App,
|
||||
AppContext, Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, InteractiveElement, IntoElement, KeyBinding, Keystroke, ParentElement, Pixels,
|
||||
Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
||||
WeakEntity, Window,
|
||||
actions, anchored, canvas, div, px, rems, Action, AnyElement, App, AppContext, Bounds, Context,
|
||||
Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
||||
IntoElement, KeyBinding, Keystroke, ParentElement, Pixels, Render, ScrollHandle, SharedString,
|
||||
StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{
|
||||
button::Button,
|
||||
h_flex,
|
||||
list::ListItem,
|
||||
popover::Popover,
|
||||
scroll::{Scrollbar, ScrollbarState},
|
||||
v_flex, Icon, IconName, Selectable, Sizable as _, StyledExt,
|
||||
};
|
||||
use crate::button::Button;
|
||||
use crate::list::ListItem;
|
||||
use crate::popover::Popover;
|
||||
use crate::scroll::{Scrollbar, ScrollbarState};
|
||||
use crate::{h_flex, v_flex, Icon, IconName, Selectable, Sizable as _, StyledExt};
|
||||
|
||||
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::confirm))
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_mouse_down_out(
|
||||
cx.listener(|this, _, window, cx| this.dismiss(&Dismiss, window, cx)),
|
||||
)
|
||||
.on_mouse_down_out(cx.listener(|this, _, window, cx| this.dismiss(&Dismiss, window, cx)))
|
||||
.popover_style(cx)
|
||||
.relative()
|
||||
.p_1()
|
||||
@@ -564,9 +561,7 @@ impl Render for PopupMenu {
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
// Skip last separator
|
||||
.filter(|(ix, item)| {
|
||||
!(*ix == items_count - 1 && item.is_separator())
|
||||
})
|
||||
.filter(|(ix, item)| !(*ix == items_count - 1 && item.is_separator()))
|
||||
.map(|(ix, item)| {
|
||||
let this = ListItem::new(("menu-item", ix))
|
||||
.relative()
|
||||
@@ -575,68 +570,51 @@ impl Render for PopupMenu {
|
||||
.px_2()
|
||||
.rounded_md()
|
||||
.text_sm()
|
||||
.on_mouse_enter(cx.listener(
|
||||
move |this, _, _window, cx| {
|
||||
this.hovered_menu_ix = Some(ix);
|
||||
cx.notify();
|
||||
},
|
||||
));
|
||||
.on_mouse_enter(cx.listener(move |this, _, _window, cx| {
|
||||
this.hovered_menu_ix = Some(ix);
|
||||
cx.notify();
|
||||
}));
|
||||
|
||||
match item {
|
||||
PopupMenuItem::Separator => {
|
||||
this.h_auto().p_0().disabled(true).child(
|
||||
div()
|
||||
.rounded_none()
|
||||
.h(px(1.))
|
||||
.mx_neg_1()
|
||||
.my_0p5()
|
||||
.bg(cx.theme().border_disabled),
|
||||
)
|
||||
}
|
||||
PopupMenuItem::Separator => this.h_auto().p_0().disabled(true).child(
|
||||
div()
|
||||
.rounded_none()
|
||||
.h(px(1.))
|
||||
.mx_neg_1()
|
||||
.my_0p5()
|
||||
.bg(cx.theme().border_disabled),
|
||||
),
|
||||
PopupMenuItem::ElementItem { render, .. } => this
|
||||
.on_click(cx.listener(
|
||||
move |this, _, window, cx| {
|
||||
.on_click(
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.on_click(ix, window, cx)
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.min_h(ITEM_HEIGHT)
|
||||
.items_center()
|
||||
.gap_x_1()
|
||||
.children(Self::render_icon(
|
||||
has_icon, None, window, cx,
|
||||
))
|
||||
.children(Self::render_icon(has_icon, None, window, cx))
|
||||
.child((render)(window, cx)),
|
||||
),
|
||||
PopupMenuItem::Item {
|
||||
icon,
|
||||
label,
|
||||
action,
|
||||
..
|
||||
icon, label, action, ..
|
||||
} => {
|
||||
let action = action
|
||||
.as_ref()
|
||||
.map(|action| action.boxed_clone());
|
||||
let key =
|
||||
Self::render_keybinding(action, window, cx);
|
||||
let action = action.as_ref().map(|action| action.boxed_clone());
|
||||
let key = Self::render_keybinding(action, window, cx);
|
||||
|
||||
this.on_click(cx.listener(
|
||||
move |this, _, window, cx| {
|
||||
this.on_click(
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.on_click(ix, window, cx)
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.h(ITEM_HEIGHT)
|
||||
.items_center()
|
||||
.gap_x_1p5()
|
||||
.children(Self::render_icon(
|
||||
has_icon,
|
||||
icon.clone(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.children(Self::render_icon(has_icon, icon.clone(), window, cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
@@ -649,9 +627,7 @@ impl Render for PopupMenu {
|
||||
)
|
||||
}
|
||||
PopupMenuItem::Submenu { icon, label, menu } => this
|
||||
.when(self.hovered_menu_ix == Some(ix), |this| {
|
||||
this.selected(true)
|
||||
})
|
||||
.when(self.hovered_menu_ix == Some(ix), |this| this.selected(true))
|
||||
.child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
@@ -673,57 +649,44 @@ impl Render for PopupMenu {
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(label.clone())
|
||||
.child(
|
||||
IconName::CaretRight,
|
||||
),
|
||||
.child(IconName::CaretRight),
|
||||
),
|
||||
)
|
||||
.when_some(
|
||||
self.hovered_menu_ix,
|
||||
|this, hovered_ix| {
|
||||
let (anchor, left) =
|
||||
if window.bounds().size.width
|
||||
- bounds.origin.x
|
||||
< max_width
|
||||
{
|
||||
(Corner::TopRight, -px(15.))
|
||||
} else {
|
||||
(
|
||||
Corner::TopLeft,
|
||||
bounds.size.width
|
||||
- px(10.),
|
||||
)
|
||||
};
|
||||
.when_some(self.hovered_menu_ix, |this, hovered_ix| {
|
||||
let (anchor, left) = if window.bounds().size.width
|
||||
- bounds.origin.x
|
||||
< max_width
|
||||
{
|
||||
(Corner::TopRight, -px(15.))
|
||||
} else {
|
||||
(Corner::TopLeft, bounds.size.width - px(10.))
|
||||
};
|
||||
|
||||
let top = if bounds.origin.y
|
||||
+ bounds.size.height
|
||||
> window.bounds().size.height
|
||||
{
|
||||
px(32.)
|
||||
} else {
|
||||
-px(10.)
|
||||
};
|
||||
let top = if bounds.origin.y + bounds.size.height
|
||||
> window.bounds().size.height
|
||||
{
|
||||
px(32.)
|
||||
} else {
|
||||
-px(10.)
|
||||
};
|
||||
|
||||
if hovered_ix == ix {
|
||||
this.child(
|
||||
if hovered_ix == ix {
|
||||
this.child(
|
||||
anchored()
|
||||
.anchor(anchor)
|
||||
.child(
|
||||
div()
|
||||
.occlude()
|
||||
.top(top)
|
||||
.left(left)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.snap_to_window_with_margin(
|
||||
Edges::all(px(8.)),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
},
|
||||
),
|
||||
.anchor(anchor)
|
||||
.child(
|
||||
div()
|
||||
.occlude()
|
||||
.top(top)
|
||||
.left(left)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.snap_to_window_with_margin(Edges::all(px(8.))),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}),
|
||||
),
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -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 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 crate::{h_flex, v_flex, AxisExt};
|
||||
|
||||
@@ -470,8 +472,8 @@ impl IntoElement for ResizePanelGroupElement {
|
||||
}
|
||||
|
||||
impl Element for ResizePanelGroupElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
type RequestLayoutState = ();
|
||||
|
||||
fn id(&self) -> Option<gpui::ElementId> {
|
||||
None
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, App, Axis, Div, ElementId, InteractiveElement,
|
||||
IntoElement, ParentElement as _, Pixels, RenderOnce, Stateful, StatefulInteractiveElement,
|
||||
Styled as _, Window,
|
||||
div, px, App, Axis, Div, ElementId, InteractiveElement, IntoElement, ParentElement as _,
|
||||
Pixels, RenderOnce, Stateful, StatefulInteractiveElement, Styled as _, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
|
||||
@@ -6,12 +6,10 @@ use gpui::{
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{
|
||||
input::InputState,
|
||||
modal::Modal,
|
||||
notification::{Notification, NotificationList},
|
||||
window_border,
|
||||
};
|
||||
use crate::input::InputState;
|
||||
use crate::modal::Modal;
|
||||
use crate::notification::{Notification, NotificationList};
|
||||
use crate::window_border;
|
||||
|
||||
/// Extension trait for [`WindowContext`] and [`ViewContext`] to add drawer functionality.
|
||||
pub trait ContextModal: Sized {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
canvas, div, relative, AnyElement, App, Div, Element, ElementId, EntityId, GlobalElementId,
|
||||
InteractiveElement, IntoElement, ParentElement, Pixels, Position, ScrollHandle, SharedString,
|
||||
Size, Stateful, StatefulInteractiveElement, Style, StyleRefinement, Styled, Window,
|
||||
};
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use super::{Scrollbar, ScrollbarAxis, ScrollbarState};
|
||||
|
||||
@@ -143,8 +145,8 @@ impl<E> Element for Scrollable<E>
|
||||
where
|
||||
E: Element,
|
||||
{
|
||||
type RequestLayoutState = AnyElement;
|
||||
type PrepaintState = ScrollViewState;
|
||||
type RequestLayoutState = AnyElement;
|
||||
|
||||
fn id(&self) -> Option<gpui::ElementId> {
|
||||
Some(self.id.clone())
|
||||
|
||||
@@ -46,8 +46,8 @@ impl IntoElement for ScrollableMask {
|
||||
}
|
||||
|
||||
impl Element for ScrollableMask {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = Hitbox;
|
||||
type RequestLayoutState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::{
|
||||
cell::Cell,
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gpui::{
|
||||
fill, point, px, relative, App, BorderStyle, Bounds, ContentMask, CursorStyle, Edges, Element,
|
||||
@@ -389,9 +387,8 @@ pub struct AxisPrepaintState {
|
||||
}
|
||||
|
||||
impl Element for Scrollbar {
|
||||
type RequestLayoutState = ();
|
||||
|
||||
type PrepaintState = PrepaintState;
|
||||
type RequestLayoutState = ();
|
||||
|
||||
fn id(&self) -> Option<gpui::ElementId> {
|
||||
None
|
||||
@@ -631,7 +628,7 @@ impl Element for Scrollbar {
|
||||
let margin_end = state.margin_end;
|
||||
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| {
|
||||
cx.paint_quad(fill(state.bounds, state.bg));
|
||||
|
||||
@@ -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::{
|
||||
div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, AnyElement, App, Element,
|
||||
ElementId, GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _,
|
||||
SharedString, Styled as _, Window,
|
||||
div, px, Animation, AnimationExt as _, AnyElement, App, Element, ElementId, GlobalElementId,
|
||||
InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, Styled as _,
|
||||
Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -87,9 +90,8 @@ pub struct SwitchState {
|
||||
}
|
||||
|
||||
impl Element for Switch {
|
||||
type RequestLayoutState = AnyElement;
|
||||
|
||||
type PrepaintState = ();
|
||||
type RequestLayoutState = AnyElement;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder, px, AnyElement, App, Div, ElementId, InteractiveElement,
|
||||
IntoElement, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Window,
|
||||
div, px, AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
|
||||
RenderOnce, Stateful, StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use crate::h_flex;
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, AnyElement, App, Div, ElementId, InteractiveElement,
|
||||
IntoElement, ParentElement, RenderOnce, ScrollHandle, StatefulInteractiveElement as _, Styled,
|
||||
Window,
|
||||
div, px, AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
|
||||
RenderOnce, ScrollHandle, StatefulInteractiveElement as _, Styled, Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::h_flex;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct TabBar {
|
||||
base: Div,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
use common::profile::RenderProfile;
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, ElementId, FontWeight, HighlightStyle, InteractiveText, IntoElement,
|
||||
@@ -7,7 +11,6 @@ use linkify::{LinkFinder, LinkKind};
|
||||
use nostr_sdk::prelude::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::{collections::HashMap, ops::Range, sync::Arc};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
static NOSTR_URI_REGEX: Lazy<Regex> =
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
black, div, prelude::FluentBuilder as _, px, relative, white, AnyElement, App, ClickEvent, Div,
|
||||
Element, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels,
|
||||
RenderOnce, Rgba, Stateful, StatefulInteractiveElement as _, Style, Styled, Window,
|
||||
black, div, px, relative, white, AnyElement, App, ClickEvent, Div, Element, Hsla,
|
||||
InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, Rgba,
|
||||
Stateful, StatefulInteractiveElement as _, Style, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -291,8 +292,8 @@ impl IntoElement for TitleBarElement {
|
||||
}
|
||||
|
||||
impl Element for TitleBarElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
type RequestLayoutState = ();
|
||||
|
||||
fn id(&self) -> Option<gpui::ElementId> {
|
||||
None
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
canvas, div, point, prelude::FluentBuilder as _, px, AnyElement, App, Bounds, CursorStyle,
|
||||
Decorations, Edges, HitboxBehavior, Hsla, InteractiveElement as _, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, Point, RenderOnce, ResizeEdge, Size, Styled as _, Window,
|
||||
canvas, div, point, px, AnyElement, App, Bounds, CursorStyle, Decorations, Edges,
|
||||
HitboxBehavior, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels,
|
||||
Point, RenderOnce, ResizeEdge, Size, Styled as _, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -104,7 +105,7 @@ impl RenderOnce for WindowBorder {
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
Some(&hitbox),
|
||||
&hitbox,
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
9
rustfmt.toml
Normal file
9
rustfmt.toml
Normal 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"
|
||||
Reference in New Issue
Block a user