chore: clean up codebase (#186)

* refactor app state

* clean up

* clean up

* .
This commit is contained in:
reya
2025-10-18 09:46:45 +07:00
committed by GitHub
parent 32a0401907
commit a1e0934fc3
37 changed files with 516 additions and 1716 deletions

194
Cargo.lock generated
View File

@@ -82,21 +82,6 @@ version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "app_state"
version = "0.2.11"
dependencies = [
"anyhow",
"dirs 5.0.1",
"flume",
"log",
"nostr-lmdb",
"nostr-sdk",
"rustls",
"smol",
"whoami",
]
[[package]] [[package]]
name = "arbitrary" name = "arbitrary"
version = "1.4.2" version = "1.4.2"
@@ -507,7 +492,6 @@ name = "auto_update"
version = "0.2.11" version = "0.2.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app_state",
"cargo-packager-updater", "cargo-packager-updater",
"common", "common",
"gpui", "gpui",
@@ -515,6 +499,7 @@ dependencies = [
"nostr-sdk", "nostr-sdk",
"smallvec", "smallvec",
"smol", "smol",
"states",
] ]
[[package]] [[package]]
@@ -1094,11 +1079,11 @@ name = "client_keys"
version = "0.2.11" version = "0.2.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app_state",
"gpui", "gpui",
"log", "log",
"nostr-sdk", "nostr-sdk",
"smallvec", "smallvec",
"states",
] ]
[[package]] [[package]]
@@ -1128,14 +1113,14 @@ dependencies = [
[[package]] [[package]]
name = "cocoa" name = "cocoa"
version = "0.26.1" version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.9.4",
"block", "block",
"cocoa-foundation 0.2.1", "cocoa-foundation 0.2.0",
"core-foundation 0.10.1", "core-foundation 0.10.0",
"core-graphics 0.24.0", "core-graphics 0.24.0",
"foreign-types 0.5.0", "foreign-types 0.5.0",
"libc", "libc",
@@ -1158,14 +1143,15 @@ dependencies = [
[[package]] [[package]]
name = "cocoa-foundation" name = "cocoa-foundation"
version = "0.2.1" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.9.4",
"block", "block",
"core-foundation 0.10.1", "core-foundation 0.10.0",
"core-graphics-types 0.2.0", "core-graphics-types 0.2.0",
"libc",
"objc", "objc",
] ]
@@ -1183,11 +1169,10 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"workspace-hack",
] ]
[[package]] [[package]]
@@ -1221,7 +1206,6 @@ name = "common"
version = "0.2.11" version = "0.2.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app_state",
"chrono", "chrono",
"futures", "futures",
"gpui", "gpui",
@@ -1234,6 +1218,7 @@ dependencies = [
"reqwest", "reqwest",
"smallvec", "smallvec",
"smol", "smol",
"states",
"webbrowser", "webbrowser",
] ]
@@ -1295,7 +1280,6 @@ name = "coop"
version = "0.2.11" version = "0.2.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app_state",
"assets", "assets",
"auto_update", "auto_update",
"client_keys", "client_keys",
@@ -1319,9 +1303,9 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"settings", "settings",
"signer_proxy",
"smallvec", "smallvec",
"smol", "smol",
"states",
"theme", "theme",
"title_bar", "title_bar",
"tracing-subscriber", "tracing-subscriber",
@@ -1341,9 +1325,9 @@ dependencies = [
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.10.1" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -1375,7 +1359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.9.4",
"core-foundation 0.10.1", "core-foundation 0.10.0",
"core-graphics-types 0.2.0", "core-graphics-types 0.2.0",
"foreign-types 0.5.0", "foreign-types 0.5.0",
"libc", "libc",
@@ -1412,7 +1396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.9.4",
"core-foundation 0.10.1", "core-foundation 0.10.0",
"libc", "libc",
] ]
@@ -1425,7 +1409,7 @@ dependencies = [
"bitflags 2.9.4", "bitflags 2.9.4",
"block", "block",
"cfg-if", "cfg-if",
"core-foundation 0.10.1", "core-foundation 0.10.0",
"libc", "libc",
] ]
@@ -1435,7 +1419,7 @@ version = "21.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130"
dependencies = [ dependencies = [
"core-foundation 0.10.1", "core-foundation 0.10.0",
"core-graphics 0.24.0", "core-graphics 0.24.0",
"foreign-types 0.5.0", "foreign-types 0.5.0",
"libc", "libc",
@@ -1448,7 +1432,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45e71d5be22206bed53c3c3cb99315fc4c3d31b8963808c6bc4538168c4f8ef" checksum = "d45e71d5be22206bed53c3c3cb99315fc4c3d31b8963808c6bc4538168c4f8ef"
dependencies = [ dependencies = [
"block", "block",
"core-foundation 0.10.1", "core-foundation 0.10.0",
"core-graphics2", "core-graphics2",
"io-surface", "io-surface",
"libc", "libc",
@@ -1625,12 +1609,11 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.106",
"workspace-hack",
] ]
[[package]] [[package]]
@@ -2003,9 +1986,9 @@ dependencies = [
[[package]] [[package]]
name = "file-locker" name = "file-locker"
version = "1.1.3" version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6c3e69656680c6c3d76750b46dfa64bf07626bd2130c540d6cf2d306ba595a8" checksum = "75ae8b5984a4863d8a32109a848d038bd6d914f20f010cc141375f7a183c41cf"
dependencies = [ dependencies = [
"nix 0.29.0", "nix 0.29.0",
] ]
@@ -2523,7 +2506,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@@ -2538,9 +2521,10 @@ dependencies = [
"calloop", "calloop",
"calloop-wayland-source", "calloop-wayland-source",
"cbindgen", "cbindgen",
"cocoa 0.26.1", "cocoa 0.26.0",
"cocoa-foundation 0.2.0",
"collections", "collections",
"core-foundation 0.10.1", "core-foundation 0.10.0",
"core-foundation-sys", "core-foundation-sys",
"core-graphics 0.24.0", "core-graphics 0.24.0",
"core-text", "core-text",
@@ -2572,6 +2556,7 @@ dependencies = [
"parking", "parking",
"parking_lot", "parking_lot",
"pathfinder_geometry", "pathfinder_geometry",
"pin-project",
"postage", "postage",
"profiling", "profiling",
"rand 0.9.2", "rand 0.9.2",
@@ -2605,7 +2590,6 @@ dependencies = [
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-numerics", "windows-numerics",
"windows-registry 0.5.3", "windows-registry 0.5.3",
"workspace-hack",
"x11-clipboard", "x11-clipboard",
"x11rb", "x11rb",
"xkbcommon", "xkbcommon",
@@ -2617,25 +2601,23 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.106",
"workspace-hack",
] ]
[[package]] [[package]]
name = "gpui_tokio" name = "gpui_tokio"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
"tokio", "tokio",
"util", "util",
"workspace-hack",
] ]
[[package]] [[package]]
@@ -2859,7 +2841,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-compression", "async-compression",
@@ -2878,18 +2860,16 @@ dependencies = [
"tempfile", "tempfile",
"url", "url",
"util", "util",
"workspace-hack",
"zed-reqwest", "zed-reqwest",
] ]
[[package]] [[package]]
name = "http_client_tls" name = "http_client_tls"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
"workspace-hack",
] ]
[[package]] [[package]]
@@ -2898,12 +2878,6 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.7.0" version = "1.7.0"
@@ -2918,7 +2892,6 @@ dependencies = [
"http", "http",
"http-body", "http-body",
"httparse", "httparse",
"httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
@@ -3188,9 +3161,9 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.11.4" version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.16.0", "hashbrown 0.16.0",
@@ -3256,7 +3229,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e"
dependencies = [ dependencies = [
"cgl", "cgl",
"core-foundation 0.10.1", "core-foundation 0.10.0",
"core-foundation-sys", "core-foundation-sys",
"leaky-cow", "leaky-cow",
] ]
@@ -3687,17 +3660,16 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen 0.71.1", "bindgen 0.71.1",
"core-foundation 0.10.1", "core-foundation 0.10.0",
"core-video", "core-video",
"ctor 0.4.3", "ctor 0.4.3",
"foreign-types 0.5.0", "foreign-types 0.5.0",
"metal", "metal",
"objc", "objc",
"workspace-hack",
] ]
[[package]] [[package]]
@@ -3794,13 +3766,13 @@ checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.4" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -4521,12 +4493,11 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "perf" name = "perf"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"collections", "collections",
"serde", "serde",
"serde_json", "serde_json",
"workspace-hack",
] ]
[[package]] [[package]]
@@ -5138,10 +5109,9 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
"workspace-hack",
] ]
[[package]] [[package]]
@@ -5178,17 +5148,20 @@ name = "registry"
version = "0.2.11" version = "0.2.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app_state",
"common", "common",
"flume",
"fuzzy-matcher", "fuzzy-matcher",
"gpui", "gpui",
"itertools 0.13.0", "itertools 0.13.0",
"log", "log",
"nostr", "nostr",
"nostr-lmdb",
"nostr-sdk", "nostr-sdk",
"rustls",
"settings", "settings",
"smallvec", "smallvec",
"smol", "smol",
"states",
] ]
[[package]] [[package]]
@@ -5243,7 +5216,7 @@ dependencies = [
[[package]] [[package]]
name = "reqwest_client" name = "reqwest_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -5254,7 +5227,6 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"tokio", "tokio",
"workspace-hack",
"zed-reqwest", "zed-reqwest",
] ]
@@ -5298,7 +5270,7 @@ dependencies = [
[[package]] [[package]]
name = "rope" name = "rope"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -5307,7 +5279,6 @@ dependencies = [
"sum_tree", "sum_tree",
"unicode-segmentation", "unicode-segmentation",
"util", "util",
"workspace-hack",
] ]
[[package]] [[package]]
@@ -5464,9 +5435,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.32" version = "0.23.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"log", "log",
@@ -5515,7 +5486,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1"
dependencies = [ dependencies = [
"core-foundation 0.10.1", "core-foundation 0.10.0",
"core-foundation-sys", "core-foundation-sys",
"jni", "jni",
"log", "log",
@@ -5741,7 +5712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.9.4",
"core-foundation 0.10.1", "core-foundation 0.10.0",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
"security-framework-sys", "security-framework-sys",
@@ -5766,11 +5737,10 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
"workspace-hack",
] ]
[[package]] [[package]]
@@ -5915,7 +5885,6 @@ name = "settings"
version = "0.2.11" version = "0.2.11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app_state",
"gpui", "gpui",
"log", "log",
"nostr-sdk", "nostr-sdk",
@@ -5923,6 +5892,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"smallvec", "smallvec",
"states",
] ]
[[package]] [[package]]
@@ -5977,28 +5947,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "signer_proxy"
version = "0.2.11"
dependencies = [
"anyhow",
"app_state",
"atomic-destructor",
"bytes",
"futures",
"http-body-util",
"hyper",
"hyper-util",
"log",
"nostr",
"oneshot",
"serde",
"serde_json",
"smallvec",
"smol",
"uuid",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.7" version = "0.3.7"
@@ -6151,6 +6099,21 @@ dependencies = [
"syn 2.0.106", "syn 2.0.106",
] ]
[[package]]
name = "states"
version = "0.2.11"
dependencies = [
"anyhow",
"dirs 5.0.1",
"flume",
"log",
"nostr-lmdb",
"nostr-sdk",
"rustls",
"smol",
"whoami",
]
[[package]] [[package]]
name = "static_assertions" name = "static_assertions"
version = "1.1.0" version = "1.1.0"
@@ -6218,12 +6181,11 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
"rayon", "rayon",
"workspace-hack",
] ]
[[package]] [[package]]
@@ -7255,7 +7217,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",
@@ -7285,18 +7247,16 @@ dependencies = [
"unicase", "unicase",
"walkdir", "walkdir",
"which", "which",
"workspace-hack",
] ]
[[package]] [[package]]
name = "util_macros" name = "util_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#908ae95cf86930893140bee37cf37c8918ac90e8" source = "git+https://github.com/zed-industries/zed#3d6722be9abf94af5680a3323b69a452154c55b2"
dependencies = [ dependencies = [
"perf", "perf",
"quote", "quote",
"syn 2.0.106", "syn 2.0.106",
"workspace-hack",
] ]
[[package]] [[package]]
@@ -7657,7 +7617,7 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97"
dependencies = [ dependencies = [
"core-foundation 0.10.1", "core-foundation 0.10.0",
"jni", "jni",
"log", "log",
"ndk-context", "ndk-context",
@@ -8345,12 +8305,6 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "workspace-hack"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beffa227304dbaea3ad6a06ac674f9bc83a3dec3b7f63eeb442de37e7cb6bb01"
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.1" version = "0.6.1"
@@ -8583,7 +8537,7 @@ source = "git+https://github.com/zed-industries/font-kit?rev=110523127440aefb11c
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.9.4",
"byteorder", "byteorder",
"core-foundation 0.10.1", "core-foundation 0.10.0",
"core-graphics 0.24.0", "core-graphics 0.24.0",
"core-text", "core-text",
"dirs 5.0.1", "dirs 5.0.1",

View File

@@ -6,7 +6,7 @@ publish.workspace = true
[dependencies] [dependencies]
common = { path = "../common" } common = { path = "../common" }
app_state = { path = "../app_state" } states = { path = "../states" }
gpui.workspace = true gpui.workspace = true
nostr-sdk.workspace = true nostr-sdk.workspace = true

View File

@@ -1,10 +1,10 @@
use anyhow::Error; use anyhow::Error;
use app_state::constants::{APP_PUBKEY, APP_UPDATER_ENDPOINT};
use cargo_packager_updater::semver::Version; use cargo_packager_updater::semver::Version;
use cargo_packager_updater::{check_update, Config, Update}; use cargo_packager_updater::{check_update, Config, Update};
use gpui::http_client::Url; use gpui::http_client::Url;
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::constants::{APP_PUBKEY, APP_UPDATER_ENDPOINT};
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
AutoUpdater::set_global(cx.new(AutoUpdater::new), cx); AutoUpdater::set_global(cx.new(AutoUpdater::new), cx);

View File

@@ -5,7 +5,7 @@ edition.workspace = true
publish.workspace = true publish.workspace = true
[dependencies] [dependencies]
app_state = { path = "../app_state" } states = { path = "../states" }
nostr-sdk.workspace = true nostr-sdk.workspace = true
gpui.workspace = true gpui.workspace = true

View File

@@ -1,10 +1,8 @@
use std::sync::atomic::Ordering;
use app_state::app_state;
use app_state::constants::KEYRING_URL;
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Window}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Window};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::constants::KEYRING_URL;
use states::paths::config_dir;
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
ClientKeys::set_global(cx.new(ClientKeys::new), cx); ClientKeys::set_global(cx.new(ClientKeys::new), cx);
@@ -61,7 +59,6 @@ impl ClientKeys {
return; return;
} }
let app_state = app_state();
let read_client_keys = cx.read_credentials(KEYRING_URL); let read_client_keys = cx.read_credentials(KEYRING_URL);
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
@@ -76,7 +73,7 @@ impl ClientKeys {
this.set_keys(Some(keys), false, true, cx); this.set_keys(Some(keys), false, true, cx);
}) })
.ok(); .ok();
} else if app_state.is_first_run.load(Ordering::Acquire) { } else if Self::first_run() {
// If this is the first run, generate new keys and use them for the client keys // If this is the first run, generate new keys and use them for the client keys
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.new_keys(cx); this.new_keys(cx);
@@ -139,4 +136,9 @@ impl ClientKeys {
pub fn has_keys(&self) -> bool { pub fn has_keys(&self) -> bool {
self.keys.is_some() self.keys.is_some()
} }
fn first_run() -> bool {
let flag = config_dir().join(".first_run");
!flag.exists() && std::fs::write(&flag, "").is_ok()
}
} }

View File

@@ -5,7 +5,7 @@ edition.workspace = true
publish.workspace = true publish.workspace = true
[dependencies] [dependencies]
app_state = { path = "../app_state" } states = { path = "../states" }
gpui.workspace = true gpui.workspace = true
nostr-connect.workspace = true nostr-connect.workspace = true

View File

@@ -1,12 +1,12 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use app_state::constants::IMAGE_RESIZE_SERVICE;
use chrono::{Local, TimeZone}; use chrono::{Local, TimeZone};
use gpui::{Image, ImageFormat, SharedString, SharedUri}; use gpui::{Image, ImageFormat, SharedString, SharedUri};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use qrcode::render::svg; use qrcode::render::svg;
use qrcode::QrCode; use qrcode::QrCode;
use states::constants::IMAGE_RESIZE_SERVICE;
const NOW: &str = "now"; const NOW: &str = "now";
const SECONDS_IN_MINUTE: i64 = 60; const SECONDS_IN_MINUTE: i64 = 60;

View File

@@ -32,12 +32,11 @@ ui = { path = "../ui" }
title_bar = { path = "../title_bar" } title_bar = { path = "../title_bar" }
theme = { path = "../theme" } theme = { path = "../theme" }
common = { path = "../common" } common = { path = "../common" }
app_state = { path = "../app_state" } states = { path = "../states" }
registry = { path = "../registry" } registry = { path = "../registry" }
settings = { path = "../settings" } settings = { path = "../settings" }
client_keys = { path = "../client_keys" } client_keys = { path = "../client_keys" }
auto_update = { path = "../auto_update" } auto_update = { path = "../auto_update" }
signer_proxy = { path = "../signer_proxy" }
rust-i18n.workspace = true rust-i18n.workspace = true
i18n.workspace = true i18n.workspace = true

View File

@@ -1,13 +1,8 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use app_state::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH};
use app_state::state::{AuthRequest, SignalKind, UnwrappingStatus};
use app_state::{app_state, default_nip17_relays, default_nip65_relays, nostr_client};
use auto_update::AutoUpdater; use auto_update::AutoUpdater;
use client_keys::ClientKeys; use client_keys::ClientKeys;
use common::display::RenderedProfile; use common::display::RenderedProfile;
@@ -24,8 +19,10 @@ use nostr_connect::prelude::*;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::{Registry, RegistryEvent}; use registry::{Registry, RegistryEvent};
use settings::AppSettings; use settings::AppSettings;
use signer_proxy::{BrowserSignerProxy, BrowserSignerProxyOptions};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH};
use states::state::{AuthRequest, SignalKind, UnwrappingStatus};
use states::{app_state, default_nip17_relays, default_nip65_relays};
use theme::{ActiveTheme, Theme, ThemeMode}; use theme::{ActiveTheme, Theme, ThemeMode};
use title_bar::TitleBar; use title_bar::TitleBar;
use ui::actions::{CopyPublicKey, OpenPublicKey}; use ui::actions::{CopyPublicKey, OpenPublicKey};
@@ -121,21 +118,15 @@ impl ChatSpace {
let status = status.read(cx); let status = status.read(cx);
let all_panels = this.get_all_panel_ids(cx); let all_panels = this.get_all_panel_ids(cx);
match status { if matches!(
UnwrappingStatus::Processing => { status,
registry.update(cx, |this, cx| { UnwrappingStatus::Processing | UnwrappingStatus::Complete
this.load_rooms(window, cx); ) {
this.refresh_rooms(all_panels, cx); registry.update(cx, |this, cx| {
}); this.load_rooms(window, cx);
} this.refresh_rooms(all_panels, cx);
UnwrappingStatus::Complete => { });
registry.update(cx, |this, cx| { }
this.load_rooms(window, cx);
this.refresh_rooms(all_panels, cx);
});
}
_ => {}
};
}), }),
); );
@@ -184,14 +175,14 @@ impl ChatSpace {
// Wait for the signer to be set // Wait for the signer to be set
// Also verify NIP-65 and NIP-17 relays after the signer is set // Also verify NIP-65 and NIP-17 relays after the signer is set
cx.background_spawn(async move { cx.background_spawn(async move {
Self::observe_signer().await; app_state().observe_signer().await;
}), }),
); );
tasks.push( tasks.push(
// Observe gift wrap process in the background // Observe gift wrap process in the background
cx.background_spawn(async move { cx.background_spawn(async move {
Self::observe_giftwrap().await; app_state().observe_giftwrap().await;
}), }),
); );
@@ -213,90 +204,19 @@ impl ChatSpace {
} }
} }
async fn observe_signer() {
let client = nostr_client();
let app_state = app_state();
let loop_duration = Duration::from_millis(800);
loop {
if let Ok(signer) = client.signer().await {
if let Ok(pk) = signer.get_public_key().await {
// Notify the app that the signer has been set
app_state.signal.send(SignalKind::SignerSet(pk)).await;
// Get user's gossip relays
app_state.get_nip65(pk).await.ok();
// Exit the current loop
break;
}
}
smol::Timer::after(loop_duration).await;
}
}
async fn observe_giftwrap() {
let client = nostr_client();
let app_state = app_state();
let loop_duration = Duration::from_secs(20);
let mut is_start_processing = false;
let mut total_loops = 0;
loop {
if client.has_signer().await {
total_loops += 1;
if app_state.gift_wrap_processing.load(Ordering::Acquire) {
is_start_processing = true;
// Reset gift wrap processing flag
let _ = app_state.gift_wrap_processing.compare_exchange(
true,
false,
Ordering::Release,
Ordering::Relaxed,
);
let signal = SignalKind::GiftWrapStatus(UnwrappingStatus::Processing);
app_state.signal.send(signal).await;
} else {
// Only run further if we are already processing
// Wait until after 2 loops to prevent exiting early while events are still being processed
if is_start_processing && total_loops >= 2 {
let signal = SignalKind::GiftWrapStatus(UnwrappingStatus::Complete);
app_state.signal.send(signal).await;
// Reset the counter
is_start_processing = false;
total_loops = 0;
}
}
}
smol::Timer::after(loop_duration).await;
}
}
async fn handle_signals(view: WeakEntity<ChatSpace>, cx: &mut AsyncWindowContext) { async fn handle_signals(view: WeakEntity<ChatSpace>, cx: &mut AsyncWindowContext) {
let app_state = app_state(); let states = app_state();
let mut is_open_proxy_modal = false;
while let Ok(signal) = app_state.signal.receiver().recv_async().await { while let Ok(signal) = states.signal().receiver().recv_async().await {
cx.update(|window, cx| { view.update_in(cx, |this, window, cx| {
let registry = Registry::global(cx); let registry = Registry::global(cx);
let settings = AppSettings::global(cx); let settings = AppSettings::global(cx);
match signal { match signal {
SignalKind::SignerSet(public_key) => { SignalKind::SignerSet(public_key) => {
// Close the latest modal if it exists
window.close_modal(cx); window.close_modal(cx);
// Setup the default layout for current workspace
view.update(cx, |this, cx| {
this.set_default_layout(window, cx);
})
.ok();
// Load user's settings // Load user's settings
settings.update(cx, |this, cx| { settings.update(cx, |this, cx| {
this.load_settings(cx); this.load_settings(cx);
@@ -307,45 +227,33 @@ impl ChatSpace {
this.set_signer_pubkey(public_key, cx); this.set_signer_pubkey(public_key, cx);
this.load_rooms(window, cx); this.load_rooms(window, cx);
}); });
// Setup the default layout for current workspace
this.set_default_layout(window, cx);
} }
SignalKind::SignerUnset => { SignalKind::SignerUnset => {
// Setup the onboarding layout for current workspace
view.update(cx, |this, cx| {
this.set_onboarding_layout(window, cx);
})
.ok();
// Clear all current chat rooms // Clear all current chat rooms
registry.update(cx, |this, cx| { registry.update(cx, |this, cx| {
this.reset(cx); this.reset(cx);
}); });
// Setup the onboarding layout for current workspace
this.set_onboarding_layout(window, cx);
} }
SignalKind::Auth(req) => { SignalKind::Auth(req) => {
let url = &req.url; let url = &req.url;
let auto_auth = AppSettings::get_auto_auth(cx); let auto_auth = AppSettings::get_auto_auth(cx);
let is_authenticated = AppSettings::read_global(cx).is_authenticated(url); let is_authenticated = AppSettings::read_global(cx).is_authenticated(url);
view.update(cx, |this, cx| { // Store the auth request in the current view
this.push_auth_request(&req, cx); this.push_auth_request(&req, cx);
if auto_auth && is_authenticated { if auto_auth && is_authenticated {
// Automatically authenticate if the relay is authenticated before // Automatically authenticate if the relay is authenticated before
this.auth(req, window, cx); this.auth(req, window, cx);
} else { } else {
// Otherwise open the auth request popup // Otherwise open the auth request popup
this.open_auth_request(req, window, cx); this.open_auth_request(req, window, cx);
}
})
.ok();
}
SignalKind::ProxyDown => {
if !is_open_proxy_modal {
is_open_proxy_modal = true;
view.update(cx, |this, cx| {
this.render_proxy_modal(window, cx);
})
.ok();
} }
} }
SignalKind::GiftWrapStatus(status) => { SignalKind::GiftWrapStatus(status) => {
@@ -364,17 +272,11 @@ impl ChatSpace {
}); });
} }
SignalKind::GossipRelaysNotFound => { SignalKind::GossipRelaysNotFound => {
view.update(cx, |this, cx| { this.set_required_gossip_relays(cx);
this.set_required_gossip_relays(cx); this.render_setup_gossip_relays_modal(window, cx);
this.render_setup_gossip_relays_modal(window, cx);
})
.ok();
} }
SignalKind::MessagingRelaysNotFound => { SignalKind::MessagingRelaysNotFound => {
view.update(cx, |this, cx| { this.set_required_dm_relays(cx);
this.set_required_dm_relays(cx);
})
.ok();
} }
}; };
}) })
@@ -395,8 +297,8 @@ impl ChatSpace {
self.sending_auth_request(&challenge, cx); self.sending_auth_request(&challenge, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = nostr_client(); let states = app_state();
let app_state = app_state(); let client = states.client();
let signer = client.signer().await?; let signer = client.signer().await?;
// Construct event // Construct event
@@ -427,9 +329,9 @@ impl ChatSpace {
relay.resubscribe().await?; relay.resubscribe().await?;
// Get all failed events that need to be resent // Get all failed events that need to be resent
let mut event_tracker = app_state.event_tracker.write().await; let mut tracker = states.tracker().write().await;
let ids: Vec<EventId> = event_tracker let ids: Vec<EventId> = tracker
.resend_queue .resend_queue
.iter() .iter()
.filter(|(_, url)| relay_url == *url) .filter(|(_, url)| relay_url == *url)
@@ -437,7 +339,7 @@ impl ChatSpace {
.collect(); .collect();
for id in ids.into_iter() { for id in ids.into_iter() {
if let Some(relay_url) = event_tracker.resend_queue.remove(&id) { if let Some(relay_url) = tracker.resend_queue.remove(&id) {
if let Some(event) = client.database().event_by_id(&id).await? { if let Some(event) = client.database().event_by_id(&id).await? {
let event_id = relay.send_event(&event).await?; let event_id = relay.send_event(&event).await?;
@@ -447,8 +349,8 @@ impl ChatSpace {
success: HashSet::from([relay_url]), success: HashSet::from([relay_url]),
}; };
event_tracker.sent_ids.insert(event_id); tracker.sent_ids.insert(event_id);
event_tracker.resent_ids.push(output); tracker.resent_ids.push(output);
} }
} }
} }
@@ -656,7 +558,8 @@ impl ChatSpace {
fn load_local_account(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn load_local_account(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let task = cx.background_spawn(async move { let task = cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::ApplicationSpecificData) .kind(Kind::ApplicationSpecificData)
.identifier(ACCOUNT_IDENTIFIER) .identifier(ACCOUNT_IDENTIFIER)
@@ -717,9 +620,10 @@ impl ChatSpace {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = nostr_client(); let states = app_state();
let app_state = app_state(); let client = states.client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let filter = Filter::new().kind(Kind::PrivateDirectMessage); let filter = Filter::new().kind(Kind::PrivateDirectMessage);
let pubkeys: Vec<PublicKey> = client let pubkeys: Vec<PublicKey> = client
@@ -737,7 +641,7 @@ impl ChatSpace {
.authors(pubkeys); .authors(pubkeys);
client client
.subscribe_to(BOOTSTRAP_RELAYS, filter, app_state.auto_close_opts) .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await?; .await?;
Ok(()) Ok(())
@@ -756,8 +660,8 @@ impl ChatSpace {
fn on_sign_out(&mut self, _e: &Logout, _window: &mut Window, cx: &mut Context<Self>) { fn on_sign_out(&mut self, _e: &Logout, _window: &mut Window, cx: &mut Context<Self>) {
cx.background_spawn(async move { cx.background_spawn(async move {
let client = nostr_client(); let states = app_state();
let app_state = app_state(); let client = states.client();
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::ApplicationSpecificData) .kind(Kind::ApplicationSpecificData)
@@ -770,7 +674,7 @@ impl ChatSpace {
client.reset().await; client.reset().await;
// Notify the channel about the signer being unset // Notify the channel about the signer being unset
app_state.signal.send(SignalKind::SignerUnset).await; states.signal().send(SignalKind::SignerUnset).await;
}) })
.detach(); .detach();
} }
@@ -799,6 +703,37 @@ impl ChatSpace {
window.push_notification(t!("common.copied"), cx); window.push_notification(t!("common.copied"), cx);
} }
fn get_all_panel_ids(&self, cx: &App) -> Option<Vec<u64>> {
let ids: Vec<u64> = self
.dock
.read(cx)
.items
.panel_ids(cx)
.into_iter()
.filter_map(|panel| panel.parse::<u64>().ok())
.collect();
Some(ids)
}
fn set_center_panel<P>(panel: P, window: &mut Window, cx: &mut App)
where
P: PanelView,
{
if let Some(Some(root)) = window.root::<Root>() {
if let Ok(chatspace) = root.read(cx).view().clone().downcast::<ChatSpace>() {
let panel = Arc::new(panel);
let center = DockItem::panel(panel);
chatspace.update(cx, |this, cx| {
this.dock.update(cx, |this, cx| {
this.set_center(center, window, cx);
});
});
}
}
}
fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) { fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) {
let relays = default_nip65_relays(); let relays = default_nip65_relays();
@@ -875,9 +810,9 @@ impl ChatSpace {
.on_ok(|_, window, cx| { .on_ok(|_, window, cx| {
window window
.spawn(cx, async move |cx| { .spawn(cx, async move |cx| {
let app_state = app_state(); let states = app_state();
let relays = default_nip65_relays(); let relays = default_nip65_relays();
let result = app_state.set_nip65(relays).await; let result = states.set_nip65(relays).await;
cx.update(|window, cx| { cx.update(|window, cx| {
match result { match result {
@@ -977,9 +912,9 @@ impl ChatSpace {
.on_ok(|_, window, cx| { .on_ok(|_, window, cx| {
window window
.spawn(cx, async move |cx| { .spawn(cx, async move |cx| {
let app_state = app_state(); let states = app_state();
let relays = default_nip17_relays(); let relays = default_nip17_relays();
let result = app_state.set_nip17(relays).await; let result = states.set_nip17(relays).await;
cx.update(|window, cx| { cx.update(|window, cx| {
match result { match result {
@@ -1001,32 +936,6 @@ impl ChatSpace {
}) })
} }
fn render_proxy_modal(&mut self, window: &mut Window, cx: &mut App) {
window.open_modal(cx, |this, _window, _cx| {
this.overlay_closable(false)
.show_close(false)
.keyboard(false)
.alert()
.button_props(ModalButtonProps::default().ok_text(t!("common.open_browser")))
.title(shared_t!("proxy.label"))
.child(
v_flex()
.p_3()
.gap_1()
.w_full()
.items_center()
.justify_center()
.text_center()
.text_sm()
.child(shared_t!("proxy.description")),
)
.on_ok(move |_e, _window, cx| {
cx.open_url("http://localhost:7400");
false
})
});
}
fn render_client_keys_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn render_client_keys_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
window.open_modal(cx, move |this, _window, cx| { window.open_modal(cx, move |this, _window, cx| {
this.overlay_closable(false) this.overlay_closable(false)
@@ -1196,92 +1105,6 @@ impl ChatSpace {
}), }),
) )
} }
pub(crate) fn proxy_signer(window: &mut Window, cx: &mut App) {
let Some(Some(root)) = window.root::<Root>() else {
return;
};
let Ok(chatspace) = root.read(cx).view().clone().downcast::<ChatSpace>() else {
return;
};
chatspace.update(cx, |this, cx| {
let proxy = BrowserSignerProxy::new(BrowserSignerProxyOptions::default());
let url = proxy.url();
this._tasks.push(cx.background_spawn(async move {
let client = nostr_client();
let app_state = app_state();
if proxy.start().await.is_ok() {
webbrowser::open(&url).ok();
loop {
if proxy.is_session_active() {
// Save the signer to disk for further logins
if let Ok(public_key) = proxy.get_public_key().await {
let keys = Keys::generate();
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
let kind = Kind::ApplicationSpecificData;
let builder = EventBuilder::new(kind, "extension")
.tags(tags)
.build(public_key)
.sign(&keys)
.await;
if let Ok(event) = builder {
if let Err(e) = client.database().save_event(&event).await {
log::error!("Failed to save event: {e}");
};
}
}
// Set the client's signer with current proxy signer
client.set_signer(proxy.clone()).await;
break;
} else {
app_state.signal.send(SignalKind::ProxyDown).await;
}
smol::Timer::after(Duration::from_secs(1)).await;
}
}
}));
});
}
fn get_all_panel_ids(&self, cx: &App) -> Option<Vec<u64>> {
let ids: Vec<u64> = self
.dock
.read(cx)
.items
.panel_ids(cx)
.into_iter()
.filter_map(|panel| panel.parse::<u64>().ok())
.collect();
Some(ids)
}
pub(crate) fn set_center_panel<P>(panel: P, window: &mut Window, cx: &mut App)
where
P: PanelView,
{
if let Some(Some(root)) = window.root::<Root>() {
if let Ok(chatspace) = root.read(cx).view().clone().downcast::<ChatSpace>() {
let panel = Arc::new(panel);
let center = DockItem::panel(panel);
chatspace.update(cx, |this, cx| {
this.dock.update(cx, |this, cx| {
this.set_center(center, window, cx);
});
});
}
}
}
} }
impl Render for ChatSpace { impl Render for ChatSpace {

View File

@@ -1,13 +1,13 @@
use std::sync::Arc; use std::sync::Arc;
use app_state::constants::{APP_ID, APP_NAME};
use app_state::{app_state, nostr_client};
use assets::Assets; use assets::Assets;
use gpui::{ use gpui::{
point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString, point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString,
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
WindowOptions, WindowOptions,
}; };
use states::app_state;
use states::constants::{APP_ID, APP_NAME};
use ui::Root; use ui::Root;
use crate::actions::{load_embedded_fonts, quit, Quit}; use crate::actions::{load_embedded_fonts, quit, Quit};
@@ -22,9 +22,6 @@ fn main() {
// Initialize logging // Initialize logging
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
// Initialize the Nostr client
let _client = nostr_client();
// Initialize the coop simple storage // Initialize the coop simple storage
let _app_state = app_state(); let _app_state = app_state();

View File

@@ -1,9 +1,6 @@
use std::time::Duration; use std::time::Duration;
use anyhow::Error; use anyhow::Error;
use app_state::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
use app_state::state::SignalKind;
use app_state::{app_state, nostr_client};
use client_keys::ClientKeys; use client_keys::ClientKeys;
use common::display::RenderedProfile; use common::display::RenderedProfile;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
@@ -17,6 +14,9 @@ use i18n::{shared_t, t};
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::app_state;
use states::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
use states::state::SignalKind;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
@@ -28,7 +28,6 @@ use ui::popup_menu::PopupMenu;
use ui::{h_flex, v_flex, ContextModal, Sizable, StyledExt}; use ui::{h_flex, v_flex, ContextModal, Sizable, StyledExt};
use crate::actions::CoopAuthUrlHandler; use crate::actions::CoopAuthUrlHandler;
use crate::chatspace::ChatSpace;
pub fn init( pub fn init(
profile: Profile, profile: Profile,
@@ -43,7 +42,6 @@ pub struct Account {
profile: Profile, profile: Profile,
stored_secret: String, stored_secret: String,
is_bunker: bool, is_bunker: bool,
is_extension: bool,
loading: bool, loading: bool,
name: SharedString, name: SharedString,
@@ -57,8 +55,6 @@ pub struct Account {
impl Account { impl Account {
fn new(secret: String, profile: Profile, window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(secret: String, profile: Profile, window: &mut Window, cx: &mut Context<Self>) -> Self {
let is_bunker = secret.starts_with("bunker://"); let is_bunker = secret.starts_with("bunker://");
let is_extension = secret.starts_with("extension");
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
subscriptions.push( subscriptions.push(
@@ -74,7 +70,6 @@ impl Account {
Self { Self {
profile, profile,
is_bunker, is_bunker,
is_extension,
stored_secret: secret, stored_secret: secret,
loading: false, loading: false,
name: "Account".into(), name: "Account".into(),
@@ -92,8 +87,6 @@ impl Account {
if let Ok(uri) = NostrConnectURI::parse(&self.stored_secret) { if let Ok(uri) = NostrConnectURI::parse(&self.stored_secret) {
self.nostr_connect(uri, window, cx); self.nostr_connect(uri, window, cx);
} }
} else if self.is_extension {
self.set_proxy(window, cx);
} else if let Ok(enc) = EncryptedSecretKey::from_bech32(&self.stored_secret) { } else if let Ok(enc) = EncryptedSecretKey::from_bech32(&self.stored_secret) {
self.keys(enc, window, cx); self.keys(enc, window, cx);
} else { } else {
@@ -115,7 +108,7 @@ impl Account {
self._tasks.push( self._tasks.push(
// Handle connection in the background // Handle connection in the background
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let client = nostr_client(); let client = app_state().client();
match signer.bunker_uri().await { match signer.bunker_uri().await {
Ok(_) => { Ok(_) => {
@@ -134,10 +127,6 @@ impl Account {
); );
} }
fn set_proxy(&mut self, window: &mut Window, cx: &mut Context<Self>) {
ChatSpace::proxy_signer(window, cx);
}
fn keys(&mut self, enc: EncryptedSecretKey, window: &mut Window, cx: &mut Context<Self>) { fn keys(&mut self, enc: EncryptedSecretKey, window: &mut Window, cx: &mut Context<Self>) {
let pwd_input: Entity<InputState> = cx.new(|cx| InputState::new(window, cx).masked(true)); let pwd_input: Entity<InputState> = cx.new(|cx| InputState::new(window, cx).masked(true));
let weak_input = pwd_input.downgrade(); let weak_input = pwd_input.downgrade();
@@ -245,7 +234,7 @@ impl Account {
}) })
.ok(); .ok();
let client = nostr_client(); let client = app_state().client();
let keys = Keys::new(secret); let keys = Keys::new(secret);
// Set the client's signer with the current keys // Set the client's signer with the current keys
@@ -268,8 +257,8 @@ impl Account {
self._tasks.push( self._tasks.push(
// Reset the nostr client in the background // Reset the nostr client in the background
cx.background_spawn(async move { cx.background_spawn(async move {
let client = nostr_client(); let states = app_state();
let app_state = app_state(); let client = states.client();
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::ApplicationSpecificData) .kind(Kind::ApplicationSpecificData)
@@ -282,7 +271,7 @@ impl Account {
client.unset_signer().await; client.unset_signer().await;
// Notify the channel about the signer being unset // Notify the channel about the signer being unset
app_state.signal.send(SignalKind::SignerUnset).await; states.signal().send(SignalKind::SignerUnset).await;
}), }),
); );
} }
@@ -392,41 +381,20 @@ impl Render for Account {
.child(Avatar::new(avatar).size(rems(1.5))) .child(Avatar::new(avatar).size(rems(1.5)))
.child(div().pb_px().font_semibold().child(name)), .child(div().pb_px().font_semibold().child(name)),
) )
.child( .child(div().when(self.is_bunker, |this| {
div() let label = SharedString::from("Nostr Connect");
.when(self.is_bunker, |this| {
let label = SharedString::from("Nostr Connect");
this.child( this.child(
div() div()
.py_0p5() .py_0p5()
.px_2() .px_2()
.text_xs() .text_xs()
.bg(cx.theme().secondary_active) .bg(cx.theme().secondary_active)
.text_color( .text_color(cx.theme().secondary_foreground)
cx.theme().secondary_foreground, .rounded_full()
) .child(label),
.rounded_full() )
.child(label), })),
)
})
.when(self.is_extension, |this| {
let label = SharedString::from("Extension");
this.child(
div()
.py_0p5()
.px_2()
.text_xs()
.bg(cx.theme().secondary_active)
.text_color(
cx.theme().secondary_foreground,
)
.rounded_full()
.child(label),
)
}),
),
) )
}) })
.active(|this| this.bg(cx.theme().element_active)) .active(|this| this.bg(cx.theme().element_active))

View File

@@ -1,7 +1,6 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::time::Duration; use std::time::Duration;
use app_state::{app_state, nostr_client};
use common::display::{RenderedProfile, RenderedTimestamp}; use common::display::{RenderedProfile, RenderedTimestamp};
use common::nip96::nip96_upload; use common::nip96::nip96_upload;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
@@ -24,6 +23,7 @@ use serde::Deserialize;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::fs; use smol::fs;
use states::app_state;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::actions::{CopyPublicKey, OpenPublicKey}; use ui::actions::{CopyPublicKey, OpenPublicKey};
use ui::avatar::Avatar; use ui::avatar::Avatar;
@@ -169,8 +169,8 @@ impl Chat {
let message = Message::user(event); let message = Message::user(event);
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let app_state = app_state(); let states = app_state();
let event_tracker = app_state.event_tracker.read().await; let event_tracker = states.tracker().read().await;
let sent_ids = event_tracker.sent_ids(); let sent_ids = event_tracker.sent_ids();
this.update_in(cx, |this, _window, cx| { this.update_in(cx, |this, _window, cx| {
@@ -530,7 +530,7 @@ impl Chat {
let path = paths.pop()?; let path = paths.pop()?;
let upload = Tokio::spawn(cx, async move { let upload = Tokio::spawn(cx, async move {
let client = nostr_client(); let client = app_state().client();
let file = fs::read(path).await.ok()?; let file = fs::read(path).await.ok()?;
let url = nip96_upload(client, &nip96_server, file).await.ok()?; let url = nip96_upload(client, &nip96_server, file).await.ok()?;
@@ -1239,9 +1239,9 @@ impl Chat {
let id = ev.0; let id = ev.0;
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move { let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
let client = nostr_client(); let states = app_state();
let app_state = app_state(); let client = states.client();
let event_tracker = app_state.event_tracker.read().await; let event_tracker = states.tracker().read().await;
let mut relays: Vec<RelayUrl> = vec![]; let mut relays: Vec<RelayUrl> = vec![];
let filter = Filter::new() let filter = Filter::new()

View File

@@ -2,8 +2,6 @@ use std::ops::Range;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use app_state::constants::BOOTSTRAP_RELAYS;
use app_state::{app_state, nostr_client};
use common::display::{RenderedProfile, TextUtils}; use common::display::{RenderedProfile, TextUtils};
use common::nip05::nip05_profile; use common::nip05::nip05_profile;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
@@ -19,6 +17,8 @@ use registry::room::Room;
use registry::Registry; use registry::Registry;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::app_state;
use states::constants::BOOTSTRAP_RELAYS;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
@@ -129,7 +129,7 @@ impl Compose {
let mut tasks = smallvec![]; let mut tasks = smallvec![];
let get_contacts: Task<Result<Vec<Contact>, Error>> = cx.background_spawn(async move { let get_contacts: Task<Result<Vec<Contact>, Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
let profiles = client.database().contacts(public_key).await?; let profiles = client.database().contacts(public_key).await?;
@@ -195,13 +195,15 @@ impl Compose {
} }
async fn request_metadata(public_key: PublicKey) -> Result<(), Error> { async fn request_metadata(public_key: PublicKey) -> Result<(), Error> {
let client = nostr_client(); let states = app_state();
let app_state = app_state(); let client = states.client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
let filter = Filter::new().author(public_key).kinds(kinds).limit(10); let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
client client
.subscribe_to(BOOTSTRAP_RELAYS, filter, app_state.auto_close_opts) .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await?; .await?;
Ok(()) Ok(())

View File

@@ -2,7 +2,6 @@ use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use anyhow::Error; use anyhow::Error;
use app_state::nostr_client;
use common::nip96::nip96_upload; use common::nip96::nip96_upload;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
@@ -13,6 +12,7 @@ use i18n::{shared_t, t};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use settings::AppSettings; use settings::AppSettings;
use smol::fs; use smol::fs;
use states::app_state;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::input::{InputState, TextInput}; use ui::input::{InputState, TextInput};
@@ -58,7 +58,7 @@ impl EditProfile {
}; };
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move { let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
let metadata = client let metadata = client
@@ -125,7 +125,9 @@ impl EditProfile {
let (tx, rx) = oneshot::channel::<Url>(); let (tx, rx) = oneshot::channel::<Url>();
nostr_sdk::async_utility::task::spawn(async move { nostr_sdk::async_utility::task::spawn(async move {
if let Ok(url) = nip96_upload(nostr_client(), &nip96, file_data).await { if let Ok(url) =
nip96_upload(app_state().client(), &nip96, file_data).await
{
_ = tx.send(url); _ = tx.send(url);
} }
}); });
@@ -188,7 +190,7 @@ impl EditProfile {
} }
cx.background_spawn(async move { cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
// Sign the new metadata event // Sign the new metadata event

View File

@@ -1,7 +1,5 @@
use std::time::Duration; use std::time::Duration;
use app_state::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
use app_state::nostr_client;
use client_keys::ClientKeys; use client_keys::ClientKeys;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
@@ -12,6 +10,8 @@ use gpui::{
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::app_state;
use states::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
@@ -248,7 +248,7 @@ impl Login {
// Set the client's signer with the current keys // Set the client's signer with the current keys
cx.background_spawn(async move { cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
client.set_signer(keys).await; client.set_signer(keys).await;
}) })
.detach(); .detach();
@@ -331,7 +331,7 @@ impl Login {
} }
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move { let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
// Update the client's signer // Update the client's signer
client.set_signer(signer).await; client.set_signer(signer).await;
@@ -362,7 +362,7 @@ impl Login {
if let Ok(enc_key) = if let Ok(enc_key) =
EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown) EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown)
{ {
let client = nostr_client(); let client = app_state().client();
let value = enc_key.to_bech32().unwrap(); let value = enc_key.to_bech32().unwrap();
let keys = Keys::generate(); let keys = Keys::generate();
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)]; let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];

View File

@@ -1,6 +1,4 @@
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use app_state::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS};
use app_state::{default_nip17_relays, default_nip65_relays, nostr_client};
use common::nip96::nip96_upload; use common::nip96::nip96_upload;
use gpui::{ use gpui::{
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
@@ -12,6 +10,8 @@ use i18n::{shared_t, t};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use settings::AppSettings; use settings::AppSettings;
use smol::fs; use smol::fs;
use states::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS};
use states::{app_state, default_nip17_relays, default_nip65_relays};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
@@ -124,7 +124,7 @@ impl NewAccount {
// Set the client's signer with the current keys // Set the client's signer with the current keys
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
// Set the client's signer with the current keys // Set the client's signer with the current keys
client.set_signer(keys).await; client.set_signer(keys).await;
@@ -176,7 +176,7 @@ impl NewAccount {
if let Ok(enc_key) = if let Ok(enc_key) =
EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown) EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown)
{ {
let client = nostr_client(); let client = app_state().client();
let value = enc_key.to_bech32().unwrap(); let value = enc_key.to_bech32().unwrap();
let keys = Keys::generate(); let keys = Keys::generate();
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)]; let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
@@ -217,7 +217,7 @@ impl NewAccount {
Ok(Some(mut paths)) => { Ok(Some(mut paths)) => {
if let Some(path) = paths.pop() { if let Some(path) = paths.pop() {
let file = fs::read(path).await?; let file = fs::read(path).await?;
let url = nip96_upload(nostr_client(), &nip96_server, file).await?; let url = nip96_upload(app_state().client(), &nip96_server, file).await?;
Ok(url) Ok(url)
} else { } else {

View File

@@ -1,10 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use app_state::constants::{
ACCOUNT_IDENTIFIER, APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT,
};
use app_state::nostr_client;
use client_keys::ClientKeys; use client_keys::ClientKeys;
use common::display::TextUtils; use common::display::TextUtils;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
@@ -16,6 +12,8 @@ use gpui::{
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::app_state;
use states::constants::{ACCOUNT_IDENTIFIER, APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
@@ -23,7 +21,7 @@ use ui::notification::Notification;
use ui::popup_menu::PopupMenu; use ui::popup_menu::PopupMenu;
use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt}; use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt};
use crate::chatspace::{self, ChatSpace}; use crate::chatspace;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
Onboarding::new(window, cx) Onboarding::new(window, cx)
@@ -163,10 +161,6 @@ impl Onboarding {
) )
} }
fn set_proxy(&mut self, window: &mut Window, cx: &mut Context<Self>) {
ChatSpace::proxy_signer(window, cx);
}
fn write_uri_to_disk( fn write_uri_to_disk(
&mut self, &mut self,
signer: NostrConnect, signer: NostrConnect,
@@ -181,7 +175,7 @@ impl Onboarding {
} }
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move { let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
// Update the client's signer // Update the client's signer
client.set_signer(signer).await; client.set_signer(signer).await;
@@ -348,30 +342,11 @@ impl Render for Onboarding {
.child( .child(
Button::new("key") Button::new("key")
.label(t!("onboarding.key_login")) .label(t!("onboarding.key_login"))
.large()
.ghost_alt() .ghost_alt()
.on_click(cx.listener(move |_, _, window, cx| { .on_click(cx.listener(move |_, _, window, cx| {
chatspace::login(window, cx); chatspace::login(window, cx);
})), })),
)
.child(
v_flex()
.gap_1()
.child(
Button::new("ext")
.label(t!("onboarding.ext_login"))
.ghost_alt()
.on_click(cx.listener(move |this, _, window, cx| {
this.set_proxy(window, cx);
})),
)
.child(
div()
.italic()
.text_xs()
.text_center()
.text_color(cx.theme().text_muted)
.child(shared_t!("onboarding.ext_login_note")),
),
), ),
), ),
) )

View File

@@ -1,7 +1,5 @@
use std::time::Duration; use std::time::Duration;
use app_state::constants::BOOTSTRAP_RELAYS;
use app_state::nostr_client;
use common::display::{shorten_pubkey, RenderedProfile, RenderedTimestamp}; use common::display::{shorten_pubkey, RenderedProfile, RenderedTimestamp};
use common::nip05::nip05_verify; use common::nip05::nip05_verify;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
@@ -15,6 +13,8 @@ use nostr_sdk::prelude::*;
use registry::Registry; use registry::Registry;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::app_state;
use states::constants::BOOTSTRAP_RELAYS;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
@@ -43,7 +43,7 @@ impl Screening {
let contact_check: Task<Result<(bool, Vec<Profile>), Error>> = let contact_check: Task<Result<(bool, Vec<Profile>), Error>> =
cx.background_spawn(async move { cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
let signer_pubkey = signer.get_public_key().await?; let signer_pubkey = signer.get_public_key().await?;
@@ -68,7 +68,7 @@ impl Screening {
}); });
let activity_check = cx.background_spawn(async move { let activity_check = cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let filter = Filter::new().author(public_key).limit(1); let filter = Filter::new().author(public_key).limit(1);
let mut activity: Option<Timestamp> = None; let mut activity: Option<Timestamp> = None;
@@ -157,7 +157,7 @@ impl Screening {
let public_key = self.profile.public_key(); let public_key = self.profile.public_key();
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
let tag = Tag::public_key_report(public_key, Report::Impersonation); let tag = Tag::public_key_report(public_key, Report::Impersonation);

View File

@@ -2,7 +2,6 @@ use std::collections::HashSet;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use app_state::{app_state, nostr_client};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, px, uniform_list, App, AppContext, AsyncWindowContext, Context, Entity, div, px, uniform_list, App, AppContext, AsyncWindowContext, Context, Entity,
@@ -10,8 +9,10 @@ use gpui::{
Task, TextAlign, UniformList, Window, Task, TextAlign, UniformList, Window,
}; };
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::app_state;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
@@ -80,7 +81,7 @@ impl SetupRelay {
fn load(cx: &AsyncWindowContext) -> Task<Result<Vec<RelayUrl>, Error>> { fn load(cx: &AsyncWindowContext) -> Task<Result<Vec<RelayUrl>, Error>> {
cx.background_spawn(async move { cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
@@ -152,8 +153,8 @@ impl SetupRelay {
let relays = self.relays.clone(); let relays = self.relays.clone();
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let app_state = app_state(); let states = app_state();
let client = nostr_client(); let client = states.client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
@@ -177,16 +178,10 @@ impl SetupRelay {
} }
// Fetch gift wrap events // Fetch gift wrap events
let sub_id = app_state.gift_wrap_sub_id.clone(); states
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); .get_messages(public_key, &relays.into_iter().collect_vec())
if client
.subscribe_with_id_to(relays.clone(), sub_id, filter, None)
.await .await
.is_ok() .ok();
{
log::info!("Subscribed to messages in: {relays:?}");
};
Ok(()) Ok(())
}); });

View File

@@ -3,9 +3,6 @@ use std::ops::Range;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use app_state::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use app_state::state::UnwrappingStatus;
use app_state::{app_state, nostr_client};
use common::debounced_delay::DebouncedDelay; use common::debounced_delay::DebouncedDelay;
use common::display::{RenderedTimestamp, TextUtils}; use common::display::{RenderedTimestamp, TextUtils};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
@@ -23,6 +20,9 @@ use registry::room::{Room, RoomKind};
use registry::{Registry, RegistryEvent}; use registry::{Registry, RegistryEvent};
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::app_state;
use states::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use states::state::UnwrappingStatus;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
@@ -140,7 +140,7 @@ impl Sidebar {
} }
async fn request_metadata(public_key: PublicKey) -> Result<(), Error> { async fn request_metadata(public_key: PublicKey) -> Result<(), Error> {
let client = nostr_client(); let client = app_state().client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
let filter = Filter::new().author(public_key).kinds(kinds).limit(10); let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
@@ -165,7 +165,7 @@ impl Sidebar {
} }
async fn nip50(query: &str) -> Result<BTreeSet<Room>, Error> { async fn nip50(query: &str) -> Result<BTreeSet<Room>, Error> {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
@@ -530,9 +530,8 @@ impl Sidebar {
fn on_manage(&mut self, _ev: &RelayStatus, window: &mut Window, cx: &mut Context<Self>) { fn on_manage(&mut self, _ev: &RelayStatus, window: &mut Window, cx: &mut Context<Self>) {
let task: Task<Result<Vec<Relay>, Error>> = cx.background_spawn(async move { let task: Task<Result<Vec<Relay>, Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let app_state = app_state(); let subscription = client.subscription(&SubscriptionId::new("inbox")).await;
let subscription = client.subscription(&app_state.gift_wrap_sub_id).await;
let mut relays: Vec<Relay> = vec![]; let mut relays: Vec<Relay> = vec![];
for (url, _filter) in subscription.into_iter() { for (url, _filter) in subscription.into_iter() {

View File

@@ -1,6 +1,5 @@
use std::time::Duration; use std::time::Duration;
use app_state::nostr_client;
use common::display::RenderedProfile; use common::display::RenderedProfile;
use common::nip05::nip05_verify; use common::nip05::nip05_verify;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
@@ -14,6 +13,7 @@ use nostr_sdk::prelude::*;
use registry::Registry; use registry::Registry;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::app_state;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
@@ -39,7 +39,7 @@ impl UserProfile {
let mut tasks = smallvec![]; let mut tasks = smallvec![];
let check_follow: Task<Result<bool, Error>> = cx.background_spawn(async move { let check_follow: Task<Result<bool, Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
let contact_list = client.database().contacts_public_keys(public_key).await?; let contact_list = client.database().contacts_public_keys(public_key).await?;

View File

@@ -6,16 +6,19 @@ publish.workspace = true
[dependencies] [dependencies]
common = { path = "../common" } common = { path = "../common" }
app_state = { path = "../app_state" } states = { path = "../states" }
settings = { path = "../settings" } settings = { path = "../settings" }
gpui.workspace = true gpui.workspace = true
nostr.workspace = true nostr.workspace = true
nostr-sdk.workspace = true nostr-sdk.workspace = true
nostr-lmdb.workspace = true
anyhow.workspace = true anyhow.workspace = true
itertools.workspace = true itertools.workspace = true
smallvec.workspace = true smallvec.workspace = true
smol.workspace = true smol.workspace = true
log.workspace = true log.workspace = true
flume.workspace = true
fuzzy-matcher = "0.3.7" fuzzy-matcher = "0.3.7"
rustls = "0.23.23"

View File

@@ -2,17 +2,19 @@ use std::cmp::Reverse;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use anyhow::Error; use anyhow::Error;
use app_state::nostr_client;
use app_state::state::UnwrappingStatus;
use common::event::EventUtils; use common::event::EventUtils;
use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window}; use gpui::{
App, AppContext, AsyncApp, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window,
};
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use room::RoomKind; use room::RoomKind;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::app_state;
use states::state::UnwrappingStatus;
use crate::room::Room; use crate::room::Room;
@@ -34,7 +36,7 @@ pub enum RegistryEvent {
NewRequest(RoomKind), NewRequest(RoomKind),
} }
/// Main registry for managing chat rooms and user profiles #[derive(Debug)]
pub struct Registry { pub struct Registry {
/// Collection of all chat rooms /// Collection of all chat rooms
pub rooms: Vec<Entity<Room>>, pub rooms: Vec<Entity<Room>>,
@@ -45,7 +47,7 @@ pub struct Registry {
/// Status of the unwrapping process /// Status of the unwrapping process
pub unwrapping_status: Entity<UnwrappingStatus>, pub unwrapping_status: Entity<UnwrappingStatus>,
/// Public key of the currently activated signer /// Public Key of the currently activated signer
signer_pubkey: Option<PublicKey>, signer_pubkey: Option<PublicKey>,
/// Tasks for asynchronous operations /// Tasks for asynchronous operations
@@ -55,51 +57,40 @@ pub struct Registry {
impl EventEmitter<RegistryEvent> for Registry {} impl EventEmitter<RegistryEvent> for Registry {}
impl Registry { impl Registry {
/// Retrieve the Global Registry state /// Retrieve the global registry state
pub fn global(cx: &App) -> Entity<Self> { pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalRegistry>().0.clone() cx.global::<GlobalRegistry>().0.clone()
} }
/// Retrieve the Registry instance /// Retrieve the registry instance
pub fn read_global(cx: &App) -> &Self { pub fn read_global(cx: &App) -> &Self {
cx.global::<GlobalRegistry>().0.read(cx) cx.global::<GlobalRegistry>().0.read(cx)
} }
/// Set the global Registry instance /// Set the global registry instance
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) { pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalRegistry(state)); cx.set_global(GlobalRegistry(state));
} }
/// Create a new Registry instance /// Create a new registry instance
pub(crate) fn new(cx: &mut Context<Self>) -> Self { pub(crate) fn new(cx: &mut Context<Self>) -> Self {
let unwrapping_status = cx.new(|_| UnwrappingStatus::default()); let unwrapping_status = cx.new(|_| UnwrappingStatus::default());
let mut tasks = smallvec![]; let mut tasks = smallvec![];
let load_local_persons: Task<Result<Vec<Profile>, Error>> =
cx.background_spawn(async move {
let client = nostr_client();
let filter = Filter::new().kind(Kind::Metadata).limit(200);
let events = client.database().query(filter).await?;
let mut profiles = vec![];
for event in events.into_iter() {
let metadata = Metadata::from_json(event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata);
profiles.push(profile);
}
Ok(profiles)
});
tasks.push( tasks.push(
// Load all user profiles from the database when the Registry is created // Load all user profiles from the database
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
if let Ok(profiles) = load_local_persons.await { match Self::load_persons(cx).await {
this.update(cx, |this, cx| { Ok(profiles) => {
this.set_persons(profiles, cx); this.update(cx, |this, cx| {
}) this.set_persons(profiles, cx);
.ok(); })
} .ok();
}
Err(e) => {
log::error!("Failed to load persons: {e}");
}
};
}), }),
); );
@@ -112,6 +103,25 @@ impl Registry {
} }
} }
/// Create a task to load all user profiles from the database
fn load_persons(cx: &AsyncApp) -> Task<Result<Vec<Profile>, Error>> {
cx.background_spawn(async move {
let client = app_state().client();
let filter = Filter::new().kind(Kind::Metadata).limit(200);
let events = client.database().query(filter).await?;
let mut profiles = vec![];
for event in events.into_iter() {
let metadata = Metadata::from_json(event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata);
profiles.push(profile);
}
Ok(profiles)
})
}
/// Returns the public key of the currently activated signer. /// Returns the public key of the currently activated signer.
pub fn signer_pubkey(&self) -> Option<PublicKey> { pub fn signer_pubkey(&self) -> Option<PublicKey> {
self.signer_pubkey self.signer_pubkey
@@ -269,7 +279,7 @@ impl Registry {
let bypass_setting = AppSettings::get_contact_bypass(cx); let bypass_setting = AppSettings::get_contact_bypass(cx);
let task: Task<Result<HashSet<Room>, Error>> = cx.background_spawn(async move { let task: Task<Result<HashSet<Room>, Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
let contacts = client.database().contacts_public_keys(public_key).await?; let contacts = client.database().contacts_public_keys(public_key).await?;
@@ -339,11 +349,9 @@ impl Registry {
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
match task.await { match task.await {
Ok(rooms) => { Ok(rooms) => {
this.update_in(cx, move |_, window, cx| { this.update_in(cx, move |this, _window, cx| {
cx.defer_in(window, move |this, _window, cx| { this.extend_rooms(rooms, cx);
this.extend_rooms(rooms, cx); this.sort(cx);
this.sort(cx);
});
}) })
.ok(); .ok();
} }

View File

@@ -4,13 +4,13 @@ use std::hash::{Hash, Hasher};
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use app_state::constants::SEND_RETRY;
use app_state::{app_state, nostr_client};
use common::display::RenderedProfile; use common::display::RenderedProfile;
use common::event::EventUtils; use common::event::EventUtils;
use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task}; use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task};
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use states::app_state;
use states::constants::SEND_RETRY;
use crate::Registry; use crate::Registry;
@@ -171,9 +171,9 @@ impl From<&UnsignedEvent> for Room {
} }
impl Room { impl Room {
/// Constructs a new room instance for a private message with the given receiver and tags. /// Constructs a new room with the given receiver and tags.
pub async fn new(subject: Option<String>, receivers: Vec<PublicKey>) -> Result<Self, Error> { pub async fn new(subject: Option<String>, receivers: Vec<PublicKey>) -> Result<Self, Error> {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
@@ -310,7 +310,7 @@ impl Room {
let members = self.members.clone(); let members = self.members.clone();
cx.background_spawn(async move { cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
@@ -363,11 +363,11 @@ impl Room {
let members = self.members.clone(); let members = self.members.clone();
cx.background_spawn(async move { cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
let sent_ids: Vec<EventId> = app_state() let sent_ids: Vec<EventId> = app_state()
.event_tracker .tracker()
.read() .read()
.await .await
.sent_ids() .sent_ids()
@@ -482,8 +482,8 @@ impl Room {
let mut members = self.members.clone(); let mut members = self.members.clone();
cx.background_spawn(async move { cx.background_spawn(async move {
let app_state = app_state(); let states = app_state();
let client = nostr_client(); let client = states.client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
@@ -514,7 +514,7 @@ impl Room {
if auth_required { if auth_required {
// Wait for authenticated and resent event successfully // Wait for authenticated and resent event successfully
for attempt in 0..=SEND_RETRY { for attempt in 0..=SEND_RETRY {
let retry_manager = app_state.event_tracker.read().await; let retry_manager = states.tracker().read().await;
let ids = retry_manager.resent_ids(); let ids = retry_manager.resent_ids();
// Check if event was successfully resent // Check if event was successfully resent
@@ -579,7 +579,7 @@ impl Room {
cx: &App, cx: &App,
) -> Task<Result<Vec<SendReport>, Error>> { ) -> Task<Result<Vec<SendReport>, Error>> {
cx.background_spawn(async move { cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let mut resend_reports = vec![]; let mut resend_reports = vec![];
for report in reports.into_iter() { for report in reports.into_iter() {
@@ -633,7 +633,7 @@ impl Room {
/// Gets messaging relays for public key /// Gets messaging relays for public key
async fn messaging_relays(public_key: PublicKey) -> Vec<RelayUrl> { async fn messaging_relays(public_key: PublicKey) -> Vec<RelayUrl> {
let client = nostr_client(); let client = app_state().client();
let mut relay_urls = vec![]; let mut relay_urls = vec![];
let filter = Filter::new() let filter = Filter::new()

View File

@@ -5,7 +5,7 @@ edition.workspace = true
publish.workspace = true publish.workspace = true
[dependencies] [dependencies]
app_state = { path = "../app_state" } states = { path = "../states" }
nostr-sdk.workspace = true nostr-sdk.workspace = true
gpui.workspace = true gpui.workspace = true

View File

@@ -1,10 +1,10 @@
use anyhow::anyhow; use anyhow::anyhow;
use app_state::constants::SETTINGS_IDENTIFIER;
use app_state::nostr_client;
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use states::app_state;
use states::constants::SETTINGS_IDENTIFIER;
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
let state = cx.new(AppSettings::new); let state = cx.new(AppSettings::new);
@@ -121,7 +121,7 @@ impl AppSettings {
pub fn load_settings(&self, cx: &mut Context<Self>) { pub fn load_settings(&self, cx: &mut Context<Self>) {
let task: Task<Result<Settings, anyhow::Error>> = cx.background_spawn(async move { let task: Task<Result<Settings, anyhow::Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
@@ -153,7 +153,7 @@ impl AppSettings {
pub fn set_settings(&self, cx: &mut Context<Self>) { pub fn set_settings(&self, cx: &mut Context<Self>) {
if let Ok(content) = serde_json::to_string(&self.setting_values) { if let Ok(content) = serde_json::to_string(&self.setting_values) {
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move { let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = app_state().client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;

View File

@@ -1,25 +0,0 @@
[package]
name = "signer_proxy"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
app_state = { path = "../app_state" }
nostr.workspace = true
smol.workspace = true
oneshot.workspace = true
anyhow.workspace = true
log.workspace = true
futures.workspace = true
smallvec.workspace = true
serde.workspace = true
serde_json.workspace = true
atomic-destructor = "0.3.0"
uuid = { version = "1.17", features = ["serde", "v4"] }
hyper = { version = "1.6", features = ["server", "http1"] }
hyper-util = { version = "0.1", features = ["server"] }
bytes = "1.10"
http-body-util = "0.1"

View File

@@ -1,35 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>NIP-07 Proxy</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="container">
<h1>NIP-07 Proxy</h1>
<p>
This page acts as a proxy between your native application and
the NIP-07 browser extension.
</p>
<div class="status-box">
<strong>Status:</strong> <span id="status">Checking...</span>
</div>
<p>
<small
>Keep this tab open while using your application. The page
will automatically poll for requests from your native
app.</small
>
</p>
<h3>Debug Info</h3>
<p>
<small
>Check the browser console (F12) for detailed logs.</small
>
</p>
</div>
<script src="proxy.js"></script>
</body>
</html>

View File

@@ -1,151 +0,0 @@
let isPolling = false;
async function pollForRequests() {
if (isPolling) return;
isPolling = true;
try {
const response = await fetch("/api/pending");
const data = await response.json();
console.log("Polled for requests, got:", data);
// Process any new requests
if (data.requests && data.requests.length > 0) {
console.log(`Processing ${data.requests.length} requests`);
for (const request of data.requests) {
await handleNip07Request(request);
}
}
} catch (error) {
console.error("Polling error:", error);
updateStatus("Error: " + error.message, "error");
}
isPolling = false;
}
async function handleNip07Request(request) {
console.log("Handling request:", request);
try {
let result;
if (!window.nostr) {
throw new Error("NIP-07 extension not available");
}
switch (request.method) {
case "get_public_key":
console.log("Calling nostr.getPublicKey()");
result = await window.nostr.getPublicKey();
console.log("Got public key:", result);
break;
case "sign_event":
console.log("Calling nostr.signEvent() with:", request.params);
result = await window.nostr.signEvent(request.params);
console.log("Got signed event:", result);
break;
case "nip04_encrypt":
console.log("Calling nostr.nip04.encrypt()");
result = await window.nostr.nip04.encrypt(
request.params.public_key,
request.params.content,
);
break;
case "nip04_decrypt":
console.log("Calling nostr.nip04.decrypt()");
result = await window.nostr.nip04.decrypt(
request.params.public_key,
request.params.content,
);
break;
case "nip44_encrypt":
console.log("Calling nostr.nip44.encrypt()");
result = await window.nostr.nip44.encrypt(
request.params.public_key,
request.params.content,
);
break;
case "nip44_decrypt":
console.log("Calling nostr.nip44.decrypt()");
result = await window.nostr.nip44.decrypt(
request.params.public_key,
request.params.content,
);
break;
default:
throw new Error(`Unknown method: ${request.method}`);
}
// Send response back to server
const responsePayload = {
id: request.id,
result: result,
error: null,
};
console.log("Sending response:", responsePayload);
await fetch("/api/response", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(responsePayload),
});
console.log("Response sent successfully");
updateStatus("Request processed successfully", "connected");
} catch (error) {
console.error("Error handling request:", error);
// Send error response back to server
const errorPayload = {
id: request.id,
result: null,
error: error.message,
};
console.log("Sending error response:", errorPayload);
await fetch("/api/response", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(errorPayload),
});
updateStatus("Error: " + error.message, "error");
}
}
function updateStatus(message, className) {
const statusEl = document.getElementById("status");
statusEl.textContent = message;
statusEl.className = className;
}
// Start polling when page loads
window.addEventListener("load", () => {
console.log("NIP-07 Proxy loaded");
// Check if NIP-07 extension is available
if (window.nostr) {
console.log("NIP-07 extension detected");
updateStatus("Connected to NIP-07 extension - Ready", "connected");
} else {
console.log("NIP-07 extension not found");
updateStatus("NIP-07 extension not found", "error");
}
// Start polling every 500 ms
setInterval(pollForRequests, 500);
});

View File

@@ -1,73 +0,0 @@
use std::{fmt, io};
use hyper::http;
use nostr::event;
use oneshot::RecvError;
/// Error
#[derive(Debug)]
pub enum Error {
/// I/O error
Io(io::Error),
/// HTTP error
Http(http::Error),
/// Json error
Json(serde_json::Error),
/// Event error
Event(event::Error),
/// Oneshot channel receive error
OneShotRecv(RecvError),
/// Generic error
Generic(String),
/// Timeout
Timeout,
/// The server is shutdown
Shutdown,
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "{e}"),
Self::Http(e) => write!(f, "{e}"),
Self::Json(e) => write!(f, "{e}"),
Self::Event(e) => write!(f, "{e}"),
Self::OneShotRecv(e) => write!(f, "{e}"),
Self::Generic(e) => write!(f, "{e}"),
Self::Timeout => write!(f, "timeout"),
Self::Shutdown => write!(f, "server is shutdown"),
}
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}
impl From<http::Error> for Error {
fn from(e: http::Error) -> Self {
Self::Http(e)
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Self::Json(e)
}
}
impl From<event::Error> for Error {
fn from(e: event::Error) -> Self {
Self::Event(e)
}
}
impl From<RecvError> for Error {
fn from(e: RecvError) -> Self {
Self::OneShotRecv(e)
}
}

View File

@@ -1,678 +0,0 @@
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener};
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use atomic_destructor::{AtomicDestroyer, AtomicDestructor};
use bytes::Bytes;
use futures::FutureExt;
use http_body_util::combinators::BoxBody;
use http_body_util::{BodyExt, Full};
use hyper::body::Incoming;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use nostr::prelude::{BoxedFuture, SignerBackend};
use nostr::{Event, NostrSigner, PublicKey, SignerError, UnsignedEvent};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize, Serializer};
use serde_json::{json, Value};
use smol::io::{AsyncRead, AsyncWrite};
use smol::lock::Mutex;
use uuid::Uuid;
use crate::error::Error;
mod error;
const HTML: &str = include_str!("../index.html");
const JS: &str = include_str!("../proxy.js");
const CSS: &str = include_str!("../style.css");
/// Wrapper to make smol::Async<TcpStream> compatible with hyper
struct HyperIo<T> {
inner: T,
}
impl<T> HyperIo<T> {
fn new(inner: T) -> Self {
Self { inner }
}
}
impl<T: AsyncRead + Unpin> hyper::rt::Read for HyperIo<T> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
mut buf: hyper::rt::ReadBufCursor<'_>,
) -> Poll<Result<(), std::io::Error>> {
let mut tbuf = vec![0; buf.remaining()];
match Pin::new(&mut self.inner).poll_read(cx, &mut tbuf) {
Poll::Ready(Ok(n)) => {
buf.put_slice(&tbuf[..n]);
Poll::Ready(Ok(()))
}
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
Poll::Pending => Poll::Pending,
}
}
}
impl<T: AsyncWrite + Unpin> hyper::rt::Write for HyperIo<T> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
Pin::new(&mut self.inner).poll_write(cx, buf)
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut self.inner).poll_flush(cx)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut self.inner).poll_close(cx)
}
}
type PendingResponseMap = HashMap<Uuid, oneshot::Sender<Result<Value, String>>>;
#[derive(Debug, Deserialize)]
struct Message {
id: Uuid,
error: Option<String>,
result: Option<Value>,
}
impl Message {
fn into_result(self) -> Result<Value, String> {
if let Some(error) = self.error {
Err(error)
} else {
Ok(self.result.unwrap_or(Value::Null))
}
}
}
#[derive(Debug, Clone, Copy)]
enum RequestMethod {
GetPublicKey,
SignEvent,
Nip04Encrypt,
Nip04Decrypt,
Nip44Encrypt,
Nip44Decrypt,
}
impl RequestMethod {
fn as_str(&self) -> &str {
match self {
Self::GetPublicKey => "get_public_key",
Self::SignEvent => "sign_event",
Self::Nip04Encrypt => "nip04_encrypt",
Self::Nip04Decrypt => "nip04_decrypt",
Self::Nip44Encrypt => "nip44_encrypt",
Self::Nip44Decrypt => "nip44_decrypt",
}
}
}
impl Serialize for RequestMethod {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
#[derive(Debug, Clone, Serialize)]
struct RequestData {
id: Uuid,
method: RequestMethod,
params: Value,
}
impl RequestData {
#[inline]
fn new(method: RequestMethod, params: Value) -> Self {
Self {
id: Uuid::new_v4(),
method,
params,
}
}
}
#[derive(Serialize)]
struct Requests<'a> {
requests: &'a [RequestData],
}
impl<'a> Requests<'a> {
#[inline]
fn new(requests: &'a [RequestData]) -> Self {
Self { requests }
}
#[inline]
fn len(&self) -> usize {
self.requests.len()
}
}
/// Params for NIP-04 and NIP-44 encryption/decryption
#[derive(Serialize)]
struct CryptoParams<'a> {
public_key: &'a PublicKey,
content: &'a str,
}
impl<'a> CryptoParams<'a> {
#[inline]
fn new(public_key: &'a PublicKey, content: &'a str) -> Self {
Self {
public_key,
content,
}
}
}
#[derive(Debug)]
struct ProxyState {
/// Requests waiting to be picked up by browser
pub outgoing_requests: Mutex<Vec<RequestData>>,
/// Map of request ID to response sender
pub pending_responses: Mutex<PendingResponseMap>,
/// Last time the client ask for the pending requests
pub last_pending_request: Arc<AtomicU64>,
/// Notification for shutdown
pub shutdown_notify: smol::channel::Receiver<()>,
pub shutdown_sender: smol::channel::Sender<()>,
}
/// Configuration options for [`BrowserSignerProxy`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrowserSignerProxyOptions {
/// Request timeout for the signer extension. Default is 30 seconds.
pub timeout: Duration,
/// Proxy server IP address and port. Default is `127.0.0.1:7400`.
pub addr: SocketAddr,
}
#[derive(Debug, Clone)]
struct InnerBrowserSignerProxy {
/// Configuration options for the proxy
options: BrowserSignerProxyOptions,
/// Internal state of the proxy including request queues
state: Arc<ProxyState>,
/// Flag to indicate if the server is shutdown
is_shutdown: Arc<AtomicBool>,
/// Flat indicating if the server is started
is_started: Arc<AtomicBool>,
}
impl AtomicDestroyer for InnerBrowserSignerProxy {
fn on_destroy(&self) {
self.shutdown();
}
}
impl InnerBrowserSignerProxy {
#[inline]
fn is_shutdown(&self) -> bool {
self.is_shutdown.load(Ordering::SeqCst)
}
fn shutdown(&self) {
// Mark the server as shutdown
self.is_shutdown.store(true, Ordering::SeqCst);
// Notify all waiters that the proxy is shutting down
let _ = self.state.shutdown_sender.try_send(());
}
}
/// Nostr Browser Signer Proxy
///
/// Proxy to use Nostr Browser signer (NIP-07) in native applications.
#[derive(Debug, Clone)]
pub struct BrowserSignerProxy {
inner: AtomicDestructor<InnerBrowserSignerProxy>,
}
impl Default for BrowserSignerProxyOptions {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
// 7 for NIP-07 and 400 because the NIP title is 40 bytes :)
addr: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 7400)),
}
}
}
impl BrowserSignerProxyOptions {
/// Sets the timeout duration.
pub const fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
/// Sets the IP address.
pub const fn ip_addr(mut self, new_ip: IpAddr) -> Self {
self.addr = SocketAddr::new(new_ip, self.addr.port());
self
}
/// Sets the port number.
pub const fn port(mut self, new_port: u16) -> Self {
self.addr = SocketAddr::new(self.addr.ip(), new_port);
self
}
}
impl BrowserSignerProxy {
/// Construct a new browser signer proxy
pub fn new(options: BrowserSignerProxyOptions) -> Self {
let (shutdown_sender, shutdown_notify) = smol::channel::unbounded();
let state = ProxyState {
outgoing_requests: Mutex::new(Vec::new()),
pending_responses: Mutex::new(HashMap::new()),
last_pending_request: Arc::new(AtomicU64::new(0)),
shutdown_notify,
shutdown_sender,
};
Self {
inner: AtomicDestructor::new(InnerBrowserSignerProxy {
options,
state: Arc::new(state),
is_shutdown: Arc::new(AtomicBool::new(false)),
is_started: Arc::new(AtomicBool::new(false)),
}),
}
}
/// Indicates whether the server is currently running.
#[inline]
pub fn is_started(&self) -> bool {
self.inner.is_started.load(Ordering::SeqCst)
}
/// Checks if there is an open browser tap ready to respond to requests by
/// verifying the time since the last pending request.
#[inline]
pub fn is_session_active(&self) -> bool {
current_time() - self.inner.state.last_pending_request.load(Ordering::SeqCst) < 2
}
/// Get the signer proxy webpage URL
#[inline]
pub fn url(&self) -> String {
format!("http://{}", self.inner.options.addr)
}
/// Start the proxy
///
/// If this is not called, will be automatically started on the first interaction with the signer.
pub async fn start(&self) -> Result<(), Error> {
// Ensure is not shutdown
if self.inner.is_shutdown() {
return Err(Error::Shutdown);
}
// Mark the proxy as started and check if was already started
let is_started: bool = self.inner.is_started.swap(true, Ordering::SeqCst);
// Immediately return if already started
if is_started {
return Ok(());
}
let listener = match smol::Async::<TcpListener>::bind(self.inner.options.addr) {
Ok(listener) => listener,
Err(e) => {
// Undo the started flag if binding fails
self.inner.is_started.store(false, Ordering::SeqCst);
// Propagate error
return Err(Error::from(e));
}
};
let addr: SocketAddr = self.inner.options.addr;
let state: Arc<ProxyState> = self.inner.state.clone();
smol::spawn(async move {
log::info!("Starting proxy server on {addr}");
loop {
futures::select! {
accept_result = listener.accept().fuse() => {
let (stream, _) = match accept_result {
Ok(conn) => conn,
Err(e) => {
log::error!("Failed to accept connection: {e}");
continue;
}
};
let io = HyperIo::new(stream);
let state: Arc<ProxyState> = state.clone();
let shutdown_notify = state.shutdown_notify.clone();
smol::spawn(async move {
let service = service_fn(move |req| {
handle_request(req, state.clone())
});
futures::select! {
res = http1::Builder::new().serve_connection(io, service).fuse() => {
if let Err(e) = res {
log::error!("Error serving connection: {e}");
}
}
_ = shutdown_notify.recv().fuse() => {
log::debug!("Closing connection, proxy server is shutting down.");
}
}
}).detach();
},
_ = state.shutdown_notify.recv().fuse() => {
break;
}
}
}
log::info!("Shutting down proxy server.");
}).detach();
Ok(())
}
#[inline]
async fn store_pending_response(&self, id: Uuid, tx: oneshot::Sender<Result<Value, String>>) {
let mut pending_responses = self.inner.state.pending_responses.lock().await;
pending_responses.insert(id, tx);
}
#[inline]
async fn store_outgoing_request(&self, request: RequestData) {
let mut outgoing_requests = self.inner.state.outgoing_requests.lock().await;
outgoing_requests.push(request);
}
async fn request<T>(&self, method: RequestMethod, params: Value) -> Result<T, Error>
where
T: DeserializeOwned,
{
// Start the proxy if not already started
self.start().await?;
// Construct the request
let request: RequestData = RequestData::new(method, params);
// Create a oneshot channel
let (tx, rx) = oneshot::channel();
// Store the response sender
self.store_pending_response(request.id, tx).await;
// Add to outgoing requests queue
self.store_outgoing_request(request).await;
// Wait for response
let timeout_fut = smol::Timer::after(self.inner.options.timeout);
let recv_fut = rx;
match futures::future::select(timeout_fut, recv_fut).await {
futures::future::Either::Left(_) => Err(Error::Timeout),
futures::future::Either::Right((recv_result, _)) => {
match recv_result.map_err(|_| Error::Generic("Channel closed".to_string()))? {
Ok(res) => Ok(serde_json::from_value(res)?),
Err(error) => Err(Error::Generic(error)),
}
}
}
}
#[inline]
async fn _get_public_key(&self) -> Result<PublicKey, Error> {
self.request(RequestMethod::GetPublicKey, json!({})).await
}
#[inline]
async fn _sign_event(&self, event: UnsignedEvent) -> Result<Event, Error> {
let event: Event = self
.request(RequestMethod::SignEvent, serde_json::to_value(event)?)
.await?;
event.verify()?;
Ok(event)
}
#[inline]
async fn _nip04_encrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
let params = CryptoParams::new(public_key, content);
self.request(RequestMethod::Nip04Encrypt, serde_json::to_value(params)?)
.await
}
#[inline]
async fn _nip04_decrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
let params = CryptoParams::new(public_key, content);
self.request(RequestMethod::Nip04Decrypt, serde_json::to_value(params)?)
.await
}
#[inline]
async fn _nip44_encrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
let params = CryptoParams::new(public_key, content);
self.request(RequestMethod::Nip44Encrypt, serde_json::to_value(params)?)
.await
}
#[inline]
async fn _nip44_decrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
let params = CryptoParams::new(public_key, content);
self.request(RequestMethod::Nip44Decrypt, serde_json::to_value(params)?)
.await
}
}
impl NostrSigner for BrowserSignerProxy {
fn backend(&self) -> SignerBackend {
SignerBackend::BrowserExtension
}
#[inline]
fn get_public_key(&self) -> BoxedFuture<Result<PublicKey, SignerError>> {
Box::pin(async move { self._get_public_key().await.map_err(SignerError::backend) })
}
#[inline]
fn sign_event(&self, unsigned: UnsignedEvent) -> BoxedFuture<Result<Event, SignerError>> {
Box::pin(async move {
self._sign_event(unsigned)
.await
.map_err(SignerError::backend)
})
}
#[inline]
fn nip04_encrypt<'a>(
&'a self,
public_key: &'a PublicKey,
content: &'a str,
) -> BoxedFuture<'a, Result<String, SignerError>> {
Box::pin(async move {
self._nip04_encrypt(public_key, content)
.await
.map_err(SignerError::backend)
})
}
#[inline]
fn nip04_decrypt<'a>(
&'a self,
public_key: &'a PublicKey,
encrypted_content: &'a str,
) -> BoxedFuture<'a, Result<String, SignerError>> {
Box::pin(async move {
self._nip04_decrypt(public_key, encrypted_content)
.await
.map_err(SignerError::backend)
})
}
#[inline]
fn nip44_encrypt<'a>(
&'a self,
public_key: &'a PublicKey,
content: &'a str,
) -> BoxedFuture<'a, Result<String, SignerError>> {
Box::pin(async move {
self._nip44_encrypt(public_key, content)
.await
.map_err(SignerError::backend)
})
}
#[inline]
fn nip44_decrypt<'a>(
&'a self,
public_key: &'a PublicKey,
payload: &'a str,
) -> BoxedFuture<'a, Result<String, SignerError>> {
Box::pin(async move {
self._nip44_decrypt(public_key, payload)
.await
.map_err(SignerError::backend)
})
}
}
async fn handle_request(
req: Request<Incoming>,
state: Arc<ProxyState>,
) -> Result<Response<BoxBody<Bytes, Error>>, Error> {
match (req.method(), req.uri().path()) {
// Serve the HTML proxy page
(&Method::GET, "/") => Ok(Response::builder()
.header("Content-Type", "text/html")
.body(full(HTML))?),
// Serve the CSS page style
(&Method::GET, "/style.css") => Ok(Response::builder()
.header("Content-Type", "text/css")
.body(full(CSS))?),
// Serve the JS proxy script
(&Method::GET, "/proxy.js") => Ok(Response::builder()
.header("Content-Type", "application/javascript")
.body(full(JS))?),
// Browser polls this endpoint to get pending requests
(&Method::GET, "/api/pending") => {
state
.last_pending_request
.store(current_time(), Ordering::SeqCst);
let mut outgoing = state.outgoing_requests.lock().await;
let requests: Requests<'_> = Requests::new(&outgoing);
let json: String = serde_json::to_string(&requests)?;
log::debug!("Sending {} pending requests to browser", requests.len());
// Clear the outgoing requests after sending them
outgoing.clear();
Ok(Response::builder()
.header("Content-Type", "application/json")
.header("Access-Control-Allow-Origin", "*")
.body(full(json))?)
}
// Get response
(&Method::POST, "/api/response") => {
// Correctly collect the body bytes from the stream
let body_bytes: Bytes = match req.into_body().collect().await {
Ok(collected) => collected.to_bytes(),
Err(e) => {
log::error!("Failed to read body: {e}");
let response = Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(full("Failed to read body"))?;
return Ok(response);
}
};
// Handle responses from the browser extension
let message: Message = match serde_json::from_slice(&body_bytes) {
Ok(json) => json,
Err(_) => {
let response = Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(full("Invalid JSON"))?;
return Ok(response);
}
};
log::debug!("Received response from browser: {message:?}");
let id: Uuid = message.id;
let mut pending = state.pending_responses.lock().await;
match pending.remove(&id) {
Some(sender) => {
let _ = sender.send(message.into_result());
}
None => log::warn!("No pending request found for {id}"),
}
let response = Response::builder()
.header("Access-Control-Allow-Origin", "*")
.body(full("OK"))?;
Ok(response)
}
(&Method::OPTIONS, _) => {
// Handle CORS preflight requests
let response = Response::builder()
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
.header("Access-Control-Allow-Headers", "Content-Type")
.body(full(""))?;
Ok(response)
}
// 404 - not found
_ => {
let response = Response::builder()
.status(StatusCode::NOT_FOUND)
.body(full("Not Found"))?;
Ok(response)
}
}
}
#[inline]
fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, Error> {
Full::new(chunk.into())
.map_err(|never| match never {})
.boxed()
}
/// Gets the current time in seconds since the Unix epoch (1970-01-01). If the
/// time is before the epoch, returns 0.
#[inline]
fn current_time() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or_default()
}

View File

@@ -1,30 +0,0 @@
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans,
Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
padding: 20px;
background-color: #f0f0f0;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.connected {
color: green;
font-weight: bold;
}
.error {
color: red;
font-weight: bold;
}
.status-box {
background: #f9f9f9;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
border-left: 4px solid #ccc;
}

View File

@@ -1,5 +1,5 @@
[package] [package]
name = "app_state" name = "states"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
publish.workspace = true publish.workspace = true

View File

@@ -1,9 +1,6 @@
use std::sync::OnceLock; use std::sync::OnceLock;
use std::time::Duration;
use nostr_lmdb::NostrLMDB;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use paths::nostr_file;
use crate::state::AppState; use crate::state::AppState;
@@ -12,7 +9,6 @@ pub mod paths;
pub mod state; pub mod state;
static APP_STATE: OnceLock<AppState> = OnceLock::new(); static APP_STATE: OnceLock<AppState> = OnceLock::new();
static NOSTR_CLIENT: OnceLock<Client> = OnceLock::new();
static NIP65_RELAYS: OnceLock<Vec<(RelayUrl, Option<RelayMetadata>)>> = OnceLock::new(); static NIP65_RELAYS: OnceLock<Vec<(RelayUrl, Option<RelayMetadata>)>> = OnceLock::new();
static NIP17_RELAYS: OnceLock<Vec<RelayUrl>> = OnceLock::new(); static NIP17_RELAYS: OnceLock<Vec<RelayUrl>> = OnceLock::new();
@@ -21,31 +17,7 @@ pub fn app_state() -> &'static AppState {
APP_STATE.get_or_init(AppState::new) APP_STATE.get_or_init(AppState::new)
} }
/// Initialize the nostr client. /// Default NIP-65 Relays. Used for new account
pub fn nostr_client() -> &'static Client {
NOSTR_CLIENT.get_or_init(|| {
// rustls uses the `aws_lc_rs` provider by default
// This only errors if the default provider has already
// been installed. We can ignore this `Result`.
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.ok();
let lmdb = NostrLMDB::open(nostr_file()).expect("Database is NOT initialized");
let opts = ClientOptions::new()
.gossip(true)
.automatic_authentication(false)
.verify_subscriptions(false)
.sleep_when_idle(SleepWhenIdle::Enabled {
timeout: Duration::from_secs(600),
});
ClientBuilder::default().database(lmdb).opts(opts).build()
})
}
/// Default NIP65 Relays. Used for new account
pub fn default_nip65_relays() -> &'static Vec<(RelayUrl, Option<RelayMetadata>)> { pub fn default_nip65_relays() -> &'static Vec<(RelayUrl, Option<RelayMetadata>)> {
NIP65_RELAYS.get_or_init(|| { NIP65_RELAYS.get_or_init(|| {
vec![ vec![
@@ -71,7 +43,7 @@ pub fn default_nip65_relays() -> &'static Vec<(RelayUrl, Option<RelayMetadata>)>
}) })
} }
/// Default NIP17 Relays. Used for new account /// Default NIP-17 Relays. Used for new account
pub fn default_nip17_relays() -> &'static Vec<RelayUrl> { pub fn default_nip17_relays() -> &'static Vec<RelayUrl> {
NIP17_RELAYS.get_or_init(|| { NIP17_RELAYS.get_or_init(|| {
vec![ vec![

View File

@@ -5,14 +5,16 @@ use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use flume::{Receiver, Sender}; use flume::{Receiver, Sender};
use nostr_lmdb::NostrLMDB;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smol::lock::RwLock; use smol::lock::RwLock;
use crate::constants::{ use crate::constants::{
BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT, SEARCH_RELAYS, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT, SEARCH_RELAYS,
}; };
use crate::nostr_client; use crate::paths::config_dir;
use crate::paths::support_dir;
const TIMEOUT: u64 = 5;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct AuthRequest { pub struct AuthRequest {
@@ -51,9 +53,6 @@ pub enum SignalKind {
/// A signal to notify UI that the relay requires authentication /// A signal to notify UI that the relay requires authentication
Auth(AuthRequest), Auth(AuthRequest),
/// A signal to notify UI that the browser proxy service is down
ProxyDown,
/// A signal to notify UI that a new profile has been received /// A signal to notify UI that a new profile has been received
NewProfile(Profile), NewProfile(Profile),
@@ -70,7 +69,7 @@ pub enum SignalKind {
GiftWrapStatus(UnwrappingStatus), GiftWrapStatus(UnwrappingStatus),
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Signal { pub struct Signal {
rx: Receiver<SignalKind>, rx: Receiver<SignalKind>,
tx: Sender<SignalKind>, tx: Sender<SignalKind>,
@@ -103,7 +102,7 @@ impl Signal {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Ingester { pub struct Ingester {
rx: Receiver<PublicKey>, rx: Receiver<PublicKey>,
tx: Sender<PublicKey>, tx: Sender<PublicKey>,
@@ -165,32 +164,25 @@ impl EventTracker {
} }
} }
/// A simple storage to store all states that using across the application.
#[derive(Debug)] #[derive(Debug)]
pub struct AppState { pub struct AppState {
/// A client to interact with Nostr
client: Client,
/// Tracks activity related to Nostr events
event_tracker: RwLock<EventTracker>,
/// Signal channel for communication between Nostr and GPUI
signal: Signal,
/// Ingester channel for processing public keys
ingester: Ingester,
/// The timestamp when the application was initialized. /// The timestamp when the application was initialized.
pub initialized_at: Timestamp, pub initialized_at: Timestamp,
/// Whether this is the first run of the application.
pub is_first_run: AtomicBool,
/// Whether gift wrap processing is in progress. /// Whether gift wrap processing is in progress.
pub gift_wrap_processing: AtomicBool, pub gift_wrap_processing: AtomicBool,
/// Subscription ID for listening to gift wrap events from relays.
pub gift_wrap_sub_id: SubscriptionId,
/// Auto-close options for relay subscriptions
pub auto_close_opts: Option<SubscribeAutoCloseOptions>,
/// Tracks activity related to Nostr events
pub event_tracker: RwLock<EventTracker>,
/// Signal channel for communication between Nostr and GPUI
pub signal: Signal,
/// Ingester channel for processing public keys
pub ingester: Ingester,
} }
impl Default for AppState { impl Default for AppState {
@@ -201,28 +193,127 @@ impl Default for AppState {
impl AppState { impl AppState {
pub fn new() -> Self { pub fn new() -> Self {
let first_run = Self::first_run(); // rustls uses the `aws_lc_rs` provider by default
let initialized_at = Timestamp::now(); // This only errors if the default provider has already
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); // been installed. We can ignore this `Result`.
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.ok();
let lmdb =
NostrLMDB::open(config_dir().join("nostr")).expect("Database is NOT initialized");
let opts = ClientOptions::new()
.gossip(true)
.automatic_authentication(false)
.verify_subscriptions(false)
.sleep_when_idle(SleepWhenIdle::Enabled {
timeout: Duration::from_secs(600),
});
let client = ClientBuilder::default().database(lmdb).opts(opts).build();
let event_tracker = RwLock::new(EventTracker::default());
let signal = Signal::default(); let signal = Signal::default();
let ingester = Ingester::default(); let ingester = Ingester::default();
Self { Self {
initialized_at, client,
event_tracker,
signal, signal,
ingester, ingester,
is_first_run: AtomicBool::new(first_run), initialized_at: Timestamp::now(),
gift_wrap_sub_id: SubscriptionId::new("inbox"),
gift_wrap_processing: AtomicBool::new(false), gift_wrap_processing: AtomicBool::new(false),
auto_close_opts: Some(opts),
event_tracker: RwLock::new(EventTracker::default()),
} }
} }
pub async fn handle_notifications(&self) -> Result<(), Error> { /// Returns a reference to the nostr client
let client = nostr_client(); pub fn client(&'static self) -> &'static Client {
&self.client
}
/// Returns a reference to the event tracker
pub fn tracker(&'static self) -> &'static RwLock<EventTracker> {
&self.event_tracker
}
/// Returns a reference to the signal channel
pub fn signal(&'static self) -> &'static Signal {
&self.signal
}
/// Returns a reference to the ingester channel
pub fn ingester(&'static self) -> &'static Ingester {
&self.ingester
}
/// Observes the signer and notifies the app when it's set
pub async fn observe_signer(&'static self) {
let client = self.client();
let loop_duration = Duration::from_millis(800);
loop {
if let Ok(signer) = client.signer().await {
if let Ok(pk) = signer.get_public_key().await {
// Notify the app that the signer has been set
self.signal().send(SignalKind::SignerSet(pk)).await;
// Get user's gossip relays
self.get_nip65(pk).await.ok();
// Exit the current loop
break;
}
}
smol::Timer::after(loop_duration).await;
}
}
/// Observes the gift wrap status and notifies the app when it's set
pub async fn observe_giftwrap(&'static self) {
let client = self.client();
let loop_duration = Duration::from_secs(20);
let mut is_start_processing = false;
let mut total_loops = 0;
loop {
if client.has_signer().await {
total_loops += 1;
if self.gift_wrap_processing.load(Ordering::Acquire) {
is_start_processing = true;
// Reset gift wrap processing flag
let _ = self.gift_wrap_processing.compare_exchange(
true,
false,
Ordering::Release,
Ordering::Relaxed,
);
let signal = SignalKind::GiftWrapStatus(UnwrappingStatus::Processing);
self.signal().send(signal).await;
} else {
// Only run further if we are already processing
// Wait until after 2 loops to prevent exiting early while events are still being processed
if is_start_processing && total_loops >= 2 {
let signal = SignalKind::GiftWrapStatus(UnwrappingStatus::Complete);
self.signal().send(signal).await;
// Reset the counter
is_start_processing = false;
total_loops = 0;
}
}
}
smol::Timer::after(loop_duration).await;
}
}
/// Handles events from the nostr client
pub async fn handle_notifications(&self) -> Result<(), Error> {
// Get all bootstrapping relays // Get all bootstrapping relays
let mut urls = vec![]; let mut urls = vec![];
urls.extend(BOOTSTRAP_RELAYS); urls.extend(BOOTSTRAP_RELAYS);
@@ -230,15 +321,15 @@ impl AppState {
// Add relay to the relay pool // Add relay to the relay pool
for url in urls.into_iter() { for url in urls.into_iter() {
client.add_relay(url).await?; self.client.add_relay(url).await?;
} }
// Establish connection to relays // Establish connection to relays
client.connect().await; self.client.connect().await;
let mut processed_events: HashSet<EventId> = HashSet::new(); let mut processed_events: HashSet<EventId> = HashSet::new();
let mut challenges: HashSet<Cow<'_, str>> = HashSet::new(); let mut challenges: HashSet<Cow<'_, str>> = HashSet::new();
let mut notifications = client.notifications(); let mut notifications = self.client.notifications();
while let Ok(notification) = notifications.recv().await { while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, relay_url } = notification else { let RelayPoolNotification::Message { message, relay_url } = notification else {
@@ -265,7 +356,7 @@ impl AppState {
match event.kind { match event.kind {
Kind::RelayList => { Kind::RelayList => {
// Get events if relay list belongs to current user // Get events if relay list belongs to current user
if let Ok(true) = Self::is_self_authored(&event).await { if let Ok(true) = self.is_self_authored(&event).await {
let author = event.pubkey; let author = event.pubkey;
// Fetch user's metadata event // Fetch user's metadata event
@@ -286,7 +377,7 @@ impl AppState {
} }
Kind::InboxRelays => { Kind::InboxRelays => {
// Subscribe to gift wrap events if messaging relays belong to the current user // Subscribe to gift wrap events if messaging relays belong to the current user
if let Ok(true) = Self::is_self_authored(&event).await { if let Ok(true) = self.is_self_authored(&event).await {
let urls: Vec<RelayUrl> = let urls: Vec<RelayUrl> =
nip17::extract_relay_list(event.as_ref()).cloned().collect(); nip17::extract_relay_list(event.as_ref()).cloned().collect();
@@ -296,7 +387,7 @@ impl AppState {
} }
} }
Kind::ContactList => { Kind::ContactList => {
if let Ok(true) = Self::is_self_authored(&event).await { if let Ok(true) = self.is_self_authored(&event).await {
let public_keys: HashSet<PublicKey> = let public_keys: HashSet<PublicKey> =
event.tags.public_keys().copied().collect(); event.tags.public_keys().copied().collect();
@@ -318,7 +409,7 @@ impl AppState {
} }
} }
RelayMessage::EndOfStoredEvents(subscription_id) => { RelayMessage::EndOfStoredEvents(subscription_id) => {
if *subscription_id == self.gift_wrap_sub_id { if subscription_id.as_ref() == &SubscriptionId::new("inbox") {
self.signal self.signal
.send(SignalKind::GiftWrapStatus(UnwrappingStatus::Processing)) .send(SignalKind::GiftWrapStatus(UnwrappingStatus::Processing))
.await; .await;
@@ -353,6 +444,7 @@ impl AppState {
Ok(()) Ok(())
} }
/// Batch metadata requests into a single subscription
pub async fn handle_metadata_batching(&self) { pub async fn handle_metadata_batching(&self) {
let timeout = Duration::from_millis(METADATA_BATCH_TIMEOUT); let timeout = Duration::from_millis(METADATA_BATCH_TIMEOUT);
let mut processed_pubkeys: HashSet<PublicKey> = HashSet::new(); let mut processed_pubkeys: HashSet<PublicKey> = HashSet::new();
@@ -411,41 +503,46 @@ impl AppState {
} }
} }
async fn is_self_authored(event: &Event) -> Result<bool, Error> { /// Check if event is published by current user
let client = nostr_client(); async fn is_self_authored(&self, event: &Event) -> Result<bool, Error> {
let signer = client.signer().await?; let signer = self.client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
Ok(public_key == event.pubkey) Ok(public_key == event.pubkey)
} }
/// Subscribe for events that match the given kind for a given author /// Subscribe for events that match the given kind for a given author
async fn subscribe(&self, author: PublicKey, kind: Kind) -> Result<(), Error> { pub async fn subscribe(&self, author: PublicKey, kind: Kind) -> Result<(), Error> {
let client = nostr_client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let filter = Filter::new().author(author).kind(kind).limit(1); let filter = Filter::new().author(author).kind(kind).limit(1);
// Subscribe to filters from the user's write relays // Subscribe to filters from the user's write relays
client.subscribe(filter, Some(opts)).await?; self.client.subscribe(filter, Some(opts)).await?;
Ok(()) Ok(())
} }
/// Get metadata for a list of public keys /// Get metadata for a list of public keys
async fn get_metadata_for_list(&self, public_keys: HashSet<PublicKey>) -> Result<(), Error> { pub async fn get_metadata_for_list<I>(&self, public_keys: I) -> Result<(), Error>
if public_keys.is_empty() { where
return Err(anyhow!("You need at least one public key")); I: IntoIterator<Item = PublicKey>,
{
let authors: Vec<PublicKey> = public_keys.into_iter().collect();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
// Return if the list is empty
if authors.is_empty() {
return Err(anyhow!("You need at least one public key".to_string(),));
} }
let client = nostr_client(); let filter = Filter::new()
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); .limit(authors.len() * kinds.len() + 20)
.authors(authors)
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; .kinds(kinds);
let limit = public_keys.len() * kinds.len() + 20;
let filter = Filter::new().authors(public_keys).kinds(kinds).limit(limit);
// Subscribe to filters to the bootstrap relays // Subscribe to filters to the bootstrap relays
client self.client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await?; .await?;
@@ -454,9 +551,7 @@ impl AppState {
/// Get and verify NIP-65 relays for a given public key /// Get and verify NIP-65 relays for a given public key
pub async fn get_nip65(&self, public_key: PublicKey) -> Result<(), Error> { pub async fn get_nip65(&self, public_key: PublicKey) -> Result<(), Error> {
let client = nostr_client(); let timeout = Duration::from_secs(TIMEOUT);
let tx = self.signal.sender().clone();
let timeout = Duration::from_secs(5);
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let filter = Filter::new() let filter = Filter::new()
@@ -465,15 +560,18 @@ impl AppState {
.limit(1); .limit(1);
// Subscribe to events from the bootstrapping relays // Subscribe to events from the bootstrapping relays
client self.client
.subscribe_to(BOOTSTRAP_RELAYS, filter.clone(), Some(opts)) .subscribe_to(BOOTSTRAP_RELAYS, filter.clone(), Some(opts))
.await?; .await?;
let tx = self.signal.sender().clone();
let database = self.client.database().clone();
// Verify the received data after a timeout // Verify the received data after a timeout
smol::spawn(async move { smol::spawn(async move {
smol::Timer::after(timeout).await; smol::Timer::after(timeout).await;
if client.database().count(filter).await.unwrap_or(0) < 1 { if database.count(filter).await.unwrap_or(0) < 1 {
tx.send_async(SignalKind::GossipRelaysNotFound).await.ok(); tx.send_async(SignalKind::GossipRelaysNotFound).await.ok();
} }
}) })
@@ -487,12 +585,12 @@ impl AppState {
&self, &self,
relays: &[(RelayUrl, Option<RelayMetadata>)], relays: &[(RelayUrl, Option<RelayMetadata>)],
) -> Result<(), Error> { ) -> Result<(), Error> {
let client = nostr_client(); let signer = self.client.signer().await?;
let signer = client.signer().await?;
let tags: Vec<Tag> = relays let tags: Vec<Tag> = relays
.iter() .iter()
.map(|(url, metadata)| Tag::relay_metadata(url.to_owned(), metadata.to_owned())) .cloned()
.map(|(url, metadata)| Tag::relay_metadata(url, metadata))
.collect(); .collect();
let event = EventBuilder::new(Kind::RelayList, "") let event = EventBuilder::new(Kind::RelayList, "")
@@ -501,7 +599,7 @@ impl AppState {
.await?; .await?;
// Send event to the public relays // Send event to the public relays
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?; self.client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
// Get NIP-17 relays // Get NIP-17 relays
self.get_nip17(event.pubkey).await?; self.get_nip17(event.pubkey).await?;
@@ -511,9 +609,7 @@ impl AppState {
/// Get and verify NIP-17 relays for a given public key /// Get and verify NIP-17 relays for a given public key
pub async fn get_nip17(&self, public_key: PublicKey) -> Result<(), Error> { pub async fn get_nip17(&self, public_key: PublicKey) -> Result<(), Error> {
let client = nostr_client(); let timeout = Duration::from_secs(TIMEOUT);
let tx = self.signal.sender().clone();
let timeout = Duration::from_secs(5);
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let filter = Filter::new() let filter = Filter::new()
@@ -522,13 +618,16 @@ impl AppState {
.limit(1); .limit(1);
// Subscribe to events from the bootstrapping relays // Subscribe to events from the bootstrapping relays
client.subscribe(filter.clone(), Some(opts)).await?; self.client.subscribe(filter.clone(), Some(opts)).await?;
let tx = self.signal.sender().clone();
let database = self.client.database().clone();
// Verify the received data after a timeout // Verify the received data after a timeout
smol::spawn(async move { smol::spawn(async move {
smol::Timer::after(timeout).await; smol::Timer::after(timeout).await;
if client.database().count(filter).await.unwrap_or(0) < 1 { if database.count(filter).await.unwrap_or(0) < 1 {
tx.send_async(SignalKind::MessagingRelaysNotFound) tx.send_async(SignalKind::MessagingRelaysNotFound)
.await .await
.ok(); .ok();
@@ -541,26 +640,28 @@ impl AppState {
/// Set NIP-17 relays for a current user /// Set NIP-17 relays for a current user
pub async fn set_nip17(&self, relays: &[RelayUrl]) -> Result<(), Error> { pub async fn set_nip17(&self, relays: &[RelayUrl]) -> Result<(), Error> {
let client = nostr_client(); let signer = self.client.signer().await?;
let signer = client.signer().await?;
let event = EventBuilder::new(Kind::InboxRelays, "") let event = EventBuilder::new(Kind::InboxRelays, "")
.tags(relays.iter().map(|relay| Tag::relay(relay.to_owned()))) .tags(relays.iter().cloned().map(Tag::relay))
.sign(&signer) .sign(&signer)
.await?; .await?;
// Send event to the public relays // Send event to the public relays
client.send_event(&event).await?; self.client.send_event(&event).await?;
// Run inbox monitor // Get all gift wrap events after published event
self.get_messages(event.pubkey, relays).await?; self.get_messages(event.pubkey, relays).await?;
Ok(()) Ok(())
} }
/// Get all gift wrap events in the messaging relays for a given public key /// Get all gift wrap events in the messaging relays for a given public key
async fn get_messages(&self, public_key: PublicKey, urls: &[RelayUrl]) -> Result<(), Error> { pub async fn get_messages(
let client = nostr_client(); &self,
public_key: PublicKey,
urls: &[RelayUrl],
) -> Result<(), Error> {
let id = SubscriptionId::new("inbox"); let id = SubscriptionId::new("inbox");
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
@@ -571,22 +672,22 @@ impl AppState {
// Ensure connection to relays // Ensure connection to relays
for url in urls.iter() { for url in urls.iter() {
client.add_relay(url).await?; self.client.add_relay(url).await?;
client.connect_relay(url).await?; self.client.connect_relay(url).await?;
} }
// Subscribe to filters to user's messaging relays // Subscribe to filters to user's messaging relays
client.subscribe_with_id_to(urls, id, filter, None).await?; self.client
.subscribe_with_id_to(urls, id, filter, None)
.await?;
Ok(()) Ok(())
} }
/// Stores an unwrapped event in local database with reference to original /// Stores an unwrapped event in local database with reference to original
async fn set_rumor(&self, id: EventId, rumor: &Event) -> Result<(), Error> { async fn set_rumor(&self, id: EventId, rumor: &Event) -> Result<(), Error> {
let client = nostr_client();
// Save unwrapped event // Save unwrapped event
client.database().save_event(rumor).await?; self.client.database().save_event(rumor).await?;
// Create a reference event pointing to the unwrapped event // Create a reference event pointing to the unwrapped event
let event = EventBuilder::new(Kind::ApplicationSpecificData, "") let event = EventBuilder::new(Kind::ApplicationSpecificData, "")
@@ -595,23 +696,22 @@ impl AppState {
.await?; .await?;
// Save reference event // Save reference event
client.database().save_event(&event).await?; self.client.database().save_event(&event).await?;
Ok(()) Ok(())
} }
/// Retrieves a previously unwrapped event from local database /// Retrieves a previously unwrapped event from local database
async fn get_rumor(&self, id: EventId) -> Result<Event, Error> { async fn get_rumor(&self, id: EventId) -> Result<Event, Error> {
let client = nostr_client();
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::ApplicationSpecificData) .kind(Kind::ApplicationSpecificData)
.identifier(id) .identifier(id)
.limit(1); .limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() { if let Some(event) = self.client.database().query(filter).await?.first_owned() {
let target_id = event.tags.event_ids().collect::<Vec<_>>()[0]; let target_id = event.tags.event_ids().collect::<Vec<_>>()[0];
if let Some(event) = client.database().event_by_id(target_id).await? { if let Some(event) = self.client.database().event_by_id(target_id).await? {
Ok(event) Ok(event)
} else { } else {
Err(anyhow!("Event not found.")) Err(anyhow!("Event not found."))
@@ -623,13 +723,11 @@ impl AppState {
// Unwraps a gift-wrapped event and processes its contents. // Unwraps a gift-wrapped event and processes its contents.
async fn extract_rumor(&self, gift_wrap: &Event) { async fn extract_rumor(&self, gift_wrap: &Event) {
let client = nostr_client();
let mut rumor: Option<Event> = None; let mut rumor: Option<Event> = None;
if let Ok(event) = self.get_rumor(gift_wrap.id).await { if let Ok(event) = self.get_rumor(gift_wrap.id).await {
rumor = Some(event); rumor = Some(event);
} else if let Ok(unwrapped) = client.unwrap_gift_wrap(gift_wrap).await { } else if let Ok(unwrapped) = self.client.unwrap_gift_wrap(gift_wrap).await {
// Sign the unwrapped event with a RANDOM KEYS // Sign the unwrapped event with a RANDOM KEYS
if let Ok(event) = unwrapped.rumor.sign_with_keys(&Keys::generate()) { if let Ok(event) = unwrapped.rumor.sign_with_keys(&Keys::generate()) {
// Save this event to the database for future use. // Save this event to the database for future use.
@@ -661,9 +759,4 @@ impl AppState {
} }
} }
} }
fn first_run() -> bool {
let flag = support_dir().join(".first_run");
!flag.exists() && std::fs::write(&flag, "").is_ok()
}
} }