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

* refactor

* update

* .

* rustfmt

* .

* .

* .

* .

* .

* add document

* .

* add logout

* handle error

* chore: update gpui

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

189
Cargo.lock generated
View File

@@ -2,21 +2,6 @@
# It is not intended for manual editing.
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",
]

View File

@@ -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
View File

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

After

Width:  |  Height:  |  Size: 302 B

View File

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

After

Width:  |  Height:  |  Size: 855 B

View File

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

View File

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

View File

@@ -1,18 +1,15 @@
use std::{
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")?;

View File

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

View File

@@ -1,10 +1,11 @@
use std::{cmp::Reverse, collections::BTreeSet};
use std::cmp::Reverse;
use std::collections::BTreeSet;
use account::Account;
use anyhow::Error;
use 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);
}

View File

@@ -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;

View File

@@ -1,18 +1,17 @@
use std::{cmp::Ordering, sync::Arc};
use std::cmp::Ordering;
use std::sync::Arc;
use account::Account;
use anyhow::{anyhow, Error};
use 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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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();
})),
),
),
)

View File

@@ -1,17 +1,14 @@
use anyhow::{anyhow, Error};
use std::sync::Arc;
use std::time::Duration;
use anyhow::Error;
use asset::Assets;
use 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())
}
}

View File

@@ -1,19 +1,21 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use account::Account;
use async_utility::task::spawn;
use 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}");

View File

@@ -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();

View File

@@ -1,24 +1,27 @@
use std::{sync::Arc, time::Duration};
use std::sync::Arc;
use std::time::Duration;
use account::Account;
use common::string_to_qr;
use 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")

View File

@@ -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;

View File

@@ -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);
}
});

View File

@@ -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;

View File

@@ -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(())
});

View File

@@ -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()

View File

@@ -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 {

View File

@@ -1,33 +1,32 @@
use std::{collections::BTreeSet, ops::Range, time::Duration};
use std::collections::BTreeSet;
use std::ops::Range;
use std::time::Duration;
use account::Account;
use async_utility::task::spawn;
use 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(

View File

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

View File

@@ -4,11 +4,9 @@ use gpui::{
ParentElement, Render, Styled, Window,
};
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,

View File

@@ -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)

View File

@@ -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"

View File

@@ -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",
];

View File

@@ -1,110 +1,598 @@
//! Global state management for the Nostr client application.
//!
//! This module provides a singleton global state that manages:
//! - Nostr client connections and event handling
//! - User identity and profile management
//! - Batched metadata fetching for performance
//! - Cross-component communication via channels
use std::collections::{BTreeMap, BTreeSet};
use std::mem;
use std::sync::OnceLock;
use std::time::Duration;
use anyhow::{anyhow, Error};
use constants::{
ALL_MESSAGES_SUB_ID, APP_ID, APP_PUBKEY, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT,
METADATA_BATCH_TIMEOUT, NEW_MESSAGE_SUB_ID, SEARCH_RELAYS,
};
use nostr_keyring::prelude::*;
use nostr_sdk::prelude::*;
use 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);
}

View File

@@ -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![

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -1,11 +1,15 @@
use crate::popup_menu::PopupMenu;
use std::cell::RefCell;
use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
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())

View File

@@ -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;

View File

@@ -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

View File

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

View File

@@ -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);

View File

@@ -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>>,

View File

@@ -1,24 +1,24 @@
use gpui::{
div, prelude::FluentBuilder, px, rems, App, AppContext, Context, Corner, DefiniteLength,
DismissEvent, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, Render, ScrollHandle,
SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
};
use std::sync::Arc;
use 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 {

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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",

View File

@@ -1,10 +1,13 @@
use crate::{Icon, IconName, Sizable, Size};
use gpui::{
div, ease_in_out, percentage, prelude::FluentBuilder as _, Animation, AnimationExt as _, App,
Hsla, IntoElement, ParentElement, RenderOnce, Styled as _, Transformation, Window,
};
use std::time::Duration;
use 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,

View File

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

View File

@@ -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 {

View File

@@ -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

View File

@@ -1,25 +1,28 @@
use std::cell::Cell;
use std::ops::Range;
use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
actions, div, impl_internal_actions, point, px, App, AppContext, Bounds, ClipboardItem,
Context, DefiniteLength, Entity, EntityInputHandler, EventEmitter, FocusHandle, Focusable,
InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, ScrollHandle,
ScrollWheelEvent, SharedString, Styled as _, Subscription, UTF16Selection, Window, WrappedLine,
};
use serde::Deserialize;
use 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 {

View File

@@ -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 {

View File

@@ -1,21 +1,22 @@
use std::{cell::Cell, rc::Rc, time::Duration};
use std::cell::Cell;
use std::rc::Rc;
use std::time::Duration;
use gpui::prelude::FluentBuilder;
use gpui::{
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");

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,18 +1,17 @@
use std::{rc::Rc, time::Duration};
use std::rc::Rc;
use std::time::Duration;
use gpui::prelude::FluentBuilder;
use gpui::{
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]);

View File

@@ -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,

View File

@@ -1,12 +1,13 @@
use std::{cell::RefCell, rc::Rc};
use std::cell::RefCell;
use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
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())

View File

@@ -1,22 +1,21 @@
use std::{cell::Cell, ops::Deref, rc::Rc};
use std::cell::Cell;
use std::ops::Deref;
use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
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
}
}),
),
}
}),

View File

@@ -1,11 +1,13 @@
use gpui::{
canvas, div, prelude::FluentBuilder, px, relative, Along, AnyElement, AnyView, App, AppContext,
Axis, Bounds, Context, Element, Entity, EntityId, EventEmitter, IntoElement, IsZero,
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Render, StatefulInteractiveElement as _,
Style, Styled, WeakEntity, Window,
};
use std::rc::Rc;
use 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

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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())

View File

@@ -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

View File

@@ -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));

View File

@@ -1,9 +1,12 @@
use std::{cell::RefCell, rc::Rc, time::Duration};
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
use gpui::prelude::FluentBuilder as _;
use gpui::{
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())

View File

@@ -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;

View File

@@ -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,

View File

@@ -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> =

View File

@@ -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

View File

@@ -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
View File

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