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::>(); // 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}"); diff --git a/crates/coop/src/views/compose.rs b/crates/coop/src/views/compose.rs index 45a5587..03c8689 100644 --- a/crates/coop/src/views/compose.rs +++ b/crates/coop/src/views/compose.rs @@ -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 { cx.new(|cx| Compose::new(window, cx)) @@ -71,10 +68,13 @@ impl Compose { cx.spawn(async move |this, cx| { let task: Task, 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> = 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) { - 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(); diff --git a/crates/coop/src/views/login.rs b/crates/coop/src/views/login.rs index 20c445d..4aee0b1 100644 --- a/crates/coop/src/views/login.rs +++ b/crates/coop/src/views/login.rs @@ -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 { } pub struct Login { - // Inputs key_input: Entity, + relay_input: Entity, + connection_string: Entity, + qr_image: Entity>>, + // Signer that created by Connection String + active_signer: Entity>, + // Error for the key input error: Entity>, is_logging_in: bool, - // Nostr Connect - qr: Entity>>, - connect_relay: Entity, - connect_client: Entity>, - // 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 { - let connect_client: Entity> = 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::( + 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::>(); - 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::>(); + + // 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) { - 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) { + fn save_keys(&self, keys: &Keys, cx: &mut Context) { + 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) { + 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) { + 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, cx: &mut Context) { 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) { @@ -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") diff --git a/crates/coop/src/views/mod.rs b/crates/coop/src/views/mod.rs index b5914f4..6e79d99 100644 --- a/crates/coop/src/views/mod.rs +++ b/crates/coop/src/views/mod.rs @@ -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; diff --git a/crates/coop/src/views/new_account.rs b/crates/coop/src/views/new_account.rs index fc353fa..2e2acfc 100644 --- a/crates/coop/src/views/new_account.rs +++ b/crates/coop/src/views/new_account.rs @@ -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::new(window, cx) @@ -44,8 +41,10 @@ impl NewAccount { fn view(window: &mut Window, cx: &mut Context) -> 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) { + fn submit(&mut self, _window: &mut Window, cx: &mut Context) { 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) { @@ -109,11 +119,10 @@ impl NewAccount { }; if let Ok(file_data) = fs::read(path).await { - let client = get_client(); let (tx, rx) = oneshot::channel::(); 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); } }); diff --git a/crates/coop/src/views/onboarding.rs b/crates/coop/src/views/onboarding.rs index e1b39e3..705da74 100644 --- a/crates/coop/src/views/onboarding.rs +++ b/crates/coop/src/views/onboarding.rs @@ -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; diff --git a/crates/coop/src/views/profile.rs b/crates/coop/src/views/profile.rs index 71d28cd..5064ce2 100644 --- a/crates/coop/src/views/profile.rs +++ b/crates/coop/src/views/profile.rs @@ -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::new(window, cx) @@ -54,10 +55,10 @@ impl Profile { }; let task: Task, 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::(); 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> = 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(()) }); diff --git a/crates/coop/src/views/relays.rs b/crates/coop/src/views/relays.rs index ad0ba2e..e9855ec 100644 --- a/crates/coop/src/views/relays.rs +++ b/crates/coop/src/views/relays.rs @@ -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, 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> = 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() diff --git a/crates/coop/src/views/sidebar/element.rs b/crates/coop/src/views/sidebar/element.rs index 8cda3dd..b82fab3 100644 --- a/crates/coop/src/views/sidebar/element.rs +++ b/crates/coop/src/views/sidebar/element.rs @@ -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 { diff --git a/crates/coop/src/views/sidebar/mod.rs b/crates/coop/src/views/sidebar/mod.rs index 9e7705b..ceddf6a 100644 --- a/crates/coop/src/views/sidebar/mod.rs +++ b/crates/coop/src/views/sidebar/mod.rs @@ -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::(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( diff --git a/crates/coop/src/views/startup.rs b/crates/coop/src/views/startup.rs new file mode 100644 index 0000000..33c016d --- /dev/null +++ b/crates/coop/src/views/startup.rs @@ -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::new(window, cx) +} + +pub struct Startup { + name: SharedString, + focus_handle: FocusHandle, +} + +impl Startup { + fn new(_window: &mut Window, cx: &mut App) -> Entity { + 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