diff --git a/Cargo.lock b/Cargo.lock
index 0f9cb05..0af5249 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
]
diff --git a/Cargo.toml b/Cargo.toml
index 5feb624..d235919 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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" }
diff --git a/assets/icons/logout.svg b/assets/icons/logout.svg
new file mode 100644
index 0000000..e7beade
--- /dev/null
+++ b/assets/icons/logout.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg
new file mode 100644
index 0000000..059f4a8
--- /dev/null
+++ b/assets/icons/settings.svg
@@ -0,0 +1,4 @@
+
diff --git a/crates/account/Cargo.toml b/crates/account/Cargo.toml
deleted file mode 100644
index 2630f3f..0000000
--- a/crates/account/Cargo.toml
+++ /dev/null
@@ -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
diff --git a/crates/account/src/lib.rs b/crates/account/src/lib.rs
deleted file mode 100644
index f62ec37..0000000
--- a/crates/account/src/lib.rs
+++ /dev/null
@@ -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);
-
-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,
-}
-
-impl Account {
- pub fn global(cx: &App) -> Entity {
- cx.global::().0.clone()
- }
-
- pub fn get_global(cx: &App) -> &Self {
- cx.global::().0.read(cx)
- }
-
- pub fn set_global(account: Entity, cx: &mut App) {
- cx.set_global(GlobalAccount(account));
- }
-
- /// Login to the account using the given signer.
- pub fn login(&mut self, signer: S, window: &mut Window, cx: &mut Context)
- where
- S: NostrSigner + 'static,
- {
- let task: Task> = 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) {
- 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> = 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 = 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 = 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.profile = Some(profile);
- cx.notify();
- }
-
- /// Subscribes to the current account's metadata.
- pub fn subscribe(&self, cx: &mut Context) {
- 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> = 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();
- }
-}
diff --git a/crates/auto_update/src/lib.rs b/crates/auto_update/src/lib.rs
index 44966f3..533fa35 100644
--- a/crates/auto_update/src/lib.rs
+++ b/crates/auto_update/src/lib.rs
@@ -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);
@@ -129,10 +126,9 @@ impl AutoUpdater {
self.set_status(AutoUpdateStatus::Downloading, cx);
let task: Task> = 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")?;
diff --git a/crates/chats/Cargo.toml b/crates/chats/Cargo.toml
index 260e097..4e1bf5d 100644
--- a/crates/chats/Cargo.toml
+++ b/crates/chats/Cargo.toml
@@ -5,7 +5,6 @@ edition.workspace = true
publish.workspace = true
[dependencies]
-account = { path = "../account" }
common = { path = "../common" }
global = { path = "../global" }
diff --git a/crates/chats/src/lib.rs b/crates/chats/src/lib.rs
index 0fcc9af..787f388 100644
--- a/crates/chats/src/lib.rs
+++ b/crates/chats/src/lib.rs
@@ -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, cx: &mut App) {
+ pub(crate) fn set_global(state: Entity, 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) {
- // 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, 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) {
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);
}
diff --git a/crates/chats/src/message.rs b/crates/chats/src/message.rs
index e3c9125..272ee42 100644
--- a/crates/chats/src/message.rs
+++ b/crates/chats/src/message.rs
@@ -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;
diff --git a/crates/chats/src/room.rs b/crates/chats/src/room.rs
index c2b688d..3e62f9b 100644
--- a/crates/chats/src/room.rs
+++ b/crates/chats/src/room.rs
@@ -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::>()
.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::>();
let mut name = profiles
@@ -325,14 +323,18 @@ impl Room {
/// A Task that resolves to Result)>, Error>
#[allow(clippy::type_complexity)]
pub fn load_metadata(&self, cx: &mut Context) -> Task> {
- 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, Error> where
/// the boolean indicates if the member has inbox relays configured
pub fn messaging_relays(&self, cx: &App) -> Task, 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, Error> containing
/// all messages for this room
pub fn load_messages(&self, cx: &App) -> Task, 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::>();
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) {
- 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>,
- cx: &App,
+ _cx: &App,
) -> Option {
- 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 = 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 {
.collect::>();
for pubkey in pubkey_tokens.into_iter() {
- mentions.push(get_cache_profile(&pubkey));
+ mentions.push(shared_state().person(&pubkey));
}
mentions
diff --git a/crates/common/src/debounced_delay.rs b/crates/common/src/debounced_delay.rs
index 03281d1..2b9d301 100644
--- a/crates/common/src/debounced_delay.rs
+++ b/crates/common/src/debounced_delay.rs
@@ -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 {
task: Option>,
diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs
index 6979d56..f3a6738 100644
--- a/crates/common/src/lib.rs
+++ b/crates/common/src/lib.rs
@@ -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, 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> {
+ 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(a: &[T], b: &[T]) -> bool
diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml
index 6e0919b..985799c 100644
--- a/crates/coop/Cargo.toml
+++ b/crates/coop/Cargo.toml
@@ -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
diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs
index 0467cc5..8112562 100644
--- a/crates/coop/src/chatspace.rs
+++ b/crates/coop/src/chatspace.rs
@@ -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,
+ titlebar: bool,
#[allow(unused)]
- subscriptions: SmallVec<[Subscription; 3]>,
+ subscriptions: SmallVec<[Subscription; 2]>,
}
impl ChatSpace {
pub fn new(window: &mut Window, cx: &mut App) -> Entity {
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::(|this, window, cx| {
+ // Automatically load messages when chat panel opens
+ subscriptions.push(cx.observe_new::(|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.titlebar = true;
- cx.notify();
- }
+ pub fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context) {
+ // Disable the titlebar
+ self.titlebar(false, cx);
- fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context) {
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.show_titlebar(cx);
+ pub fn open_chats(&mut self, window: &mut Window, cx: &mut Context) {
+ // 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.titlebar = status;
+ cx.notify();
+ }
+
fn verify_messaging_relays(&self, cx: &App) -> Task> {
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();
+ })),
),
),
)
diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs
index 8bfa79b..481def8 100644
--- a/crates/coop/src/main.rs
+++ b/crates/coop/src/main.rs
@@ -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::(2048);
- let (batch_tx, batch_rx) = smol::channel::bounded::>(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 = 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 = 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::>();
-
- 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) -> Result {
+ 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 {
- 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,
- 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())
}
}
diff --git a/crates/coop/src/views/chat.rs b/crates/coop/src/views/chat.rs
index d56879a..32c556e 100644
--- a/crates/coop/src/views/chat.rs
+++ b/crates/coop/src/views/chat.rs
@@ -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) -> 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::