chore: improve data requests (#81)
* refactor * refactor * add documents * clean up * refactor * clean up * refactor identity * . * . * rename
This commit is contained in:
138
Cargo.lock
generated
138
Cargo.lock
generated
@@ -184,9 +184,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-channel"
|
name = "async-channel"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "16c74e56284d2188cabb6ad99603d1ace887a5d7e7b695d01b728155ed9ed427"
|
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"event-listener-strategy",
|
"event-listener-strategy",
|
||||||
@@ -730,9 +730,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blocking"
|
name = "blocking"
|
||||||
version = "1.6.1"
|
version = "1.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
|
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"async-task",
|
"async-task",
|
||||||
@@ -856,9 +856,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.27"
|
version = "1.2.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
|
checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jobserver",
|
"jobserver",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -935,29 +935,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chats"
|
|
||||||
version = "1.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"chrono",
|
|
||||||
"common",
|
|
||||||
"fuzzy-matcher",
|
|
||||||
"global",
|
|
||||||
"gpui",
|
|
||||||
"i18n",
|
|
||||||
"identity",
|
|
||||||
"itertools 0.13.0",
|
|
||||||
"log",
|
|
||||||
"nostr",
|
|
||||||
"nostr-sdk",
|
|
||||||
"oneshot",
|
|
||||||
"rust-i18n",
|
|
||||||
"settings",
|
|
||||||
"smallvec",
|
|
||||||
"smol",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.41"
|
version = "0.4.41"
|
||||||
@@ -1001,10 +978,8 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
"i18n",
|
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"rust-i18n",
|
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1100,7 +1075,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5253702200f4fb491b6608ac0a0c6df89b224b0f"
|
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1195,7 +1170,6 @@ version = "1.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"auto_update",
|
"auto_update",
|
||||||
"chats",
|
|
||||||
"client_keys",
|
"client_keys",
|
||||||
"common",
|
"common",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@@ -1210,6 +1184,7 @@ dependencies = [
|
|||||||
"nostr-connect",
|
"nostr-connect",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"oneshot",
|
"oneshot",
|
||||||
|
"registry",
|
||||||
"reqwest_client",
|
"reqwest_client",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"rust-i18n",
|
"rust-i18n",
|
||||||
@@ -1487,7 +1462,7 @@ 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#5253702200f4fb491b6608ac0a0c6df89b224b0f"
|
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2333,7 +2308,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5253702200f4fb491b6608ac0a0c6df89b224b0f"
|
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2425,7 +2400,7 @@ 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#5253702200f4fb491b6608ac0a0c6df89b224b0f"
|
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2648,7 +2623,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#5253702200f4fb491b6608ac0a0c6df89b224b0f"
|
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2665,7 +2640,7 @@ dependencies = [
|
|||||||
[[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#5253702200f4fb491b6608ac0a0c6df89b224b0f"
|
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -2733,9 +2708,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.14"
|
version = "0.1.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
|
checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2883,12 +2858,10 @@ dependencies = [
|
|||||||
"common",
|
"common",
|
||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
"i18n",
|
|
||||||
"log",
|
"log",
|
||||||
"nostr-connect",
|
"nostr-connect",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"oneshot",
|
"oneshot",
|
||||||
"rust-i18n",
|
|
||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"ui",
|
"ui",
|
||||||
@@ -3444,7 +3417,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5253702200f4fb491b6608ac0a0c6df89b224b0f"
|
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
@@ -3667,7 +3640,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.42.1"
|
version = "0.42.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#5a1aacb2fecdb4177574345a1b07ffbfa99e006d"
|
source = "git+https://github.com/rust-nostr/nostr#c4d16c691f5bc03448cf95bb8b2f59f7d5d0ca79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -3690,7 +3663,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.42.0"
|
version = "0.42.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#5a1aacb2fecdb4177574345a1b07ffbfa99e006d"
|
source = "git+https://github.com/rust-nostr/nostr#c4d16c691f5bc03448cf95bb8b2f59f7d5d0ca79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3702,7 +3675,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.42.0"
|
version = "0.42.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#5a1aacb2fecdb4177574345a1b07ffbfa99e006d"
|
source = "git+https://github.com/rust-nostr/nostr#c4d16c691f5bc03448cf95bb8b2f59f7d5d0ca79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -3713,7 +3686,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.42.0"
|
version = "0.42.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#5a1aacb2fecdb4177574345a1b07ffbfa99e006d"
|
source = "git+https://github.com/rust-nostr/nostr#c4d16c691f5bc03448cf95bb8b2f59f7d5d0ca79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"heed",
|
"heed",
|
||||||
@@ -3726,7 +3699,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-relay-pool"
|
name = "nostr-relay-pool"
|
||||||
version = "0.42.0"
|
version = "0.42.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#5a1aacb2fecdb4177574345a1b07ffbfa99e006d"
|
source = "git+https://github.com/rust-nostr/nostr#c4d16c691f5bc03448cf95bb8b2f59f7d5d0ca79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -3742,7 +3715,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.42.0"
|
version = "0.42.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#5a1aacb2fecdb4177574345a1b07ffbfa99e006d"
|
source = "git+https://github.com/rust-nostr/nostr#c4d16c691f5bc03448cf95bb8b2f59f7d5d0ca79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4749,7 +4722,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#5253702200f4fb491b6608ac0a0c6df89b224b0f"
|
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
@@ -4784,6 +4757,29 @@ version = "0.8.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "registry"
|
||||||
|
version = "1.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"common",
|
||||||
|
"fuzzy-matcher",
|
||||||
|
"global",
|
||||||
|
"gpui",
|
||||||
|
"i18n",
|
||||||
|
"identity",
|
||||||
|
"itertools 0.13.0",
|
||||||
|
"log",
|
||||||
|
"nostr",
|
||||||
|
"nostr-sdk",
|
||||||
|
"oneshot",
|
||||||
|
"rust-i18n",
|
||||||
|
"settings",
|
||||||
|
"smallvec",
|
||||||
|
"smol",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.15"
|
version = "0.12.15"
|
||||||
@@ -4879,7 +4875,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#5253702200f4fb491b6608ac0a0c6df89b224b0f"
|
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -4911,9 +4907,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rgb"
|
name = "rgb"
|
||||||
version = "0.8.50"
|
version = "0.8.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
|
checksum = "a457e416a0f90d246a4c3288bd7a25b2304ca727f253f95be383dd17af56be8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
@@ -5242,9 +5238,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "1.0.3"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1375ba8ef45a6f15d83fa8748f1079428295d403d6ea991d09ab100155fbc06d"
|
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
@@ -5256,9 +5252,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars_derive"
|
name = "schemars_derive"
|
||||||
version = "1.0.3"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b13ed22d6d49fe23712e068770b5c4df4a693a2b02eeff8e7ca3135627a24f6"
|
checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -5361,7 +5357,7 @@ 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#5253702200f4fb491b6608ac0a0c6df89b224b0f"
|
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5492,10 +5488,8 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
"i18n",
|
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"rust-i18n",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@@ -5739,7 +5733,7 @@ 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#5253702200f4fb491b6608ac0a0c6df89b224b0f"
|
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -6135,9 +6129,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.46.0"
|
version = "1.46.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4"
|
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -6648,7 +6642,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#5253702200f4fb491b6608ac0a0c6df89b224b0f"
|
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
@@ -7740,9 +7734,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zbus"
|
name = "zbus"
|
||||||
version = "5.7.1"
|
version = "5.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68"
|
checksum = "597f45e98bc7e6f0988276012797855613cd8269e23b5be62cc4e5d28b7e515d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-broadcast",
|
"async-broadcast",
|
||||||
"async-executor",
|
"async-executor",
|
||||||
@@ -7773,9 +7767,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zbus_macros"
|
name = "zbus_macros"
|
||||||
version = "5.7.1"
|
version = "5.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a"
|
checksum = "e5c8e4e14dcdd9d97a98b189cd1220f30e8394ad271e8c987da84f73693862c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -7924,9 +7918,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zvariant"
|
name = "zvariant"
|
||||||
version = "5.5.3"
|
version = "5.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d30786f75e393ee63a21de4f9074d4c038d52c5b1bb4471f955db249f9dffb1"
|
checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"endi",
|
"endi",
|
||||||
"enumflags2",
|
"enumflags2",
|
||||||
@@ -7939,9 +7933,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zvariant_derive"
|
name = "zvariant_derive"
|
||||||
version = "5.5.3"
|
version = "5.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75fda702cd42d735ccd48117b1630432219c0e9616bf6cb0f8350844ee4d9580"
|
checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::ffi::OsString;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as _, Error};
|
use anyhow::{anyhow, Context as _, Error};
|
||||||
use global::shared_state;
|
use global::nostr_client;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, SemanticVersion, Task};
|
use gpui::{App, AppContext, Context, Entity, Global, SemanticVersion, Task};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smol::fs::{self, File};
|
use smol::fs::{self, File};
|
||||||
@@ -128,10 +128,9 @@ impl AutoUpdater {
|
|||||||
self.set_status(AutoUpdateStatus::Downloading, cx);
|
self.set_status(AutoUpdateStatus::Downloading, cx);
|
||||||
|
|
||||||
let task: Task<Result<(TempDir, PathBuf), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(TempDir, PathBuf), Error>> = cx.background_spawn(async move {
|
||||||
let database = shared_state().client().database();
|
|
||||||
let ids = event.tags.event_ids().copied();
|
let ids = event.tags.event_ids().copied();
|
||||||
let filter = Filter::new().ids(ids).kind(Kind::FileMetadata);
|
let filter = Filter::new().ids(ids).kind(Kind::FileMetadata);
|
||||||
let events = database.query(filter).await?;
|
let events = nostr_client().database().query(filter).await?;
|
||||||
|
|
||||||
if let Some(event) = events.into_iter().find(|event| event.content == OS) {
|
if let Some(event) = events.into_iter().find(|event| event.content == OS) {
|
||||||
let tag = event.tags.find(TagKind::Url).context("url not found")?;
|
let tag = event.tags.find(TagKind::Url).context("url not found")?;
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
pub(crate) const NOW: &str = "now";
|
|
||||||
pub(crate) const SECONDS_IN_MINUTE: i64 = 60;
|
|
||||||
pub(crate) const MINUTES_IN_HOUR: i64 = 60;
|
|
||||||
pub(crate) const HOURS_IN_DAY: i64 = 24;
|
|
||||||
pub(crate) const DAYS_IN_MONTH: i64 = 30;
|
|
||||||
@@ -7,8 +7,6 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
|
|
||||||
rust-i18n.workspace = true
|
|
||||||
i18n.workspace = true
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
use global::constants::KEYRING_URL;
|
use global::{constants::KEYRING_URL, first_run};
|
||||||
use global::shared_state;
|
|
||||||
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};
|
||||||
|
|
||||||
i18n::init!();
|
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -66,7 +63,7 @@ impl ClientKeys {
|
|||||||
this.set_keys(Some(keys), false, true, cx);
|
this.set_keys(Some(keys), false, true, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
} else if shared_state().first_run() {
|
} else if *first_run() {
|
||||||
// Generate a new keys and update
|
// Generate a new keys and update
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.new_keys(cx);
|
this.new_keys(cx);
|
||||||
|
|||||||
48
crates/common/src/display.rs
Normal file
48
crates/common/src/display.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use global::constants::IMAGE_RESIZE_SERVICE;
|
||||||
|
use gpui::SharedString;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
const FALLBACK_IMG: &str = "https://image.nostr.build/c30703b48f511c293a9003be8100cdad37b8798b77a1dc3ec6eb8a20443d5dea.png";
|
||||||
|
|
||||||
|
pub trait DisplayProfile {
|
||||||
|
fn avatar_url(&self, proxy: bool) -> SharedString;
|
||||||
|
fn display_name(&self) -> SharedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayProfile for Profile {
|
||||||
|
fn avatar_url(&self, proxy: bool) -> SharedString {
|
||||||
|
self.metadata()
|
||||||
|
.picture
|
||||||
|
.as_ref()
|
||||||
|
.filter(|picture| !picture.is_empty())
|
||||||
|
.map(|picture| {
|
||||||
|
if proxy {
|
||||||
|
format!(
|
||||||
|
"{IMAGE_RESIZE_SERVICE}/?url={picture}&w=100&h=100&fit=cover&mask=circle&default={FALLBACK_IMG}&n=-1"
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
picture.into()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "brand/avatar.png".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_name(&self) -> SharedString {
|
||||||
|
if let Some(display_name) = self.metadata().display_name.as_ref() {
|
||||||
|
if !display_name.is_empty() {
|
||||||
|
return display_name.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(name) = self.metadata().name.as_ref() {
|
||||||
|
if !name.is_empty() {
|
||||||
|
return name.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pubkey = self.public_key().to_hex();
|
||||||
|
|
||||||
|
format!("{}:{}", &pubkey[0..4], &pubkey[pubkey.len() - 4..]).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,10 @@ use nostr_sdk::prelude::*;
|
|||||||
use qrcode_generator::QrCodeEcc;
|
use qrcode_generator::QrCodeEcc;
|
||||||
|
|
||||||
pub mod debounced_delay;
|
pub mod debounced_delay;
|
||||||
|
pub mod display;
|
||||||
pub mod handle_auth;
|
pub mod handle_auth;
|
||||||
pub mod nip05;
|
pub mod nip05;
|
||||||
pub mod nip96;
|
pub mod nip96;
|
||||||
pub mod profile;
|
|
||||||
|
|
||||||
pub fn room_hash(event: &Event) -> u64 {
|
pub fn room_hash(event: &Event) -> u64 {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ identity = { path = "../identity" }
|
|||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
chats = { path = "../chats" }
|
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" }
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use chats::{ChatRegistry, RoomEmitter};
|
|
||||||
use client_keys::ClientKeys;
|
use client_keys::ClientKeys;
|
||||||
use global::constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH};
|
use global::constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH};
|
||||||
use global::shared_state;
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, Action, App, AppContext, Axis, Context, Entity, IntoElement, ParentElement,
|
div, px, relative, Action, App, AppContext, Axis, Context, Entity, IntoElement, ParentElement,
|
||||||
@@ -13,6 +12,7 @@ use gpui::{
|
|||||||
use i18n::t;
|
use i18n::t;
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
|
use registry::{Registry, RoomEmitter};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use theme::{ActiveTheme, Theme, ThemeMode};
|
use theme::{ActiveTheme, Theme, ThemeMode};
|
||||||
@@ -84,7 +84,7 @@ impl ChatSpace {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let chats = ChatRegistry::global(cx);
|
let registry = Registry::global(cx);
|
||||||
let client_keys = ClientKeys::global(cx);
|
let client_keys = ClientKeys::global(cx);
|
||||||
let identity = Identity::global(cx);
|
let identity = Identity::global(cx);
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
@@ -153,11 +153,11 @@ impl ChatSpace {
|
|||||||
&identity,
|
&identity,
|
||||||
window,
|
window,
|
||||||
|this: &mut Self, state, window, cx| {
|
|this: &mut Self, state, window, cx| {
|
||||||
if !state.read(cx).has_profile() {
|
if !state.read(cx).has_signer() {
|
||||||
this.open_onboarding(window, cx);
|
this.open_onboarding(window, cx);
|
||||||
} else {
|
} else {
|
||||||
// Load all chat rooms from database
|
// Load all chat rooms from database
|
||||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
Registry::global(cx).update(cx, |this, cx| {
|
||||||
this.load_rooms(window, cx);
|
this.load_rooms(window, cx);
|
||||||
});
|
});
|
||||||
// Open chat panels
|
// Open chat panels
|
||||||
@@ -175,7 +175,7 @@ impl ChatSpace {
|
|||||||
|
|
||||||
// Subscribe to open chat room requests
|
// Subscribe to open chat room requests
|
||||||
subscriptions.push(cx.subscribe_in(
|
subscriptions.push(cx.subscribe_in(
|
||||||
&chats,
|
®istry,
|
||||||
window,
|
window,
|
||||||
|this: &mut Self, _state, event, window, cx| {
|
|this: &mut Self, _state, event, window, cx| {
|
||||||
if let RoomEmitter::Open(room) = event {
|
if let RoomEmitter::Open(room) = event {
|
||||||
@@ -187,10 +187,7 @@ impl ChatSpace {
|
|||||||
this.add_panel(panel, placement, window, cx);
|
this.add_panel(panel, placement, window, cx);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
window.push_notification(
|
window.push_notification(t!("chatspace.failed_to_open_room"), cx);
|
||||||
SharedString::new(t!("chatspace.failed_to_open_room")),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -283,7 +280,7 @@ impl ChatSpace {
|
|||||||
|
|
||||||
fn verify_messaging_relays(&self, cx: &App) -> Task<Result<bool, Error>> {
|
fn verify_messaging_relays(&self, cx: &App) -> Task<Result<bool, Error>> {
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let client = shared_state().client();
|
let client = nostr_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 filter = Filter::new()
|
let filter = Filter::new()
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
|
use std::collections::BTreeSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Error};
|
||||||
use asset::Assets;
|
use asset::Assets;
|
||||||
use auto_update::AutoUpdater;
|
use auto_update::AutoUpdater;
|
||||||
use chats::ChatRegistry;
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
use global::constants::APP_NAME;
|
use global::constants::APP_NAME;
|
||||||
use global::constants::{ALL_MESSAGES_SUB_ID, APP_ID};
|
use global::constants::{
|
||||||
use global::{shared_state, NostrSignal};
|
ALL_MESSAGES_SUB_ID, APP_ID, APP_PUBKEY, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT,
|
||||||
|
METADATA_BATCH_TIMEOUT, NEW_MESSAGE_SUB_ID, SEARCH_RELAYS,
|
||||||
|
};
|
||||||
|
use global::{nostr_client, NostrSignal};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
|
actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
|
||||||
WindowBounds, WindowKind, WindowOptions,
|
WindowBounds, WindowKind, WindowOptions,
|
||||||
@@ -15,7 +20,10 @@ use gpui::{
|
|||||||
use gpui::{point, SharedString, TitlebarOptions};
|
use gpui::{point, SharedString, TitlebarOptions};
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use gpui::{WindowBackgroundAppearance, WindowDecorations};
|
use gpui::{WindowBackgroundAppearance, WindowDecorations};
|
||||||
use nostr_sdk::SubscriptionId;
|
use itertools::Itertools;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use registry::Registry;
|
||||||
|
use smol::channel::{self, Sender};
|
||||||
use theme::Theme;
|
use theme::Theme;
|
||||||
use ui::Root;
|
use ui::Root;
|
||||||
|
|
||||||
@@ -31,15 +39,145 @@ fn main() {
|
|||||||
// Initialize logging
|
// Initialize logging
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
// Initialize the Nostr Client
|
||||||
|
let client = nostr_client();
|
||||||
|
|
||||||
// Initialize the Application
|
// Initialize the Application
|
||||||
let app = Application::new()
|
let app = Application::new()
|
||||||
.with_assets(Assets)
|
.with_assets(Assets)
|
||||||
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
|
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
|
||||||
|
|
||||||
// Initialize the Global State and process events in a separate thread.
|
let (signal_tx, signal_rx) = channel::bounded::<NostrSignal>(2048);
|
||||||
|
let (mta_tx, mta_rx) = channel::bounded::<PublicKey>(1024);
|
||||||
|
let (event_tx, event_rx) = channel::unbounded::<Event>();
|
||||||
|
|
||||||
|
let signal_tx_clone = signal_tx.clone();
|
||||||
|
let mta_tx_clone = mta_tx.clone();
|
||||||
|
|
||||||
app.background_executor()
|
app.background_executor()
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
shared_state().start().await;
|
// Subscribe for app updates from the bootstrap relays.
|
||||||
|
if let Err(e) = connect(client).await {
|
||||||
|
log::error!("Failed to connect to bootstrap relays: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to bootstrap relays.
|
||||||
|
if let Err(e) = subscribe_for_app_updates(client).await {
|
||||||
|
log::error!("Failed to subscribe for app updates: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Nostr notifications.
|
||||||
|
//
|
||||||
|
// Send the redefined signal back to GPUI via channel.
|
||||||
|
if let Err(e) =
|
||||||
|
handle_nostr_notifications(client, &signal_tx_clone, &mta_tx_clone, &event_tx).await
|
||||||
|
{
|
||||||
|
log::error!("Failed to handle Nostr notifications: {e}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
app.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
let duration = Duration::from_millis(METADATA_BATCH_TIMEOUT);
|
||||||
|
let mut batch: BTreeSet<PublicKey> = BTreeSet::new();
|
||||||
|
|
||||||
|
/// Internal events for the metadata batching system
|
||||||
|
enum BatchEvent {
|
||||||
|
NewKeys(PublicKey),
|
||||||
|
Timeout,
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let duration = smol::Timer::after(duration);
|
||||||
|
|
||||||
|
let recv = || async {
|
||||||
|
if let Ok(public_key) = mta_rx.recv().await {
|
||||||
|
BatchEvent::NewKeys(public_key)
|
||||||
|
} else {
|
||||||
|
BatchEvent::Closed
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let timeout = || async {
|
||||||
|
duration.await;
|
||||||
|
BatchEvent::Timeout
|
||||||
|
};
|
||||||
|
|
||||||
|
match smol::future::or(recv(), timeout()).await {
|
||||||
|
BatchEvent::NewKeys(public_key) => {
|
||||||
|
batch.insert(public_key);
|
||||||
|
// Process immediately if batch limit reached
|
||||||
|
if batch.len() >= METADATA_BATCH_LIMIT {
|
||||||
|
sync_data_for_pubkeys(client, std::mem::take(&mut batch)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BatchEvent::Timeout => {
|
||||||
|
if !batch.is_empty() {
|
||||||
|
sync_data_for_pubkeys(client, std::mem::take(&mut batch)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BatchEvent::Closed => {
|
||||||
|
if !batch.is_empty() {
|
||||||
|
sync_data_for_pubkeys(client, std::mem::take(&mut batch)).await;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
app.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
let mut counter = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Signer is unset, probably user is not ready to retrieve gift wrap events
|
||||||
|
if client.signer().await.is_err() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let duration = smol::Timer::after(Duration::from_secs(75));
|
||||||
|
|
||||||
|
let recv = || async {
|
||||||
|
// prevent inline format
|
||||||
|
(event_rx.recv().await).ok()
|
||||||
|
};
|
||||||
|
|
||||||
|
let timeout = || async {
|
||||||
|
duration.await;
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
match smol::future::or(recv(), timeout()).await {
|
||||||
|
Some(event) => {
|
||||||
|
// Process the gift wrap event unwrapping
|
||||||
|
let is_cached =
|
||||||
|
try_unwrap_event(client, &signal_tx, &mta_tx, &event, false).await;
|
||||||
|
|
||||||
|
// Increment the total messages counter if message is not from cache
|
||||||
|
if !is_cached {
|
||||||
|
counter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send partial finish signal to GPUI
|
||||||
|
if counter >= 20 {
|
||||||
|
signal_tx.send(NostrSignal::PartialFinish).await.ok();
|
||||||
|
// Reset counter
|
||||||
|
counter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
signal_tx.send(NostrSignal::Finish).await.ok();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event channel is no longer needed when all gift wrap events have been processed
|
||||||
|
event_rx.close();
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
@@ -98,6 +236,8 @@ fn main() {
|
|||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
// Initialize components
|
// Initialize components
|
||||||
ui::init(cx);
|
ui::init(cx);
|
||||||
|
// Initialize app registry
|
||||||
|
registry::init(cx);
|
||||||
// Initialize settings
|
// Initialize settings
|
||||||
settings::init(cx);
|
settings::init(cx);
|
||||||
// Initialize client keys
|
// Initialize client keys
|
||||||
@@ -106,44 +246,51 @@ fn main() {
|
|||||||
identity::init(window, cx);
|
identity::init(window, cx);
|
||||||
// Initialize auto update
|
// Initialize auto update
|
||||||
auto_update::init(cx);
|
auto_update::init(cx);
|
||||||
// Initialize chat state
|
|
||||||
chats::init(cx);
|
|
||||||
|
|
||||||
// Spawn a task to handle events from nostr channel
|
// Spawn a task to handle events from nostr channel
|
||||||
cx.spawn_in(window, async move |_, cx| {
|
cx.spawn_in(window, async move |_, cx| {
|
||||||
let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
||||||
|
|
||||||
while let Ok(signal) = shared_state().signal().recv().await {
|
while let Ok(signal) = signal_rx.recv().await {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
let chats = ChatRegistry::global(cx);
|
let registry = Registry::global(cx);
|
||||||
let auto_updater = AutoUpdater::global(cx);
|
let auto_updater = AutoUpdater::global(cx);
|
||||||
|
|
||||||
match signal {
|
match signal {
|
||||||
NostrSignal::Event(event) => {
|
|
||||||
chats.update(cx, |this, cx| {
|
|
||||||
this.event_to_message(event, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Load chat rooms and stop the loading status
|
// Load chat rooms and stop the loading status
|
||||||
NostrSignal::Finish => {
|
NostrSignal::Finish => {
|
||||||
chats.update(cx, |this, cx| {
|
registry.update(cx, |this, cx| {
|
||||||
this.load_rooms(window, cx);
|
this.load_rooms(window, cx);
|
||||||
this.set_loading(false, cx);
|
this.set_loading(false, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Load chat rooms without setting as finished
|
// Load chat rooms without setting as finished
|
||||||
NostrSignal::PartialFinish => {
|
NostrSignal::PartialFinish => {
|
||||||
chats.update(cx, |this, cx| {
|
registry.update(cx, |this, cx| {
|
||||||
this.load_rooms(window, cx);
|
this.load_rooms(window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Load chat rooms without setting as finished
|
||||||
NostrSignal::Eose(subscription_id) => {
|
NostrSignal::Eose(subscription_id) => {
|
||||||
|
// Only load chat rooms if the subscription ID matches the all_messages_sub_id
|
||||||
if subscription_id == all_messages_sub_id {
|
if subscription_id == all_messages_sub_id {
|
||||||
chats.update(cx, |this, cx| {
|
registry.update(cx, |this, cx| {
|
||||||
this.load_rooms(window, cx);
|
this.load_rooms(window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Add the new metadata to the registry or update the existing one
|
||||||
|
NostrSignal::Metadata(event) => {
|
||||||
|
registry.update(cx, |this, cx| {
|
||||||
|
this.insert_or_update_person(event, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Convert the gift wrapped message to a message
|
||||||
|
NostrSignal::GiftWrap(event) => {
|
||||||
|
registry.update(cx, |this, cx| {
|
||||||
|
this.event_to_message(event, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
NostrSignal::Notice(_msg) => {
|
NostrSignal::Notice(_msg) => {
|
||||||
// window.push_notification(msg, cx);
|
// window.push_notification(msg, cx);
|
||||||
}
|
}
|
||||||
@@ -170,3 +317,262 @@ fn quit(_: &Quit, cx: &mut App) {
|
|||||||
log::info!("Gracefully quitting the application . . .");
|
log::info!("Gracefully quitting the application . . .");
|
||||||
cx.quit();
|
cx.quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn connect(client: &Client) -> Result<(), Error> {
|
||||||
|
for relay in BOOTSTRAP_RELAYS.into_iter() {
|
||||||
|
client.add_relay(relay).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Connected to bootstrap relays");
|
||||||
|
|
||||||
|
for relay in SEARCH_RELAYS.into_iter() {
|
||||||
|
client.add_relay(relay).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Connected to search relays");
|
||||||
|
|
||||||
|
// Establish connection to relays
|
||||||
|
client.connect().await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_nostr_notifications(
|
||||||
|
client: &Client,
|
||||||
|
signal_tx: &Sender<NostrSignal>,
|
||||||
|
mta_tx: &Sender<PublicKey>,
|
||||||
|
event_tx: &Sender<Event>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let new_messages_sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
|
let mut notifications = client.notifications();
|
||||||
|
let mut processed_events: BTreeSet<EventId> = BTreeSet::new();
|
||||||
|
let mut processed_dm_relays: BTreeSet<PublicKey> = BTreeSet::new();
|
||||||
|
|
||||||
|
while let Ok(notification) = notifications.recv().await {
|
||||||
|
let RelayPoolNotification::Message { message, .. } = notification else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
match message {
|
||||||
|
RelayMessage::Event {
|
||||||
|
event,
|
||||||
|
subscription_id,
|
||||||
|
} => {
|
||||||
|
if processed_events.contains(&event.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Skip events that have already been processed
|
||||||
|
processed_events.insert(event.id);
|
||||||
|
|
||||||
|
match event.kind {
|
||||||
|
Kind::GiftWrap => {
|
||||||
|
if *subscription_id == new_messages_sub_id {
|
||||||
|
let event = event.as_ref();
|
||||||
|
_ = try_unwrap_event(client, signal_tx, mta_tx, event, false).await;
|
||||||
|
} else {
|
||||||
|
event_tx.send(event.into_owned()).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Kind::Metadata => {
|
||||||
|
signal_tx
|
||||||
|
.send(NostrSignal::Metadata(event.into_owned()))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
Kind::ContactList => {
|
||||||
|
if let Ok(true) = check_author(client, &event).await {
|
||||||
|
for public_key in event.tags.public_keys().copied() {
|
||||||
|
mta_tx.send(public_key).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Kind::RelayList => {
|
||||||
|
if processed_dm_relays.contains(&event.pubkey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Skip public keys that have already been processed
|
||||||
|
processed_dm_relays.insert(event.pubkey);
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.author(event.pubkey)
|
||||||
|
.kind(Kind::InboxRelays)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let relay_urls = nip65::extract_owned_relay_list(event.into_owned())
|
||||||
|
.map(|(url, _)| url)
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
if !relay_urls.is_empty() {
|
||||||
|
client
|
||||||
|
.subscribe_to(relay_urls, filter, Some(opts))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
log::info!("Subscribe for messaging relays")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Kind::ReleaseArtifactSet => {
|
||||||
|
let ids = event.tags.event_ids().copied();
|
||||||
|
let filter = Filter::new().ids(ids).kind(Kind::FileMetadata);
|
||||||
|
|
||||||
|
client
|
||||||
|
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
signal_tx
|
||||||
|
.send(NostrSignal::AppUpdate(event.into_owned()))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RelayMessage::EndOfStoredEvents(subscription_id) => {
|
||||||
|
signal_tx
|
||||||
|
.send(NostrSignal::Eose(subscription_id.into_owned()))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn subscribe_for_app_updates(client: &Client) -> Result<(), Error> {
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
|
let coordinate = Coordinate {
|
||||||
|
kind: Kind::Custom(32267),
|
||||||
|
public_key: PublicKey::from_hex(APP_PUBKEY).expect("App Pubkey is invalid"),
|
||||||
|
identifier: APP_ID.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::ReleaseArtifactSet)
|
||||||
|
.coordinate(&coordinate)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
client
|
||||||
|
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_author(client: &Client, event: &Event) -> Result<bool, Error> {
|
||||||
|
let signer = client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
Ok(public_key == event.pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync_data_for_pubkeys(client: &Client, public_keys: BTreeSet<PublicKey>) {
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.limit(public_keys.len() * kinds.len())
|
||||||
|
.authors(public_keys)
|
||||||
|
.kinds(kinds);
|
||||||
|
|
||||||
|
if let Err(e) = client
|
||||||
|
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
log::error!("Failed to sync metadata: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores an unwrapped event in local database with reference to original
|
||||||
|
async fn set_unwrapped(client: &Client, root: EventId, event: &Event) -> Result<(), Error> {
|
||||||
|
// Must be use the random generated keys to sign this event
|
||||||
|
let event = EventBuilder::new(Kind::ApplicationSpecificData, event.as_json())
|
||||||
|
.tags(vec![Tag::identifier(root), Tag::event(root)])
|
||||||
|
.sign(&Keys::generate())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Only save this event into the local database
|
||||||
|
client.database().save_event(&event).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves a previously unwrapped event from local database
|
||||||
|
async fn get_unwrapped(client: &Client, target: EventId) -> Result<Event, Error> {
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::ApplicationSpecificData)
|
||||||
|
.identifier(target)
|
||||||
|
.event(target)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||||
|
Ok(Event::from_json(event.content)?)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Event is not cached yet"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unwraps a gift-wrapped event and processes its contents.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `event` - The gift-wrapped event to unwrap
|
||||||
|
/// * `incoming` - Whether this is a newly received event (true) or old
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Returns `true` if the event was successfully loaded from cache or saved after unwrapping.
|
||||||
|
async fn try_unwrap_event(
|
||||||
|
client: &Client,
|
||||||
|
signal_tx: &Sender<NostrSignal>,
|
||||||
|
mta_tx: &Sender<PublicKey>,
|
||||||
|
event: &Event,
|
||||||
|
incoming: bool,
|
||||||
|
) -> bool {
|
||||||
|
let mut is_cached = false;
|
||||||
|
|
||||||
|
let event = match get_unwrapped(client, event.id).await {
|
||||||
|
Ok(event) => {
|
||||||
|
is_cached = true;
|
||||||
|
event
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
match client.unwrap_gift_wrap(event).await {
|
||||||
|
Ok(unwrap) => {
|
||||||
|
let Ok(unwrapped) = unwrap.rumor.sign_with_keys(&Keys::generate()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save this event to the database for future use.
|
||||||
|
if let Err(e) = set_unwrapped(client, event.id, &unwrapped).await {
|
||||||
|
log::error!("Failed to save event: {e}")
|
||||||
|
}
|
||||||
|
|
||||||
|
unwrapped
|
||||||
|
}
|
||||||
|
Err(_) => return false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save the event to the database, use for query directly.
|
||||||
|
if let Err(e) = client.database().save_event(&event).await {
|
||||||
|
log::error!("Failed to save event: {e}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all pubkeys to the batch to sync metadata
|
||||||
|
mta_tx.send(event.pubkey).await.ok();
|
||||||
|
|
||||||
|
for public_key in event.tags.public_keys().copied() {
|
||||||
|
mta_tx.send(public_key).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a notify to GPUI if this is a new message
|
||||||
|
if incoming {
|
||||||
|
signal_tx.send(NostrSignal::GiftWrap(event)).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
is_cached
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,16 +3,14 @@ use std::collections::HashMap;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use chats::message::Message;
|
use common::display::DisplayProfile;
|
||||||
use chats::room::{Room, RoomKind, SendError};
|
|
||||||
use common::nip96::nip96_upload;
|
use common::nip96::nip96_upload;
|
||||||
use common::profile::RenderProfile;
|
use global::nostr_client;
|
||||||
use global::shared_state;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, list, px, red, relative, rems, svg, white, Action, AnyElement, App, AppContext,
|
div, img, list, px, red, rems, white, Action, AnyElement, App, AppContext, ClipboardItem,
|
||||||
ClipboardItem, Context, Div, Element, Empty, Entity, EventEmitter, Flatten, FocusHandle,
|
Context, Div, Element, Empty, Entity, EventEmitter, Flatten, FocusHandle, Focusable,
|
||||||
Focusable, InteractiveElement, IntoElement, ListAlignment, ListState, ObjectFit, ParentElement,
|
InteractiveElement, IntoElement, ListAlignment, ListState, ObjectFit, ParentElement,
|
||||||
PathPromptOptions, Render, RetainAllImageCache, SharedString, StatefulInteractiveElement,
|
PathPromptOptions, Render, RetainAllImageCache, SharedString, StatefulInteractiveElement,
|
||||||
Styled, StyledImage, Subscription, Window,
|
Styled, StyledImage, Subscription, Window,
|
||||||
};
|
};
|
||||||
@@ -20,6 +18,9 @@ use i18n::t;
|
|||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use registry::message::Message;
|
||||||
|
use registry::room::{Room, RoomKind, SendError};
|
||||||
|
use registry::Registry;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
@@ -71,15 +72,7 @@ impl Chat {
|
|||||||
pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
let attaches = cx.new(|_| None);
|
let attaches = cx.new(|_| None);
|
||||||
let replies_to = cx.new(|_| None);
|
let replies_to = cx.new(|_| None);
|
||||||
|
let messages = cx.new(|_| vec![]);
|
||||||
let messages = cx.new(|_| {
|
|
||||||
let message = Message::builder()
|
|
||||||
.content(t!("chat.private_conversation_notice").into())
|
|
||||||
.build_rc()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
vec![message]
|
|
||||||
});
|
|
||||||
|
|
||||||
let input = cx.new(|cx| {
|
let input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
@@ -220,15 +213,11 @@ impl Chat {
|
|||||||
|
|
||||||
// TODO: find a better way to prevent duplicate messages during optimistic updates
|
// TODO: find a better way to prevent duplicate messages during optimistic updates
|
||||||
fn prevent_duplicate_message(&self, new_msg: &Message, cx: &Context<Self>) -> bool {
|
fn prevent_duplicate_message(&self, new_msg: &Message, cx: &Context<Self>) -> bool {
|
||||||
let Some(account) = Identity::get_global(cx).profile() else {
|
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(author) = new_msg.author.as_ref() else {
|
if new_msg.author != identity {
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if account.public_key() != author.public_key() {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,12 +226,7 @@ impl Chat {
|
|||||||
self.messages
|
self.messages
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|m| {
|
.filter(|m| m.borrow().author == identity)
|
||||||
m.borrow()
|
|
||||||
.author
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|p| p.public_key() == account.public_key())
|
|
||||||
})
|
|
||||||
.any(|existing| {
|
.any(|existing| {
|
||||||
let existing = existing.borrow();
|
let existing = existing.borrow();
|
||||||
// Check if messages are within the time window
|
// Check if messages are within the time window
|
||||||
@@ -297,10 +281,10 @@ impl Chat {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.messages.update(cx, |this, cx| {
|
this.messages.update(cx, |this, cx| {
|
||||||
if let Some(msg) = id.and_then(|id| {
|
if let Some(msg) =
|
||||||
this.iter().find(|msg| msg.borrow().id == Some(id)).cloned()
|
this.iter().find(|msg| msg.borrow().id == id).cloned()
|
||||||
}) {
|
{
|
||||||
msg.borrow_mut().errors = Some(reports);
|
msg.borrow_mut().errors = Some(reports.into());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -330,7 +314,7 @@ impl Chat {
|
|||||||
.messages
|
.messages
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.iter()
|
.iter()
|
||||||
.position(|m| m.borrow().id == Some(id))
|
.position(|m| m.borrow().id == id)
|
||||||
{
|
{
|
||||||
self.list_state.scroll_to_reveal_item(ix);
|
self.list_state.scroll_to_reveal_item(ix);
|
||||||
}
|
}
|
||||||
@@ -350,7 +334,7 @@ impl Chat {
|
|||||||
fn remove_reply(&mut self, id: EventId, cx: &mut Context<Self>) {
|
fn remove_reply(&mut self, id: EventId, cx: &mut Context<Self>) {
|
||||||
self.replies_to.update(cx, |this, cx| {
|
self.replies_to.update(cx, |this, cx| {
|
||||||
if let Some(replies) = this {
|
if let Some(replies) = this {
|
||||||
if let Some(ix) = replies.iter().position(|m| m.id == Some(id)) {
|
if let Some(ix) = replies.iter().position(|m| m.id == id) {
|
||||||
replies.remove(ix);
|
replies.remove(ix);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -391,9 +375,7 @@ impl Chat {
|
|||||||
|
|
||||||
// Spawn task via async utility instead of GPUI context
|
// Spawn task via async utility instead of GPUI context
|
||||||
nostr_sdk::async_utility::task::spawn(async move {
|
nostr_sdk::async_utility::task::spawn(async move {
|
||||||
let url = nip96_upload(shared_state().client(), &nip96, file_data)
|
let url = nip96_upload(nostr_client(), &nip96, file_data).await.ok();
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
_ = tx.send(url);
|
_ = tx.send(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -482,6 +464,9 @@ impl Chat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_reply(&mut self, message: &Message, cx: &Context<Self>) -> impl IntoElement {
|
fn render_reply(&mut self, message: &Message, cx: &Context<Self>) -> impl IntoElement {
|
||||||
|
let registry = Registry::read_global(cx);
|
||||||
|
let profile = registry.get_person(&message.author, cx);
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.w_full()
|
.w_full()
|
||||||
.pl_2()
|
.pl_2()
|
||||||
@@ -503,7 +488,7 @@ impl Chat {
|
|||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_color(cx.theme().text_accent)
|
.text_color(cx.theme().text_accent)
|
||||||
.child(message.author.as_ref().unwrap().render_name()),
|
.child(profile.display_name()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -512,7 +497,7 @@ impl Chat {
|
|||||||
.xsmall()
|
.xsmall()
|
||||||
.ghost()
|
.ghost()
|
||||||
.on_click({
|
.on_click({
|
||||||
let id = message.id.unwrap();
|
let id = message.id;
|
||||||
cx.listener(move |this, _, _, cx| {
|
cx.listener(move |this, _, _, cx| {
|
||||||
this.remove_reply(id, cx);
|
this.remove_reply(id, cx);
|
||||||
})
|
})
|
||||||
@@ -541,43 +526,16 @@ impl Chat {
|
|||||||
|
|
||||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
||||||
let hide_avatar = AppSettings::get_global(cx).settings.hide_user_avatars;
|
let hide_avatar = AppSettings::get_global(cx).settings.hide_user_avatars;
|
||||||
|
let registry = Registry::read_global(cx);
|
||||||
|
|
||||||
let message = message.borrow();
|
let message = message.borrow();
|
||||||
|
let author = registry.get_person(&message.author, cx);
|
||||||
// Message without ID, Author probably the placeholder
|
let mentions = registry.get_group_person(&message.mentions, cx);
|
||||||
let (Some(id), Some(author)) = (message.id, message.author.as_ref()) else {
|
|
||||||
return div()
|
|
||||||
.id(ix)
|
|
||||||
.group("")
|
|
||||||
.w_full()
|
|
||||||
.relative()
|
|
||||||
.flex()
|
|
||||||
.gap_3()
|
|
||||||
.px_3()
|
|
||||||
.py_2()
|
|
||||||
.w_full()
|
|
||||||
.h_32()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.text_center()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_placeholder)
|
|
||||||
.line_height(relative(1.3))
|
|
||||||
.child(
|
|
||||||
svg()
|
|
||||||
.path("brand/coop.svg")
|
|
||||||
.size_10()
|
|
||||||
.text_color(cx.theme().elevated_surface_background),
|
|
||||||
)
|
|
||||||
.child(message.content.clone());
|
|
||||||
};
|
|
||||||
|
|
||||||
let texts = self
|
let texts = self
|
||||||
.text_data
|
.text_data
|
||||||
.entry(id)
|
.entry(message.id)
|
||||||
.or_insert_with(|| RichText::new(message.content.to_string(), &message.mentions));
|
.or_insert_with(|| RichText::new(message.content.to_string(), &mentions));
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(ix)
|
.id(ix)
|
||||||
@@ -591,7 +549,7 @@ impl Chat {
|
|||||||
.flex()
|
.flex()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.when(!hide_avatar, |this| {
|
.when(!hide_avatar, |this| {
|
||||||
this.child(Avatar::new(author.render_avatar(proxy)).size(rems(2.)))
|
this.child(Avatar::new(author.avatar_url(proxy)).size(rems(2.)))
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -610,7 +568,7 @@ impl Chat {
|
|||||||
div()
|
div()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().text)
|
.text_color(cx.theme().text)
|
||||||
.child(author.render_name()),
|
.child(author.display_name()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -627,7 +585,7 @@ impl Chat {
|
|||||||
.messages
|
.messages
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.iter()
|
.iter()
|
||||||
.find(|msg| msg.borrow().id == Some(*id))
|
.find(|msg| msg.borrow().id == *id)
|
||||||
.cloned()
|
.cloned()
|
||||||
{
|
{
|
||||||
let message = message.borrow();
|
let message = message.borrow();
|
||||||
@@ -643,13 +601,7 @@ impl Chat {
|
|||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_color(cx.theme().text_accent)
|
.text_color(cx.theme().text_accent)
|
||||||
.child(
|
.child(author.display_name()),
|
||||||
message
|
|
||||||
.author
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.render_name(),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -664,7 +616,7 @@ impl Chat {
|
|||||||
.elevated_surface_background)
|
.elevated_surface_background)
|
||||||
})
|
})
|
||||||
.on_click({
|
.on_click({
|
||||||
let id = message.id.unwrap();
|
let id = message.id;
|
||||||
cx.listener(move |this, _, _, cx| {
|
cx.listener(move |this, _, _, cx| {
|
||||||
this.scroll_to(id, cx)
|
this.scroll_to(id, cx)
|
||||||
})
|
})
|
||||||
@@ -881,7 +833,7 @@ fn message_border(cx: &App) -> Div {
|
|||||||
.bg(cx.theme().border_transparent)
|
.bg(cx.theme().border_transparent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn message_errors(errors: Vec<SendError>, cx: &App) -> Div {
|
fn message_errors(errors: SmallVec<[SendError; 1]>, cx: &App) -> Div {
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
@@ -898,7 +850,7 @@ fn message_errors(errors: Vec<SendError>, cx: &App) -> Div {
|
|||||||
.gap_1()
|
.gap_1()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::new(t!("chat.send_to_label")))
|
.child(SharedString::new(t!("chat.send_to_label")))
|
||||||
.child(error.profile.render_name()),
|
.child(error.profile.display_name()),
|
||||||
)
|
)
|
||||||
.child(error.message)
|
.child(error.message)
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ use std::ops::Range;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use chats::room::{Room, RoomKind};
|
use common::display::DisplayProfile;
|
||||||
use chats::ChatRegistry;
|
|
||||||
use common::nip05::nip05_profile;
|
use common::nip05::nip05_profile;
|
||||||
use common::profile::RenderProfile;
|
use global::constants::BOOTSTRAP_RELAYS;
|
||||||
use global::shared_state;
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, px, red, relative, uniform_list, App, AppContext, Context, Entity,
|
div, img, px, red, relative, uniform_list, App, AppContext, Context, Entity,
|
||||||
@@ -16,37 +15,37 @@ use gpui::{
|
|||||||
use i18n::t;
|
use i18n::t;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use registry::room::{Room, RoomKind};
|
||||||
|
use registry::Registry;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::{
|
use ui::button::{Button, ButtonVariants};
|
||||||
button::{Button, ButtonVariants},
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
input::{InputEvent, InputState, TextInput},
|
use ui::notification::Notification;
|
||||||
notification::Notification,
|
use ui::{ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
||||||
ContextModal, Disableable, Icon, IconName, Sizable, StyledExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
|
||||||
cx.new(|cx| Compose::new(window, cx))
|
cx.new(|cx| Compose::new(window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug)]
|
||||||
struct Contact {
|
struct Contact {
|
||||||
profile: Profile,
|
public_key: PublicKey,
|
||||||
select: bool,
|
select: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<Profile> for Contact {
|
impl AsRef<PublicKey> for Contact {
|
||||||
fn as_ref(&self) -> &Profile {
|
fn as_ref(&self) -> &PublicKey {
|
||||||
&self.profile
|
&self.public_key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Contact {
|
impl Contact {
|
||||||
pub fn new(profile: Profile) -> Self {
|
pub fn new(public_key: PublicKey) -> Self {
|
||||||
Self {
|
Self {
|
||||||
profile,
|
public_key,
|
||||||
select: false,
|
select: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,20 +87,21 @@ impl Compose {
|
|||||||
&user_input,
|
&user_input,
|
||||||
window,
|
window,
|
||||||
move |this, _input, event, window, cx| {
|
move |this, _input, event, window, cx| {
|
||||||
match event {
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
InputEvent::PressEnter { .. } => this.add_and_select_contact(window, cx),
|
this.add_and_select_contact(window, cx)
|
||||||
InputEvent::Change(_) => {}
|
|
||||||
_ => {}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
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 = shared_state().client();
|
let client = nostr_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?;
|
||||||
let contacts = profiles.into_iter().map(Contact::new).collect_vec();
|
let contacts = profiles
|
||||||
|
.into_iter()
|
||||||
|
.map(|profile| Contact::new(profile.public_key()))
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
Ok(contacts)
|
Ok(contacts)
|
||||||
});
|
});
|
||||||
@@ -110,7 +110,7 @@ impl Compose {
|
|||||||
match get_contacts.await {
|
match get_contacts.await {
|
||||||
Ok(contacts) => {
|
Ok(contacts) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.contacts(contacts, cx);
|
this.extend_contacts(contacts, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -135,6 +135,28 @@ impl Compose {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> {
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
||||||
|
let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
|
||||||
|
|
||||||
|
client
|
||||||
|
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_pubkey(content: &str) -> Result<PublicKey, Error> {
|
||||||
|
if content.starts_with("nprofile1") {
|
||||||
|
Ok(Nip19Profile::from_bech32(content)?.public_key)
|
||||||
|
} else if content.starts_with("npub1") {
|
||||||
|
Ok(PublicKey::parse(content)?)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(t!("common.pubkey_invalid")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let public_keys: Vec<PublicKey> = self.selected(cx);
|
let public_keys: Vec<PublicKey> = self.selected(cx);
|
||||||
|
|
||||||
@@ -158,7 +180,7 @@ impl Compose {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let event: Task<Result<Room, anyhow::Error>> = cx.background_spawn(async move {
|
let event: Task<Result<Room, anyhow::Error>> = cx.background_spawn(async move {
|
||||||
let signer = shared_state().client().signer().await?;
|
let signer = nostr_client().signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
let room = EventBuilder::private_msg_rumor(public_keys[0], "")
|
let room = EventBuilder::private_msg_rumor(public_keys[0], "")
|
||||||
@@ -180,7 +202,7 @@ impl Compose {
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
Registry::global(cx).update(cx, |this, cx| {
|
||||||
this.push_room(cx.new(|_| room), cx);
|
this.push_room(cx.new(|_| room), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,7 +221,10 @@ impl Compose {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn contacts(&mut self, contacts: impl IntoIterator<Item = Contact>, cx: &mut Context<Self>) {
|
fn extend_contacts<I>(&mut self, contacts: I, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Contact>,
|
||||||
|
{
|
||||||
self.contacts
|
self.contacts
|
||||||
.extend(contacts.into_iter().map(|contact| cx.new(|_| contact)));
|
.extend(contacts.into_iter().map(|contact| cx.new(|_| contact)));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -209,15 +234,12 @@ impl Compose {
|
|||||||
if !self
|
if !self
|
||||||
.contacts
|
.contacts
|
||||||
.iter()
|
.iter()
|
||||||
.any(|e| e.read(cx).profile.public_key() == contact.profile.public_key())
|
.any(|e| e.read(cx).public_key == contact.public_key)
|
||||||
{
|
{
|
||||||
self.contacts.insert(0, cx.new(|_| contact));
|
self.contacts.insert(0, cx.new(|_| contact));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
} else {
|
} else {
|
||||||
self.set_error(
|
self.set_error(Some(t!("compose.contact_existed").into()), cx);
|
||||||
Some(t!("compose.contact_existed", name = contact.profile.name()).into()),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +248,7 @@ impl Compose {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|contact| {
|
.filter_map(|contact| {
|
||||||
if contact.read(cx).select {
|
if contact.read(cx).select {
|
||||||
Some(contact.read(cx).profile.public_key())
|
Some(contact.read(cx).public_key)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -245,7 +267,7 @@ impl Compose {
|
|||||||
this.set_loading(true, cx);
|
this.set_loading(true, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let task: Task<Result<Contact, anyhow::Error>> = if content.contains("@") {
|
let task: Task<Result<Contact, Error>> = if content.contains("@") {
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let (tx, rx) = oneshot::channel::<Option<Nip05Profile>>();
|
let (tx, rx) = oneshot::channel::<Option<Nip05Profile>>();
|
||||||
|
|
||||||
@@ -255,62 +277,33 @@ impl Compose {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Ok(Some(profile)) = rx.await {
|
if let Ok(Some(profile)) = rx.await {
|
||||||
|
let client = nostr_client();
|
||||||
let public_key = profile.public_key;
|
let public_key = profile.public_key;
|
||||||
let metadata = shared_state()
|
let contact = Contact::new(public_key).select();
|
||||||
.client()
|
|
||||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
Self::request_metadata(client, public_key).await?;
|
||||||
.await?
|
|
||||||
.unwrap_or_default();
|
|
||||||
let profile = Profile::new(public_key, metadata);
|
|
||||||
let contact = Contact::new(profile).select();
|
|
||||||
|
|
||||||
Ok(contact)
|
Ok(contact)
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!(t!("common.not_found")))
|
Err(anyhow!(t!("common.not_found")))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if content.starts_with("nprofile1") {
|
} else if let Ok(public_key) = Self::parse_pubkey(&content) {
|
||||||
let Some(public_key) = Nip19Profile::from_bech32(&content)
|
|
||||||
.map(|nip19| nip19.public_key)
|
|
||||||
.ok()
|
|
||||||
else {
|
|
||||||
self.set_error(Some(t!("common.pubkey_invalid").into()), cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let metadata = shared_state()
|
let client = nostr_client();
|
||||||
.client()
|
let contact = Contact::new(public_key).select();
|
||||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
|
||||||
.await?
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let profile = Profile::new(public_key, metadata);
|
Self::request_metadata(client, public_key).await?;
|
||||||
let contact = Contact::new(profile).select();
|
|
||||||
|
|
||||||
Ok(contact)
|
Ok(contact)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let Ok(public_key) = PublicKey::parse(&content) else {
|
|
||||||
self.set_error(Some(t!("common.pubkey_invalid").into()), cx);
|
self.set_error(Some(t!("common.pubkey_invalid").into()), cx);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let metadata = shared_state()
|
match task.await {
|
||||||
.client()
|
|
||||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
|
||||||
.await?
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let profile = Profile::new(public_key, metadata);
|
|
||||||
let contact = Contact::new(profile).select();
|
|
||||||
|
|
||||||
Ok(contact)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| match task.await {
|
|
||||||
Ok(contact) => {
|
Ok(contact) => {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
@@ -331,6 +324,7 @@ impl Compose {
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
@@ -374,6 +368,7 @@ impl Compose {
|
|||||||
|
|
||||||
fn list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
|
fn list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
|
||||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
||||||
|
let registry = Registry::read_global(cx);
|
||||||
let mut items = Vec::with_capacity(self.contacts.len());
|
let mut items = Vec::with_capacity(self.contacts.len());
|
||||||
|
|
||||||
for ix in range {
|
for ix in range {
|
||||||
@@ -381,14 +376,16 @@ impl Compose {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let profile = entity.read(cx).as_ref();
|
let public_key = entity.read(cx).as_ref();
|
||||||
|
let profile = registry.get_person(public_key, cx);
|
||||||
let selected = entity.read(cx).select;
|
let selected = entity.read(cx).select;
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
div()
|
div()
|
||||||
.id(ix)
|
.id(ix)
|
||||||
.w_full()
|
.w_full()
|
||||||
.h_10()
|
.h_11()
|
||||||
|
.py_1()
|
||||||
.px_3()
|
.px_3()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -399,14 +396,14 @@ impl Compose {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(img(profile.render_avatar(proxy)).size_7().flex_shrink_0())
|
.child(img(profile.avatar_url(proxy)).size_7().flex_shrink_0())
|
||||||
.child(profile.render_name()),
|
.child(profile.display_name()),
|
||||||
)
|
)
|
||||||
.when(selected, |this| {
|
.when(selected, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Icon::new(IconName::CheckCircleFill)
|
Icon::new(IconName::CheckCircleFill)
|
||||||
.small()
|
.small()
|
||||||
.text_color(cx.theme().ring),
|
.text_color(cx.theme().text_accent),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||||
@@ -542,7 +539,6 @@ impl Render for Compose {
|
|||||||
this.list_items(range, cx)
|
this.list_items(range, cx)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.pb_4()
|
|
||||||
.min_h(px(280.)),
|
.min_h(px(280.)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use i18n::t;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -12,6 +11,7 @@ use gpui::{
|
|||||||
EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
|
EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
|
||||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
||||||
};
|
};
|
||||||
|
use i18n::t;
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
@@ -581,15 +581,13 @@ impl Render for Login {
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
||||||
|
let msg = t!("login.approve_message", i = i);
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_center()
|
.text_center()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::new(t!(
|
.child(SharedString::new(msg)),
|
||||||
"login.approve_message",
|
|
||||||
i = i
|
|
||||||
))),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when_some(self.error.read(cx).clone(), |this, error| {
|
.when_some(self.error.read(cx).clone(), |this, error| {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use common::nip96::nip96_upload;
|
use common::nip96::nip96_upload;
|
||||||
use global::shared_state;
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten,
|
div, img, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten,
|
||||||
@@ -102,7 +102,7 @@ impl NewAccount {
|
|||||||
.ok();
|
.ok();
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
.on_ok(move |_, _window, cx| {
|
.on_ok(move |_, window, cx| {
|
||||||
let metadata = metadata.clone();
|
let metadata = metadata.clone();
|
||||||
let value = weak_input
|
let value = weak_input
|
||||||
.read_with(cx, |state, _cx| state.value().to_owned())
|
.read_with(cx, |state, _cx| state.value().to_owned())
|
||||||
@@ -110,7 +110,7 @@ impl NewAccount {
|
|||||||
|
|
||||||
if let Some(password) = value {
|
if let Some(password) = value {
|
||||||
Identity::global(cx).update(cx, |this, cx| {
|
Identity::global(cx).update(cx, |this, cx| {
|
||||||
this.new_identity(Keys::generate(), password.to_string(), metadata, cx);
|
this.new_identity(password.to_string(), metadata, window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,9 +161,7 @@ impl NewAccount {
|
|||||||
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) =
|
if let Ok(url) = nip96_upload(nostr_client(), &nip96, file_data).await {
|
||||||
nip96_upload(shared_state().client(), &nip96, file_data).await
|
|
||||||
{
|
|
||||||
_ = tx.send(url);
|
_ = tx.send(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use common::profile::RenderProfile;
|
use common::display::DisplayProfile;
|
||||||
use global::constants::ACCOUNT_D;
|
use global::constants::ACCOUNT_D;
|
||||||
use global::shared_state;
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||||
@@ -46,14 +46,12 @@ impl Onboarding {
|
|||||||
let local_account = cx.new(|_| None);
|
let local_account = cx.new(|_| None);
|
||||||
|
|
||||||
let task = cx.background_spawn(async move {
|
let task = cx.background_spawn(async move {
|
||||||
let database = shared_state().client().database();
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::ApplicationSpecificData)
|
.kind(Kind::ApplicationSpecificData)
|
||||||
.identifier(ACCOUNT_D)
|
.identifier(ACCOUNT_D)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if let Some(event) = database.query(filter).await?.first_owned() {
|
if let Some(event) = nostr_client().database().query(filter).await?.first_owned() {
|
||||||
let public_key = event
|
let public_key = event
|
||||||
.tags
|
.tags
|
||||||
.public_keys()
|
.public_keys()
|
||||||
@@ -62,10 +60,14 @@ impl Onboarding {
|
|||||||
.first()
|
.first()
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let metadata = database.metadata(public_key).await?.unwrap_or_default();
|
|
||||||
let profile = Profile::new(public_key, metadata);
|
|
||||||
|
|
||||||
Ok(profile)
|
let metadata = nostr_client()
|
||||||
|
.database()
|
||||||
|
.metadata(public_key)
|
||||||
|
.await?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(Profile::new(public_key, metadata))
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("Not found"))
|
Err(anyhow!("Not found"))
|
||||||
}
|
}
|
||||||
@@ -213,15 +215,13 @@ impl Render for Onboarding {
|
|||||||
.gap_1()
|
.gap_1()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child(
|
.child(
|
||||||
Avatar::new(
|
Avatar::new(profile.avatar_url(proxy))
|
||||||
profile.render_avatar(proxy),
|
|
||||||
)
|
|
||||||
.size(rems(1.5)),
|
.size(rems(1.5)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.pb_px()
|
.pb_px()
|
||||||
.child(profile.render_name()),
|
.child(profile.display_name()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use common::profile::RenderProfile;
|
use common::display::DisplayProfile;
|
||||||
use global::constants::{DEFAULT_MODAL_WIDTH, NIP96_SERVER};
|
use global::constants::{DEFAULT_MODAL_WIDTH, NIP96_SERVER};
|
||||||
use gpui::http_client::Url;
|
use gpui::http_client::Url;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
@@ -8,6 +8,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use i18n::t;
|
use i18n::t;
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
|
use registry::Registry;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
@@ -74,9 +75,15 @@ impl Preferences {
|
|||||||
|
|
||||||
impl Render for Preferences {
|
impl Render for Preferences {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let input_state = self.media_input.downgrade();
|
let registry = Registry::read_global(cx);
|
||||||
let settings = AppSettings::get_global(cx).settings.as_ref();
|
let settings = AppSettings::get_global(cx).settings.as_ref();
|
||||||
|
|
||||||
|
let profile = Identity::read_global(cx)
|
||||||
|
.public_key()
|
||||||
|
.map(|pk| registry.get_person(&pk, cx));
|
||||||
|
|
||||||
|
let input_state = self.media_input.downgrade();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.size_full()
|
.size_full()
|
||||||
@@ -97,7 +104,7 @@ impl Render for Preferences {
|
|||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child(SharedString::new(t!("preferences.account_header"))),
|
.child(SharedString::new(t!("preferences.account_header"))),
|
||||||
)
|
)
|
||||||
.when_some(Identity::get_global(cx).profile(), |this, profile| {
|
.when_some(profile, |this, profile| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -112,7 +119,7 @@ impl Render for Preferences {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
Avatar::new(
|
Avatar::new(
|
||||||
profile.render_avatar(settings.proxy_user_avatars),
|
profile.avatar_url(settings.proxy_user_avatars),
|
||||||
)
|
)
|
||||||
.size(rems(2.4)),
|
.size(rems(2.4)),
|
||||||
)
|
)
|
||||||
@@ -124,7 +131,7 @@ impl Render for Preferences {
|
|||||||
div()
|
div()
|
||||||
.line_height(relative(1.3))
|
.line_height(relative(1.3))
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child(profile.render_name()),
|
.child(profile.display_name()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::str::FromStr;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use common::nip96::nip96_upload;
|
use common::nip96::nip96_upload;
|
||||||
use global::shared_state;
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
|
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
|
||||||
@@ -57,7 +57,7 @@ impl Profile {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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 = shared_state().client();
|
let client = nostr_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
|
||||||
@@ -106,7 +106,7 @@ impl Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let nip96_server = AppSettings::get_global(cx).settings.media_server.clone();
|
let nip96 = AppSettings::get_global(cx).settings.media_server.clone();
|
||||||
let avatar_input = self.avatar_input.downgrade();
|
let avatar_input = self.avatar_input.downgrade();
|
||||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||||
files: true,
|
files: true,
|
||||||
@@ -126,8 +126,7 @@ impl Profile {
|
|||||||
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 {
|
||||||
let client = shared_state().client();
|
if let Ok(url) = nip96_upload(nostr_client(), &nip96, file_data).await {
|
||||||
if let Ok(url) = nip96_upload(client, &nip96_server, file_data).await {
|
|
||||||
_ = tx.send(url);
|
_ = tx.send(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -193,11 +192,12 @@ impl Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
let _ = shared_state().client().set_metadata(&new_metadata).await?;
|
nostr_client().set_metadata(&new_metadata).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| match task.await {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
window.push_notification(t!("profile.updated_successfully"), cx);
|
window.push_notification(t!("profile.updated_successfully"), cx);
|
||||||
@@ -214,6 +214,7 @@ impl Profile {
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use global::constants::NEW_MESSAGE_SUB_ID;
|
use global::constants::NEW_MESSAGE_SUB_ID;
|
||||||
use global::shared_state;
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
div, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
||||||
@@ -35,7 +35,7 @@ impl Relays {
|
|||||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
|
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
|
||||||
let relays = cx.new(|cx| {
|
let relays = cx.new(|cx| {
|
||||||
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 = shared_state().client();
|
let client = nostr_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 filter = Filter::new()
|
let filter = Filter::new()
|
||||||
@@ -106,7 +106,7 @@ impl Relays {
|
|||||||
|
|
||||||
let relays = self.relays.read(cx).clone();
|
let relays = self.relays.read(cx).clone();
|
||||||
let task: Task<Result<EventId, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<EventId, Error>> = cx.background_spawn(async move {
|
||||||
let client = shared_state().client();
|
let client = nostr_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?;
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ use std::ops::Range;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use chats::room::{Room, RoomKind};
|
|
||||||
use chats::{ChatRegistry, RoomEmitter};
|
|
||||||
use common::debounced_delay::DebouncedDelay;
|
use common::debounced_delay::DebouncedDelay;
|
||||||
|
use common::display::DisplayProfile;
|
||||||
use common::nip05::nip05_verify;
|
use common::nip05::nip05_verify;
|
||||||
use common::profile::RenderProfile;
|
|
||||||
use element::DisplayRoom;
|
use element::DisplayRoom;
|
||||||
use global::constants::{DEFAULT_MODAL_WIDTH, SEARCH_RELAYS};
|
use global::constants::{DEFAULT_MODAL_WIDTH, SEARCH_RELAYS};
|
||||||
use global::shared_state;
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, uniform_list, AnyElement, App, AppContext, ClipboardItem, Context,
|
div, px, relative, rems, uniform_list, AnyElement, App, AppContext, ClipboardItem, Context,
|
||||||
@@ -18,9 +16,12 @@ use gpui::{
|
|||||||
Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
||||||
Task, Window,
|
Task, Window,
|
||||||
};
|
};
|
||||||
|
use i18n::t;
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use registry::room::{Room, RoomKind};
|
||||||
|
use registry::{Registry, RoomEmitter};
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
@@ -35,7 +36,6 @@ use ui::skeleton::Skeleton;
|
|||||||
use ui::{ContextModal, IconName, Selectable, Sizable, StyledExt};
|
use ui::{ContextModal, IconName, Selectable, Sizable, StyledExt};
|
||||||
|
|
||||||
use crate::views::compose;
|
use crate::views::compose;
|
||||||
use i18n::t;
|
|
||||||
|
|
||||||
mod element;
|
mod element;
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ impl Sidebar {
|
|||||||
InputState::new(window, cx).placeholder(t!("sidebar.find_or_start_conversation"))
|
InputState::new(window, cx).placeholder(t!("sidebar.find_or_start_conversation"))
|
||||||
});
|
});
|
||||||
|
|
||||||
let chats = ChatRegistry::global(cx);
|
let chats = Registry::global(cx);
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(cx.subscribe_in(
|
subscriptions.push(cx.subscribe_in(
|
||||||
@@ -154,7 +154,7 @@ impl Sidebar {
|
|||||||
let query_cloned = query.clone();
|
let query_cloned = query.clone();
|
||||||
|
|
||||||
let task: Task<Result<BTreeSet<Room>, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<BTreeSet<Room>, Error>> = cx.background_spawn(async move {
|
||||||
let client = shared_state().client();
|
let client = nostr_client();
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::Metadata)
|
.kind(Kind::Metadata)
|
||||||
@@ -266,7 +266,7 @@ impl Sidebar {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let task: Task<Result<(Profile, Room), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(Profile, Room), Error>> = cx.background_spawn(async move {
|
||||||
let client = shared_state().client();
|
let client = nostr_client();
|
||||||
let signer = client.signer().await.unwrap();
|
let signer = client.signer().await.unwrap();
|
||||||
let user_pubkey = signer.get_public_key().await.unwrap();
|
let user_pubkey = signer.get_public_key().await.unwrap();
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ impl Sidebar {
|
|||||||
match task.await {
|
match task.await {
|
||||||
Ok((profile, room)) => {
|
Ok((profile, room)) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
let chats = ChatRegistry::global(cx);
|
let chats = Registry::global(cx);
|
||||||
let result = chats
|
let result = chats
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.search_by_public_key(profile.public_key(), cx);
|
.search_by_public_key(profile.public_key(), cx);
|
||||||
@@ -343,7 +343,7 @@ impl Sidebar {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let chats = ChatRegistry::global(cx);
|
let chats = Registry::global(cx);
|
||||||
let result = chats.read(cx).search(&query, cx);
|
let result = chats.read(cx).search(&query, cx);
|
||||||
|
|
||||||
if result.is_empty() {
|
if result.is_empty() {
|
||||||
@@ -426,7 +426,7 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn open_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let room = if let Some(room) = ChatRegistry::get_global(cx).room(&id, cx) {
|
let room = if let Some(room) = Registry::read_global(cx).room(&id, cx) {
|
||||||
room
|
room
|
||||||
} else {
|
} else {
|
||||||
let Some(result) = self.global_result.read(cx).as_ref() else {
|
let Some(result) = self.global_result.read(cx).as_ref() else {
|
||||||
@@ -445,7 +445,7 @@ impl Sidebar {
|
|||||||
room
|
room
|
||||||
};
|
};
|
||||||
|
|
||||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
Registry::global(cx).update(cx, |this, cx| {
|
||||||
this.push_room(room, cx);
|
this.push_room(room, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -508,15 +508,15 @@ impl Sidebar {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child(Avatar::new(profile.render_avatar(proxy)).size(rems(1.75)))
|
.child(Avatar::new(profile.avatar_url(proxy)).size(rems(1.75)))
|
||||||
.child(profile.render_name())
|
.child(profile.display_name())
|
||||||
.on_click(cx.listener({
|
.on_click(cx.listener({
|
||||||
let Ok(public_key) = profile.public_key().to_bech32();
|
let Ok(public_key) = profile.public_key().to_bech32();
|
||||||
let item = ClipboardItem::new_string(public_key);
|
let item = ClipboardItem::new_string(public_key);
|
||||||
|
|
||||||
move |_, _, window, cx| {
|
move |_, _, window, cx| {
|
||||||
cx.write_to_clipboard(item.clone());
|
cx.write_to_clipboard(item.clone());
|
||||||
window.push_notification("User's NPUB is copied", cx);
|
window.push_notification(t!("common.copied"), cx);
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
@@ -616,7 +616,11 @@ impl Focusable for Sidebar {
|
|||||||
|
|
||||||
impl Render for Sidebar {
|
impl Render for Sidebar {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let chats = ChatRegistry::get_global(cx);
|
let registry = Registry::read_global(cx);
|
||||||
|
|
||||||
|
let profile = Identity::read_global(cx)
|
||||||
|
.public_key()
|
||||||
|
.map(|pk| registry.get_person(&pk, cx));
|
||||||
|
|
||||||
// Get rooms from either search results or the chat registry
|
// Get rooms from either search results or the chat registry
|
||||||
let rooms = if let Some(results) = self.local_result.read(cx) {
|
let rooms = if let Some(results) = self.local_result.read(cx) {
|
||||||
@@ -624,9 +628,9 @@ impl Render for Sidebar {
|
|||||||
} else {
|
} else {
|
||||||
#[allow(clippy::collapsible_else_if)]
|
#[allow(clippy::collapsible_else_if)]
|
||||||
if self.active_filter.read(cx) == &RoomKind::Ongoing {
|
if self.active_filter.read(cx) == &RoomKind::Ongoing {
|
||||||
chats.ongoing_rooms(cx)
|
registry.ongoing_rooms(cx)
|
||||||
} else {
|
} else {
|
||||||
chats.request_rooms(self.trusted_only, cx)
|
registry.request_rooms(self.trusted_only, cx)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -638,7 +642,7 @@ impl Render for Sidebar {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
// Account
|
// Account
|
||||||
.when_some(Identity::get_global(cx).profile(), |this, profile| {
|
.when_some(profile, |this, profile| {
|
||||||
this.child(self.account(&profile, cx))
|
this.child(self.account(&profile, cx))
|
||||||
})
|
})
|
||||||
// Search Input
|
// Search Input
|
||||||
@@ -770,7 +774,7 @@ impl Render for Sidebar {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.when(chats.loading, |this| {
|
.when(registry.loading, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
@@ -791,7 +795,7 @@ impl Render for Sidebar {
|
|||||||
.h_full(),
|
.h_full(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.when(chats.loading, |this| {
|
.when(registry.loading, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
div().absolute().bottom_4().px_4().child(
|
div().absolute().bottom_4().px_4().child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use chats::ChatRegistry;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
|
div, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
|
||||||
ParentElement, Render, SharedString, Styled, Window,
|
ParentElement, Render, SharedString, Styled, Window,
|
||||||
};
|
};
|
||||||
use i18n::t;
|
use i18n::t;
|
||||||
|
use registry::Registry;
|
||||||
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};
|
||||||
@@ -47,7 +47,7 @@ impl Subject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let registry = ChatRegistry::global(cx).read(cx);
|
let registry = Registry::global(cx).read(cx);
|
||||||
let subject = self.input.read(cx).value().clone();
|
let subject = self.input.read(cx).value().clone();
|
||||||
|
|
||||||
if let Some(room) = registry.room(&self.id, cx) {
|
if let Some(room) = registry.room(&self.id, cx) {
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ pub const ACCOUNT_D: &str = "coop:account";
|
|||||||
pub const SETTINGS_D: &str = "coop:settings";
|
pub const SETTINGS_D: &str = "coop:settings";
|
||||||
|
|
||||||
/// Bootstrap Relays.
|
/// Bootstrap Relays.
|
||||||
pub const BOOTSTRAP_RELAYS: [&str; 4] = [
|
pub const BOOTSTRAP_RELAYS: [&str; 5] = [
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://relay.primal.net",
|
"wss://relay.primal.net",
|
||||||
|
"wss://nostr.Wine",
|
||||||
"wss://user.kindpag.es",
|
"wss://user.kindpag.es",
|
||||||
"wss://purplepag.es",
|
"wss://purplepag.es",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Search Relays.
|
||||||
|
pub const SEARCH_RELAYS: [&str; 2] = ["wss://search.nos.today", "wss://relay.nostr.band"];
|
||||||
|
|
||||||
/// NIP65 Relays. Used for new account
|
/// NIP65 Relays. Used for new account
|
||||||
pub const NIP65_RELAYS: [&str; 4] = [
|
pub const NIP65_RELAYS: [&str; 4] = [
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
@@ -25,9 +29,6 @@ pub const NIP65_RELAYS: [&str; 4] = [
|
|||||||
/// Messaging Relays. Used for new account
|
/// Messaging Relays. Used for new account
|
||||||
pub const NIP17_RELAYS: [&str; 2] = ["wss://auth.nostr1.com", "wss://relay.0xchat.com"];
|
pub const NIP17_RELAYS: [&str; 2] = ["wss://auth.nostr1.com", "wss://relay.0xchat.com"];
|
||||||
|
|
||||||
/// Search Relays.
|
|
||||||
pub const SEARCH_RELAYS: [&str; 2] = ["wss://search.nos.today", "wss://relay.nostr.band"];
|
|
||||||
|
|
||||||
/// Default relay for Nostr Connect
|
/// Default relay for Nostr Connect
|
||||||
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
||||||
|
|
||||||
@@ -54,6 +55,3 @@ pub const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";
|
|||||||
|
|
||||||
/// Default NIP96 Media Server.
|
/// Default NIP96 Media Server.
|
||||||
pub const NIP96_SERVER: &str = "https://nostrmedia.com";
|
pub const NIP96_SERVER: &str = "https://nostrmedia.com";
|
||||||
|
|
||||||
pub(crate) const GLOBAL_CHANNEL_LIMIT: usize = 2048;
|
|
||||||
pub(crate) const BATCH_CHANNEL_LIMIT: usize = 2048;
|
|
||||||
|
|||||||
@@ -1,72 +1,45 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::fs;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::time::Duration;
|
|
||||||
use std::{fs, mem};
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
|
||||||
use constants::{
|
|
||||||
ALL_MESSAGES_SUB_ID, APP_ID, APP_PUBKEY, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT,
|
|
||||||
METADATA_BATCH_TIMEOUT, NEW_MESSAGE_SUB_ID, SEARCH_RELAYS,
|
|
||||||
};
|
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use paths::nostr_file;
|
use paths::nostr_file;
|
||||||
use smol::lock::RwLock;
|
|
||||||
use smol::Task;
|
|
||||||
|
|
||||||
use crate::constants::{BATCH_CHANNEL_LIMIT, GLOBAL_CHANNEL_LIMIT};
|
|
||||||
use crate::paths::support_dir;
|
use crate::paths::support_dir;
|
||||||
|
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod paths;
|
pub mod paths;
|
||||||
|
|
||||||
/// Global singleton instance for application state
|
|
||||||
static GLOBALS: OnceLock<Globals> = OnceLock::new();
|
|
||||||
|
|
||||||
/// Signals sent through the global event channel to notify UI components
|
/// Signals sent through the global event channel to notify UI components
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum NostrSignal {
|
pub enum NostrSignal {
|
||||||
/// New gift wrap event received
|
/// Received a new metadata event from Relay Pool
|
||||||
Event(Event),
|
Metadata(Event),
|
||||||
|
|
||||||
|
/// Received a new gift wrap event from Relay Pool
|
||||||
|
GiftWrap(Event),
|
||||||
|
|
||||||
/// Finished processing all gift wrap events
|
/// Finished processing all gift wrap events
|
||||||
Finish,
|
Finish,
|
||||||
|
|
||||||
/// Partially finished processing all gift wrap events
|
/// Partially finished processing all gift wrap events
|
||||||
PartialFinish,
|
PartialFinish,
|
||||||
|
|
||||||
/// Receives EOSE response from relay pool
|
/// Receives EOSE response from relay pool
|
||||||
Eose(SubscriptionId),
|
Eose(SubscriptionId),
|
||||||
|
|
||||||
/// Notice from Relay Pool
|
/// Notice from Relay Pool
|
||||||
Notice(String),
|
Notice(String),
|
||||||
|
|
||||||
/// Application update event received
|
/// Application update event received
|
||||||
AppUpdate(Event),
|
AppUpdate(Event),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Global application state containing Nostr client and shared resources
|
static NOSTR_CLIENT: OnceLock<Client> = OnceLock::new();
|
||||||
pub struct Globals {
|
static FIRST_RUN: OnceLock<bool> = OnceLock::new();
|
||||||
/// The Nostr SDK client
|
|
||||||
client: Client,
|
|
||||||
|
|
||||||
/// Determines if this is the first time user run Coop
|
pub fn nostr_client() -> &'static Client {
|
||||||
first_run: bool,
|
NOSTR_CLIENT.get_or_init(|| {
|
||||||
|
|
||||||
/// Cache of user profiles mapped by their public keys
|
|
||||||
persons: RwLock<BTreeMap<PublicKey, Option<Metadata>>>,
|
|
||||||
|
|
||||||
/// Channel sender for broadcasting global Nostr events to UI
|
|
||||||
global_sender: smol::channel::Sender<NostrSignal>,
|
|
||||||
|
|
||||||
/// Channel receiver for handling global Nostr events
|
|
||||||
global_receiver: smol::channel::Receiver<NostrSignal>,
|
|
||||||
|
|
||||||
batch_sender: smol::channel::Sender<PublicKey>,
|
|
||||||
batch_receiver: smol::channel::Receiver<PublicKey>,
|
|
||||||
|
|
||||||
event_sender: smol::channel::Sender<Event>,
|
|
||||||
event_receiver: smol::channel::Receiver<Event>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the global singleton instance, initializing it if necessary
|
|
||||||
pub fn shared_state() -> &'static Globals {
|
|
||||||
GLOBALS.get_or_init(|| {
|
|
||||||
// rustls uses the `aws_lc_rs` provider by default
|
// rustls uses the `aws_lc_rs` provider by default
|
||||||
// This only errors if the default provider has already
|
// This only errors if the default provider has already
|
||||||
// been installed. We can ignore this `Result`.
|
// been installed. We can ignore this `Result`.
|
||||||
@@ -74,570 +47,24 @@ pub fn shared_state() -> &'static Globals {
|
|||||||
.install_default()
|
.install_default()
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
let first_run = is_first_run().unwrap_or(true);
|
|
||||||
let opts = ClientOptions::new().gossip(true);
|
let opts = ClientOptions::new().gossip(true);
|
||||||
let lmdb = NostrLMDB::open(nostr_file()).expect("Database is NOT initialized");
|
let lmdb = NostrLMDB::open(nostr_file()).expect("Database is NOT initialized");
|
||||||
|
|
||||||
let (global_sender, global_receiver) =
|
ClientBuilder::default().database(lmdb).opts(opts).build()
|
||||||
smol::channel::bounded::<NostrSignal>(GLOBAL_CHANNEL_LIMIT);
|
|
||||||
|
|
||||||
let (batch_sender, batch_receiver) =
|
|
||||||
smol::channel::bounded::<PublicKey>(BATCH_CHANNEL_LIMIT);
|
|
||||||
|
|
||||||
let (event_sender, event_receiver) = smol::channel::unbounded::<Event>();
|
|
||||||
|
|
||||||
Globals {
|
|
||||||
client: ClientBuilder::default().database(lmdb).opts(opts).build(),
|
|
||||||
persons: RwLock::new(BTreeMap::new()),
|
|
||||||
first_run,
|
|
||||||
global_sender,
|
|
||||||
global_receiver,
|
|
||||||
batch_sender,
|
|
||||||
batch_receiver,
|
|
||||||
event_sender,
|
|
||||||
event_receiver,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Globals {
|
pub fn first_run() -> &'static bool {
|
||||||
/// Starts the global event processing system and metadata batching
|
FIRST_RUN.get_or_init(|| {
|
||||||
pub async fn start(&self) {
|
|
||||||
self.connect().await;
|
|
||||||
self.preload_metadata().await;
|
|
||||||
self.subscribe_for_app_updates().await;
|
|
||||||
self.batching_metadata().detach(); // .detach() to keep running in background
|
|
||||||
|
|
||||||
let mut notifications = self.client.notifications();
|
|
||||||
let mut processed_events: BTreeSet<EventId> = BTreeSet::new();
|
|
||||||
let new_messages_sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
|
||||||
|
|
||||||
while let Ok(notification) = notifications.recv().await {
|
|
||||||
if let RelayPoolNotification::Message { message, .. } = notification {
|
|
||||||
match message {
|
|
||||||
RelayMessage::Event {
|
|
||||||
event,
|
|
||||||
subscription_id,
|
|
||||||
} => {
|
|
||||||
if processed_events.contains(&event.id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Skip events that have already been processed
|
|
||||||
processed_events.insert(event.id);
|
|
||||||
|
|
||||||
match event.kind {
|
|
||||||
Kind::GiftWrap => {
|
|
||||||
if *subscription_id == new_messages_sub_id
|
|
||||||
|| self
|
|
||||||
.event_sender
|
|
||||||
.send(event.clone().into_owned())
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
self.unwrap_event(&event, true).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Kind::Metadata => {
|
|
||||||
self.insert_person_from_event(&event).await;
|
|
||||||
}
|
|
||||||
Kind::ContactList => {
|
|
||||||
self.extract_pubkeys_and_sync(&event).await;
|
|
||||||
}
|
|
||||||
Kind::ReleaseArtifactSet => {
|
|
||||||
self.notify_update(&event).await;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RelayMessage::EndOfStoredEvents(subscription_id) => {
|
|
||||||
self.send_signal(NostrSignal::Eose(subscription_id.into_owned()))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a reference to the Nostr Client instance
|
|
||||||
pub fn client(&'static self) -> &'static Client {
|
|
||||||
&self.client
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether this is the first time the application has been run
|
|
||||||
pub fn first_run(&self) -> bool {
|
|
||||||
self.first_run
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the global signal receiver
|
|
||||||
pub fn signal(&self) -> smol::channel::Receiver<NostrSignal> {
|
|
||||||
self.global_receiver.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a signal through the global channel to notify GPUI
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `signal` - The [`NostrSignal`] to send to GPUI
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// shared_state().send_signal(NostrSignal::Finish).await;
|
|
||||||
/// ```
|
|
||||||
pub async fn send_signal(&self, signal: NostrSignal) {
|
|
||||||
if let Err(e) = self.global_sender.send(signal).await {
|
|
||||||
log::error!("Failed to send signal: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Batch metadata requests. Combine all requests from multiple authors into single filter
|
|
||||||
pub(crate) fn batching_metadata(&self) -> Task<()> {
|
|
||||||
smol::spawn(async move {
|
|
||||||
let duration = Duration::from_millis(METADATA_BATCH_TIMEOUT);
|
|
||||||
let mut batch: BTreeSet<PublicKey> = BTreeSet::new();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let timeout = smol::Timer::after(duration);
|
|
||||||
/// Internal events for the metadata batching system
|
|
||||||
enum BatchEvent {
|
|
||||||
NewKeys(PublicKey),
|
|
||||||
Timeout,
|
|
||||||
Closed,
|
|
||||||
}
|
|
||||||
|
|
||||||
let event = smol::future::or(
|
|
||||||
async {
|
|
||||||
if let Ok(public_key) = shared_state().batch_receiver.recv().await {
|
|
||||||
BatchEvent::NewKeys(public_key)
|
|
||||||
} else {
|
|
||||||
BatchEvent::Closed
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async {
|
|
||||||
timeout.await;
|
|
||||||
BatchEvent::Timeout
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match event {
|
|
||||||
BatchEvent::NewKeys(public_key) => {
|
|
||||||
batch.insert(public_key);
|
|
||||||
// Process immediately if batch limit reached
|
|
||||||
if batch.len() >= METADATA_BATCH_LIMIT {
|
|
||||||
shared_state()
|
|
||||||
.sync_data_for_pubkeys(mem::take(&mut batch))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BatchEvent::Timeout => {
|
|
||||||
if !batch.is_empty() {
|
|
||||||
shared_state()
|
|
||||||
.sync_data_for_pubkeys(mem::take(&mut batch))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BatchEvent::Closed => {
|
|
||||||
if !batch.is_empty() {
|
|
||||||
shared_state()
|
|
||||||
.sync_data_for_pubkeys(mem::take(&mut batch))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process to unwrap the gift wrapped events
|
|
||||||
pub(crate) fn process_gift_wrap_events(&self) -> Task<()> {
|
|
||||||
smol::spawn(async move {
|
|
||||||
let timeout_duration = Duration::from_secs(75); // 75 secs
|
|
||||||
let mut counter = 0;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
// Signer is unset, probably user is not ready to retrieve gift wrap events
|
|
||||||
if shared_state().client.signer().await.is_err() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeout = smol::Timer::after(timeout_duration);
|
|
||||||
|
|
||||||
// TODO: Find a way to make this code prettier
|
|
||||||
let event = smol::future::or(
|
|
||||||
async { (shared_state().event_receiver.recv().await).ok() },
|
|
||||||
async {
|
|
||||||
timeout.await;
|
|
||||||
None
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match event {
|
|
||||||
Some(event) => {
|
|
||||||
// Process the gift wrap event unwrapping
|
|
||||||
let is_cached = shared_state().unwrap_event(&event, false).await;
|
|
||||||
|
|
||||||
// Increment the total messages counter if message is not from cache
|
|
||||||
if !is_cached {
|
|
||||||
counter += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send partial finish signal to GPUI
|
|
||||||
if counter >= 20 {
|
|
||||||
shared_state().send_signal(NostrSignal::PartialFinish).await;
|
|
||||||
// Reset counter
|
|
||||||
counter = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
shared_state().send_signal(NostrSignal::Finish).await;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event channel is no longer needed when all gift wrap events have been processed
|
|
||||||
shared_state().event_receiver.close();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn request_metadata(&self, public_key: PublicKey) {
|
|
||||||
if let Err(e) = self.batch_sender.send(public_key).await {
|
|
||||||
log::error!("Failed to request metadata: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a person's profile from cache or creates default (blocking)
|
|
||||||
pub fn person(&self, public_key: &PublicKey) -> Profile {
|
|
||||||
let metadata = if let Some(metadata) = self.persons.read_blocking().get(public_key) {
|
|
||||||
metadata.clone().unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
Metadata::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
Profile::new(*public_key, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a person's profile from cache or creates default (async)
|
|
||||||
pub async fn async_person(&self, public_key: &PublicKey) -> Profile {
|
|
||||||
let metadata = if let Some(metadata) = self.persons.read().await.get(public_key) {
|
|
||||||
metadata.clone().unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
Metadata::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
Profile::new(*public_key, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a person exists or not
|
|
||||||
pub async fn has_person(&self, public_key: &PublicKey) -> bool {
|
|
||||||
self.persons.read().await.contains_key(public_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Inserts or updates a person's metadata
|
|
||||||
pub async fn insert_person(&self, public_key: PublicKey, metadata: Option<Metadata>) {
|
|
||||||
self.persons
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.entry(public_key)
|
|
||||||
.and_modify(|entry| {
|
|
||||||
if entry.is_none() {
|
|
||||||
*entry = metadata.clone();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.or_insert_with(|| metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Inserts or updates a person's metadata from a Kind::Metadata event
|
|
||||||
pub(crate) async fn insert_person_from_event(&self, event: &Event) {
|
|
||||||
let metadata = Metadata::from_json(&event.content).ok();
|
|
||||||
|
|
||||||
self.persons
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.entry(event.pubkey)
|
|
||||||
.and_modify(|entry| {
|
|
||||||
if entry.is_none() {
|
|
||||||
*entry = metadata.clone();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.or_insert_with(|| metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Connects to bootstrap and configured relays
|
|
||||||
pub(crate) async fn connect(&self) {
|
|
||||||
for relay in BOOTSTRAP_RELAYS.into_iter() {
|
|
||||||
if let Err(e) = self.client.add_relay(relay).await {
|
|
||||||
log::error!("Failed to add relay {relay}: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for relay in SEARCH_RELAYS.into_iter() {
|
|
||||||
if let Err(e) = self.client.add_relay(relay).await {
|
|
||||||
log::error!("Failed to add relay {relay}: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establish connection to relays
|
|
||||||
self.client.connect().await;
|
|
||||||
|
|
||||||
log::info!("Connected to bootstrap relays");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Subscribes to user-specific data feeds (DMs, mentions, etc.)
|
|
||||||
pub async fn subscribe_for_user_data(&self, public_key: PublicKey) {
|
|
||||||
let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
|
||||||
let new_messages_sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
|
||||||
|
|
||||||
self.client
|
|
||||||
.subscribe(
|
|
||||||
Filter::new()
|
|
||||||
.author(public_key)
|
|
||||||
.kinds(vec![
|
|
||||||
Kind::Metadata,
|
|
||||||
Kind::ContactList,
|
|
||||||
Kind::MuteList,
|
|
||||||
Kind::SimpleGroups,
|
|
||||||
Kind::InboxRelays,
|
|
||||||
Kind::RelayList,
|
|
||||||
])
|
|
||||||
.since(Timestamp::now()),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
self.client
|
|
||||||
.subscribe(
|
|
||||||
Filter::new()
|
|
||||||
.kinds(vec![
|
|
||||||
Kind::Metadata,
|
|
||||||
Kind::ContactList,
|
|
||||||
Kind::InboxRelays,
|
|
||||||
Kind::MuteList,
|
|
||||||
Kind::SimpleGroups,
|
|
||||||
])
|
|
||||||
.author(public_key)
|
|
||||||
.limit(10),
|
|
||||||
Some(SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE)),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
self.client
|
|
||||||
.subscribe_with_id(
|
|
||||||
all_messages_sub_id,
|
|
||||||
Filter::new().kind(Kind::GiftWrap).pubkey(public_key),
|
|
||||||
Some(opts),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
self.client
|
|
||||||
.subscribe_with_id(
|
|
||||||
new_messages_sub_id,
|
|
||||||
Filter::new()
|
|
||||||
.kind(Kind::GiftWrap)
|
|
||||||
.pubkey(public_key)
|
|
||||||
.limit(0),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
log::info!("Getting all user's metadata and messages...");
|
|
||||||
// Process gift-wrapped events in the background
|
|
||||||
self.process_gift_wrap_events().detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Subscribes to application update notifications
|
|
||||||
pub(crate) async fn subscribe_for_app_updates(&self) {
|
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
|
||||||
let coordinate = Coordinate {
|
|
||||||
kind: Kind::Custom(32267),
|
|
||||||
public_key: PublicKey::from_hex(APP_PUBKEY).expect("App Pubkey is invalid"),
|
|
||||||
identifier: APP_ID.into(),
|
|
||||||
};
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::ReleaseArtifactSet)
|
|
||||||
.coordinate(&coordinate)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if let Err(e) = self
|
|
||||||
.client
|
|
||||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
log::error!("Failed to subscribe for app updates: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Subscribed to app updates");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn preload_metadata(&self) {
|
|
||||||
let filter = Filter::new().kind(Kind::Metadata).limit(100);
|
|
||||||
if let Ok(events) = self.client.database().query(filter).await {
|
|
||||||
for event in events.into_iter() {
|
|
||||||
self.insert_person_from_event(&event).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stores an unwrapped event in local database with reference to original
|
|
||||||
pub(crate) async fn set_unwrapped(
|
|
||||||
&self,
|
|
||||||
root: EventId,
|
|
||||||
event: &Event,
|
|
||||||
keys: &Keys,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
// Must be use the random generated keys to sign this event
|
|
||||||
let event = EventBuilder::new(Kind::ApplicationSpecificData, event.as_json())
|
|
||||||
.tags(vec![Tag::identifier(root), Tag::event(root)])
|
|
||||||
.sign(keys)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Only save this event into the local database
|
|
||||||
self.client.database().save_event(&event).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieves a previously unwrapped event from local database
|
|
||||||
pub(crate) async fn get_unwrapped(&self, target: EventId) -> Result<Event, Error> {
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::ApplicationSpecificData)
|
|
||||||
.identifier(target)
|
|
||||||
.event(target)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if let Some(event) = self.client.database().query(filter).await?.first_owned() {
|
|
||||||
Ok(Event::from_json(event.content)?)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Event is not cached yet"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unwraps a gift-wrapped event and processes its contents.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `event` - The gift-wrapped event to unwrap
|
|
||||||
/// * `incoming` - Whether this is a newly received event (true) or old
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// Returns `true` if the event was successfully loaded from cache or saved after unwrapping.
|
|
||||||
pub(crate) async fn unwrap_event(&self, event: &Event, incoming: bool) -> bool {
|
|
||||||
let mut is_cached = false;
|
|
||||||
|
|
||||||
let event = match self.get_unwrapped(event.id).await {
|
|
||||||
Ok(event) => {
|
|
||||||
is_cached = true;
|
|
||||||
event
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
match self.client.unwrap_gift_wrap(event).await {
|
|
||||||
Ok(unwrap) => {
|
|
||||||
let keys = Keys::generate();
|
|
||||||
let Ok(unwrapped) = unwrap.rumor.sign_with_keys(&keys) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save this event to the database for future use.
|
|
||||||
if let Err(e) = self.set_unwrapped(event.id, &unwrapped, &keys).await {
|
|
||||||
log::error!("Failed to save event: {e}")
|
|
||||||
}
|
|
||||||
|
|
||||||
unwrapped
|
|
||||||
}
|
|
||||||
Err(_) => return false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save the event to the database, use for query directly.
|
|
||||||
if let Err(e) = self.client.database().save_event(&event).await {
|
|
||||||
log::error!("Failed to save event: {e}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send all pubkeys to the batch to sync metadata
|
|
||||||
self.batch_sender.send(event.pubkey).await.ok();
|
|
||||||
|
|
||||||
for public_key in event.tags.public_keys().copied() {
|
|
||||||
self.batch_sender.send(public_key).await.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a notify to GPUI if this is a new message
|
|
||||||
if incoming {
|
|
||||||
self.send_signal(NostrSignal::Event(event)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
is_cached
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extracts public keys from contact list and queues metadata sync
|
|
||||||
pub(crate) async fn extract_pubkeys_and_sync(&self, event: &Event) {
|
|
||||||
if let Ok(signer) = self.client.signer().await {
|
|
||||||
if let Ok(public_key) = signer.get_public_key().await {
|
|
||||||
if public_key == event.pubkey {
|
|
||||||
for public_key in event.tags.public_keys().copied() {
|
|
||||||
self.batch_sender.send(public_key).await.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetches metadata for a batch of public keys
|
|
||||||
pub(crate) async fn sync_data_for_pubkeys(&self, public_keys: BTreeSet<PublicKey>) {
|
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
|
||||||
let kinds = vec![
|
|
||||||
Kind::Metadata,
|
|
||||||
Kind::ContactList,
|
|
||||||
Kind::InboxRelays,
|
|
||||||
Kind::UserStatus,
|
|
||||||
];
|
|
||||||
let filter = Filter::new()
|
|
||||||
.limit(public_keys.len() * kinds.len())
|
|
||||||
.authors(public_keys)
|
|
||||||
.kinds(kinds);
|
|
||||||
|
|
||||||
if let Err(e) = shared_state()
|
|
||||||
.client
|
|
||||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
log::error!("Failed to sync metadata: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Notifies UI of application updates via global channel
|
|
||||||
pub(crate) async fn notify_update(&self, event: &Event) {
|
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
|
||||||
let filter = Filter::new()
|
|
||||||
.ids(event.tags.event_ids().copied())
|
|
||||||
.kind(Kind::FileMetadata);
|
|
||||||
|
|
||||||
if let Err(e) = self
|
|
||||||
.client
|
|
||||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
log::error!("Failed to subscribe for file metadata: {e}");
|
|
||||||
} else {
|
|
||||||
self.send_signal(NostrSignal::AppUpdate(event.to_owned()))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_first_run() -> Result<bool, anyhow::Error> {
|
|
||||||
let flag = support_dir().join(format!(".{}-first_run", env!("CARGO_PKG_VERSION")));
|
let flag = support_dir().join(format!(".{}-first_run", env!("CARGO_PKG_VERSION")));
|
||||||
|
|
||||||
if !flag.exists() {
|
if !flag.exists() {
|
||||||
fs::write(&flag, "")?;
|
if fs::write(&flag, "").is_err() {
|
||||||
Ok(true) // First run
|
return false;
|
||||||
} else {
|
|
||||||
Ok(false) // Not first run
|
|
||||||
}
|
}
|
||||||
|
true // First run
|
||||||
|
} else {
|
||||||
|
false // Not first run
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ common = { path = "../common" }
|
|||||||
client_keys = { path = "../client_keys" }
|
client_keys = { path = "../client_keys" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
|
|
||||||
rust-i18n.workspace = true
|
|
||||||
i18n.workspace = true
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
nostr-connect.workspace = true
|
nostr-connect.workspace = true
|
||||||
oneshot.workspace = true
|
oneshot.workspace = true
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ use std::time::Duration;
|
|||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use client_keys::ClientKeys;
|
use client_keys::ClientKeys;
|
||||||
use common::handle_auth::CoopAuthUrlHandler;
|
use common::handle_auth::CoopAuthUrlHandler;
|
||||||
use global::constants::{ACCOUNT_D, NIP17_RELAYS, NIP65_RELAYS, NOSTR_CONNECT_TIMEOUT};
|
use global::constants::{
|
||||||
use global::shared_state;
|
ACCOUNT_D, ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID, NIP17_RELAYS, NIP65_RELAYS,
|
||||||
|
NOSTR_CONNECT_TIMEOUT,
|
||||||
|
};
|
||||||
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, red, App, AppContext, Context, Entity, Global, ParentElement, SharedString, Styled,
|
div, red, App, AppContext, Context, Entity, Global, ParentElement, SharedString, Styled,
|
||||||
@@ -18,8 +21,6 @@ use ui::input::{InputState, TextInput};
|
|||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::{ContextModal, Sizable};
|
use ui::{ContextModal, Sizable};
|
||||||
|
|
||||||
i18n::init!();
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) {
|
pub fn init(window: &mut Window, cx: &mut App) {
|
||||||
Identity::set_global(cx.new(|cx| Identity::new(window, cx)), cx);
|
Identity::set_global(cx.new(|cx| Identity::new(window, cx)), cx);
|
||||||
}
|
}
|
||||||
@@ -29,7 +30,7 @@ struct GlobalIdentity(Entity<Identity>);
|
|||||||
impl Global for GlobalIdentity {}
|
impl Global for GlobalIdentity {}
|
||||||
|
|
||||||
pub struct Identity {
|
pub struct Identity {
|
||||||
profile: Option<Profile>,
|
public_key: Option<PublicKey>,
|
||||||
auto_logging_in_progress: bool,
|
auto_logging_in_progress: bool,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
subscriptions: SmallVec<[Subscription; 1]>,
|
subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
@@ -42,7 +43,7 @@ impl Identity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve the Identity instance
|
/// Retrieve the Identity instance
|
||||||
pub fn get_global(cx: &App) -> &Self {
|
pub fn read_global(cx: &App) -> &Self {
|
||||||
cx.global::<GlobalIdentity>().0.read(cx)
|
cx.global::<GlobalIdentity>().0.read(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,13 +66,13 @@ impl Identity {
|
|||||||
this.set_logging_in(true, cx);
|
this.set_logging_in(true, cx);
|
||||||
this.load(window, cx);
|
this.load(window, cx);
|
||||||
} else {
|
} else {
|
||||||
this.set_profile(None, cx);
|
this.set_public_key(None, cx);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
profile: None,
|
public_key: None,
|
||||||
auto_logging_in_progress: false,
|
auto_logging_in_progress: false,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
}
|
}
|
||||||
@@ -79,14 +80,12 @@ impl Identity {
|
|||||||
|
|
||||||
pub fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let task = cx.background_spawn(async move {
|
let task = cx.background_spawn(async move {
|
||||||
let database = shared_state().client().database();
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::ApplicationSpecificData)
|
.kind(Kind::ApplicationSpecificData)
|
||||||
.identifier(ACCOUNT_D)
|
.identifier(ACCOUNT_D)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if let Some(event) = database.query(filter).await?.first_owned() {
|
if let Some(event) = nostr_client().database().query(filter).await?.first_owned() {
|
||||||
let secret = event.content;
|
let secret = event.content;
|
||||||
let is_bunker = secret.starts_with("bunker://");
|
let is_bunker = secret.starts_with("bunker://");
|
||||||
|
|
||||||
@@ -107,7 +106,7 @@ impl Identity {
|
|||||||
.ok();
|
.ok();
|
||||||
} else {
|
} else {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_profile(None, cx);
|
this.set_public_key(None, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -116,24 +115,30 @@ impl Identity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn unload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn unload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let task = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
let client = shared_state().client();
|
let client = nostr_client();
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::ApplicationSpecificData)
|
.kind(Kind::ApplicationSpecificData)
|
||||||
.identifier(ACCOUNT_D)
|
.identifier(ACCOUNT_D);
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Unset signer
|
// Unset signer
|
||||||
client.unset_signer().await;
|
client.unset_signer().await;
|
||||||
|
|
||||||
// Delete account
|
// Delete account
|
||||||
client.database().delete(filter).await.is_ok()
|
client.database().delete(filter).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| match task.await {
|
||||||
if task.await {
|
Ok(_) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_profile(None, cx);
|
this.set_public_key(None, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
window.push_notification(Notification::error(e.to_string()), cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -153,13 +158,13 @@ impl Identity {
|
|||||||
self.login_with_bunker(uri, window, cx);
|
self.login_with_bunker(uri, window, cx);
|
||||||
} else {
|
} else {
|
||||||
window.push_notification(Notification::error("Bunker URI is invalid"), cx);
|
window.push_notification(Notification::error("Bunker URI is invalid"), cx);
|
||||||
self.set_profile(None, cx);
|
self.set_public_key(None, cx);
|
||||||
}
|
}
|
||||||
} else if let Ok(enc) = EncryptedSecretKey::from_bech32(secret) {
|
} else if let Ok(enc) = EncryptedSecretKey::from_bech32(secret) {
|
||||||
self.login_with_keys(enc, window, cx);
|
self.login_with_keys(enc, window, cx);
|
||||||
} else {
|
} else {
|
||||||
window.push_notification(Notification::error("Secret Key is invalid"), cx);
|
window.push_notification(Notification::error("Secret Key is invalid"), cx);
|
||||||
self.set_profile(None, cx);
|
self.set_public_key(None, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +182,7 @@ impl Identity {
|
|||||||
Notification::error("Bunker URI is invalid").title("Nostr Connect"),
|
Notification::error("Bunker URI is invalid").title("Nostr Connect"),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
self.set_profile(None, cx);
|
self.set_public_key(None, cx);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// Automatically open auth url
|
// Automatically open auth url
|
||||||
@@ -197,12 +202,9 @@ impl Identity {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
window.push_notification(
|
window.push_notification(Notification::error(e.to_string()), cx);
|
||||||
Notification::error(e.to_string()).title("Nostr Connect"),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_profile(None, cx);
|
this.set_public_key(None, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
})
|
})
|
||||||
@@ -240,7 +242,7 @@ impl Identity {
|
|||||||
.on_cancel(move |_, _window, cx| {
|
.on_cancel(move |_, _window, cx| {
|
||||||
entity
|
entity
|
||||||
.update(cx, |this, cx| {
|
.update(cx, |this, cx| {
|
||||||
this.set_profile(None, cx);
|
this.set_public_key(None, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
// Close modal
|
// Close modal
|
||||||
@@ -338,30 +340,22 @@ impl Identity {
|
|||||||
where
|
where
|
||||||
S: NostrSigner + 'static,
|
S: NostrSigner + 'static,
|
||||||
{
|
{
|
||||||
let task: Task<Result<Profile, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
||||||
let client = shared_state().client();
|
let client = nostr_client();
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
// Update signer
|
// Update signer
|
||||||
client.set_signer(signer).await;
|
client.set_signer(signer).await;
|
||||||
|
// Subscribe for user metadata
|
||||||
|
Self::subscribe(client, public_key).await?;
|
||||||
|
|
||||||
// Subscribe for user's data
|
Ok(public_key)
|
||||||
shared_state().subscribe_for_user_data(public_key).await;
|
|
||||||
|
|
||||||
// Fetch user's metadata
|
|
||||||
let metadata = client
|
|
||||||
.fetch_metadata(public_key, Duration::from_secs(3))
|
|
||||||
.await?
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// Create user's profile with public key and metadata
|
|
||||||
Ok(Profile::new(public_key, metadata))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| match task.await {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
Ok(profile) => {
|
match task.await {
|
||||||
|
Ok(public_key) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_profile(Some(profile), cx);
|
this.set_public_key(Some(public_key), cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -371,6 +365,7 @@ impl Identity {
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
@@ -378,25 +373,24 @@ impl Identity {
|
|||||||
/// Creates a new identity with the given keys and metadata
|
/// Creates a new identity with the given keys and metadata
|
||||||
pub fn new_identity(
|
pub fn new_identity(
|
||||||
&mut self,
|
&mut self,
|
||||||
keys: Keys,
|
|
||||||
password: String,
|
password: String,
|
||||||
metadata: Metadata,
|
metadata: Metadata,
|
||||||
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let profile = Profile::new(keys.public_key(), metadata.clone());
|
let keys = Keys::generate();
|
||||||
// Save keys for further use
|
let async_keys = keys.clone();
|
||||||
self.write_keys(&keys, password, cx);
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let client = shared_state().client();
|
|
||||||
|
|
||||||
|
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
||||||
|
let client = nostr_client();
|
||||||
|
let public_key = async_keys.public_key();
|
||||||
// Update signer
|
// Update signer
|
||||||
client.set_signer(keys).await;
|
client.set_signer(async_keys).await;
|
||||||
// Set metadata
|
// Set metadata
|
||||||
client.set_metadata(&metadata).await.ok();
|
client.set_metadata(&metadata).await?;
|
||||||
|
|
||||||
// Create relay list
|
// Create relay list
|
||||||
let builder = EventBuilder::new(Kind::RelayList, "").tags(
|
let relay_list = EventBuilder::new(Kind::RelayList, "").tags(
|
||||||
NIP65_RELAYS.into_iter().filter_map(|url| {
|
NIP65_RELAYS.into_iter().filter_map(|url| {
|
||||||
if let Ok(url) = RelayUrl::parse(url) {
|
if let Ok(url) = RelayUrl::parse(url) {
|
||||||
Some(Tag::relay_metadata(url, None))
|
Some(Tag::relay_metadata(url, None))
|
||||||
@@ -406,12 +400,8 @@ impl Identity {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = client.send_event_builder(builder).await {
|
|
||||||
log::error!("Failed to send relay list event: {e}");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create messaging relay list
|
// Create messaging relay list
|
||||||
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(
|
let dm_relay = EventBuilder::new(Kind::InboxRelays, "").tags(
|
||||||
NIP17_RELAYS.into_iter().filter_map(|url| {
|
NIP17_RELAYS.into_iter().filter_map(|url| {
|
||||||
if let Ok(url) = RelayUrl::parse(url) {
|
if let Ok(url) = RelayUrl::parse(url) {
|
||||||
Some(Tag::relay(url))
|
Some(Tag::relay(url))
|
||||||
@@ -421,14 +411,31 @@ impl Identity {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = client.send_event_builder(builder).await {
|
client.send_event_builder(relay_list).await?;
|
||||||
log::error!("Failed to send messaging relay list event: {e}");
|
client.send_event_builder(dm_relay).await?;
|
||||||
};
|
|
||||||
|
|
||||||
// Subscribe for user's data
|
// Subscribe for user metadata
|
||||||
shared_state()
|
Self::subscribe(client, public_key).await?;
|
||||||
.subscribe_for_user_data(profile.public_key())
|
|
||||||
.await;
|
Ok(public_key)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(public_key) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.write_keys(&keys, password, cx);
|
||||||
|
this.set_public_key(Some(public_key), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
window.push_notification(Notification::error(e.to_string()), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
@@ -447,7 +454,7 @@ impl Identity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let client = shared_state().client();
|
let client = nostr_client();
|
||||||
let keys = Keys::generate();
|
let keys = Keys::generate();
|
||||||
|
|
||||||
let builder = EventBuilder::new(Kind::ApplicationSpecificData, value).tags(vec![
|
let builder = EventBuilder::new(Kind::ApplicationSpecificData, value).tags(vec![
|
||||||
@@ -472,17 +479,15 @@ impl Identity {
|
|||||||
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 = shared_state().client();
|
let client = nostr_client();
|
||||||
let keys = Keys::generate();
|
let content = enc_key.to_bech32().unwrap();
|
||||||
|
|
||||||
let builder =
|
let builder = EventBuilder::new(Kind::ApplicationSpecificData, content).tags(vec![
|
||||||
EventBuilder::new(Kind::ApplicationSpecificData, enc_key.to_bech32().unwrap())
|
|
||||||
.tags(vec![
|
|
||||||
Tag::identifier(ACCOUNT_D),
|
Tag::identifier(ACCOUNT_D),
|
||||||
Tag::public_key(public_key),
|
Tag::public_key(public_key),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if let Ok(event) = builder.sign(&keys).await {
|
if let Ok(event) = builder.sign(&Keys::generate()).await {
|
||||||
if let Err(e) = client.database().save_event(&event).await {
|
if let Err(e) = client.database().save_event(&event).await {
|
||||||
log::error!("Failed to save event: {e}");
|
log::error!("Failed to save event: {e}");
|
||||||
};
|
};
|
||||||
@@ -492,19 +497,19 @@ impl Identity {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_profile(&mut self, profile: Option<Profile>, cx: &mut Context<Self>) {
|
pub(crate) fn set_public_key(&mut self, public_key: Option<PublicKey>, cx: &mut Context<Self>) {
|
||||||
self.profile = profile;
|
self.public_key = public_key;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current profile
|
/// Returns the current identity's public key
|
||||||
pub fn profile(&self) -> Option<Profile> {
|
pub fn public_key(&self) -> Option<PublicKey> {
|
||||||
self.profile.as_ref().cloned()
|
self.public_key
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if a profile is currently loaded
|
/// Returns true if a signer is currently set
|
||||||
pub fn has_profile(&self) -> bool {
|
pub fn has_signer(&self) -> bool {
|
||||||
self.profile.is_some()
|
self.public_key.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logging_in(&self) -> bool {
|
pub fn logging_in(&self) -> bool {
|
||||||
@@ -515,4 +520,60 @@ impl Identity {
|
|||||||
self.auto_logging_in_progress = status;
|
self.auto_logging_in_progress = status;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn subscribe(client: &Client, public_key: PublicKey) -> Result<(), Error> {
|
||||||
|
let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
||||||
|
let new_messages_sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
|
client
|
||||||
|
.subscribe(
|
||||||
|
Filter::new()
|
||||||
|
.author(public_key)
|
||||||
|
.kinds(vec![
|
||||||
|
Kind::Metadata,
|
||||||
|
Kind::ContactList,
|
||||||
|
Kind::MuteList,
|
||||||
|
Kind::SimpleGroups,
|
||||||
|
Kind::InboxRelays,
|
||||||
|
Kind::RelayList,
|
||||||
|
])
|
||||||
|
.since(Timestamp::now()),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
client
|
||||||
|
.subscribe(
|
||||||
|
Filter::new()
|
||||||
|
.kinds(vec![Kind::Metadata, Kind::ContactList, Kind::RelayList])
|
||||||
|
.author(public_key)
|
||||||
|
.limit(10),
|
||||||
|
Some(opts),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
client
|
||||||
|
.subscribe_with_id(
|
||||||
|
all_messages_sub_id,
|
||||||
|
Filter::new().kind(Kind::GiftWrap).pubkey(public_key),
|
||||||
|
Some(opts),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
client
|
||||||
|
.subscribe_with_id(
|
||||||
|
new_messages_sub_id,
|
||||||
|
Filter::new()
|
||||||
|
.kind(Kind::GiftWrap)
|
||||||
|
.pubkey(public_key)
|
||||||
|
.limit(0),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
log::info!("Getting all user's metadata and messages...");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "chats"
|
name = "registry"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::{BTreeSet, HashMap};
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use common::room_hash;
|
use common::room_hash;
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
use fuzzy_matcher::FuzzyMatcher;
|
||||||
use global::shared_state;
|
use global::nostr_client;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window,
|
App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
@@ -20,17 +20,15 @@ use crate::room::Room;
|
|||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod room;
|
pub mod room;
|
||||||
|
|
||||||
mod constants;
|
|
||||||
|
|
||||||
i18n::init!();
|
i18n::init!();
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
ChatRegistry::set_global(cx.new(ChatRegistry::new), cx);
|
Registry::set_global(cx.new(Registry::new), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GlobalChatRegistry(Entity<ChatRegistry>);
|
struct GlobalRegistry(Entity<Registry>);
|
||||||
|
|
||||||
impl Global for GlobalChatRegistry {}
|
impl Global for GlobalRegistry {}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum RoomEmitter {
|
pub enum RoomEmitter {
|
||||||
@@ -39,16 +37,13 @@ pub enum RoomEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Main registry for managing chat rooms and user profiles
|
/// Main registry for managing chat rooms and user profiles
|
||||||
///
|
pub struct Registry {
|
||||||
/// The ChatRegistry is responsible for:
|
|
||||||
/// - Managing chat rooms and their states
|
|
||||||
/// - Tracking user profiles
|
|
||||||
/// - Loading room data from the lmdb
|
|
||||||
/// - Handling messages and room creation
|
|
||||||
pub struct ChatRegistry {
|
|
||||||
/// Collection of all chat rooms
|
/// Collection of all chat rooms
|
||||||
pub rooms: Vec<Entity<Room>>,
|
pub rooms: Vec<Entity<Room>>,
|
||||||
|
|
||||||
|
/// Collection of all persons (user profiles)
|
||||||
|
pub persons: BTreeMap<PublicKey, Entity<Profile>>,
|
||||||
|
|
||||||
/// Indicates if rooms are currently being loaded
|
/// Indicates if rooms are currently being loaded
|
||||||
///
|
///
|
||||||
/// Always equal to `true` when the app starts
|
/// Always equal to `true` when the app starts
|
||||||
@@ -56,43 +51,126 @@ pub struct ChatRegistry {
|
|||||||
|
|
||||||
/// Subscriptions for observing changes
|
/// Subscriptions for observing changes
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
subscriptions: SmallVec<[Subscription; 1]>,
|
subscriptions: SmallVec<[Subscription; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<RoomEmitter> for ChatRegistry {}
|
impl EventEmitter<RoomEmitter> for Registry {}
|
||||||
|
|
||||||
impl ChatRegistry {
|
impl Registry {
|
||||||
/// Retrieve the Global ChatRegistry instance
|
/// Retrieve the Global Registry state
|
||||||
pub fn global(cx: &App) -> Entity<Self> {
|
pub fn global(cx: &App) -> Entity<Self> {
|
||||||
cx.global::<GlobalChatRegistry>().0.clone()
|
cx.global::<GlobalRegistry>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve the ChatRegistry instance
|
/// Retrieve the Registry instance
|
||||||
pub fn get_global(cx: &App) -> &Self {
|
pub fn read_global(cx: &App) -> &Self {
|
||||||
cx.global::<GlobalChatRegistry>().0.read(cx)
|
cx.global::<GlobalRegistry>().0.read(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the global ChatRegistry 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(GlobalChatRegistry(state));
|
cx.set_global(GlobalRegistry(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new ChatRegistry instance
|
/// Create a new Registry instance
|
||||||
fn new(cx: &mut Context<Self>) -> Self {
|
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
// When any Room is created, load metadata for all members
|
// Load all user profiles from the database when the Registry is created
|
||||||
|
subscriptions.push(cx.observe_new::<Self>(|this, _window, cx| {
|
||||||
|
let task = this.load_local_person(cx);
|
||||||
|
this.set_persons_from_task(task, cx);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// When any Room is created, load members metadata
|
||||||
subscriptions.push(cx.observe_new::<Room>(|this, _window, cx| {
|
subscriptions.push(cx.observe_new::<Room>(|this, _window, cx| {
|
||||||
this.load_metadata(cx).detach();
|
let task = this.load_metadata(cx);
|
||||||
|
Self::global(cx).update(cx, |this, cx| {
|
||||||
|
this.set_persons_from_task(task, cx);
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
rooms: vec![],
|
rooms: vec![],
|
||||||
|
persons: BTreeMap::new(),
|
||||||
loading: true,
|
loading: true,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_persons_from_task(
|
||||||
|
&mut self,
|
||||||
|
task: Task<Result<Vec<Profile>, Error>>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
if let Ok(profiles) = task.await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
for profile in profiles {
|
||||||
|
this.persons
|
||||||
|
.insert(profile.public_key(), cx.new(|_| profile));
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load_local_person(&self, cx: &App) -> Task<Result<Vec<Profile>, Error>> {
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let filter = Filter::new().kind(Kind::Metadata).limit(100);
|
||||||
|
let events = nostr_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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_person(&self, public_key: &PublicKey, cx: &App) -> Profile {
|
||||||
|
self.persons
|
||||||
|
.get(public_key)
|
||||||
|
.map(|e| e.read(cx))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(Profile::new(public_key.to_owned(), Metadata::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_group_person(&self, public_keys: &[PublicKey], cx: &App) -> Vec<Option<Profile>> {
|
||||||
|
let mut profiles = vec![];
|
||||||
|
|
||||||
|
for public_key in public_keys.iter() {
|
||||||
|
let profile = self.persons.get(public_key).map(|e| e.read(cx)).cloned();
|
||||||
|
profiles.push(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
profiles
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_or_update_person(&mut self, event: Event, cx: &mut App) {
|
||||||
|
let public_key = event.pubkey;
|
||||||
|
let Ok(metadata) = Metadata::from_json(event.content) else {
|
||||||
|
// Invalid metadata, no need to process further.
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(person) = self.persons.get(&public_key) {
|
||||||
|
person.update(cx, |this, cx| {
|
||||||
|
*this = Profile::new(public_key, metadata);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.persons
|
||||||
|
.insert(public_key, cx.new(|_| Profile::new(public_key, metadata)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a room by its ID.
|
/// Get a room by its ID.
|
||||||
pub fn room(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
|
pub fn room(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
|
||||||
self.rooms
|
self.rooms
|
||||||
@@ -125,6 +203,12 @@ impl ChatRegistry {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a new room to the start of list.
|
||||||
|
pub fn add_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) {
|
||||||
|
self.rooms.insert(0, room);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
/// Sort rooms by their created at.
|
/// Sort rooms by their created at.
|
||||||
pub fn sort(&mut self, cx: &mut Context<Self>) {
|
pub fn sort(&mut self, cx: &mut Context<Self>) {
|
||||||
self.rooms.sort_by_key(|ev| Reverse(ev.read(cx).created_at));
|
self.rooms.sort_by_key(|ev| Reverse(ev.read(cx).created_at));
|
||||||
@@ -155,6 +239,12 @@ impl ChatRegistry {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the loading status of the registry.
|
||||||
|
pub fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
|
self.loading = status;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
/// Load all rooms from the lmdb.
|
/// Load all rooms from the lmdb.
|
||||||
///
|
///
|
||||||
/// This method:
|
/// This method:
|
||||||
@@ -166,7 +256,7 @@ impl ChatRegistry {
|
|||||||
log::info!("Starting to load rooms from database...");
|
log::info!("Starting to load rooms from database...");
|
||||||
|
|
||||||
let task: Task<Result<BTreeSet<Room>, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<BTreeSet<Room>, Error>> = cx.background_spawn(async move {
|
||||||
let client = shared_state().client();
|
let client = nostr_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?;
|
||||||
|
|
||||||
@@ -278,18 +368,15 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
/// Push a new Room to the global registry
|
/// Push a new Room to the global registry
|
||||||
pub fn push_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) {
|
pub fn push_room(&mut self, room: Entity<Room>, cx: &mut Context<Self>) {
|
||||||
let weak_room = if let Some(room) = self
|
let other_id = room.read(cx).id;
|
||||||
.rooms
|
let find_room = self.rooms.iter().find(|this| this.read(cx).id == other_id);
|
||||||
.iter()
|
|
||||||
.find(|this| this.read(cx).id == room.read(cx).id)
|
let weak_room = if let Some(room) = find_room {
|
||||||
{
|
|
||||||
room.downgrade()
|
room.downgrade()
|
||||||
} else {
|
} else {
|
||||||
let weak_room = room.downgrade();
|
let weak_room = room.downgrade();
|
||||||
|
// Add this room to the registry
|
||||||
// Add this room to the global registry
|
self.add_room(room, cx);
|
||||||
self.rooms.insert(0, room);
|
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
weak_room
|
weak_room
|
||||||
};
|
};
|
||||||
@@ -304,7 +391,8 @@ impl ChatRegistry {
|
|||||||
pub fn event_to_message(&mut self, event: Event, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn event_to_message(&mut self, event: Event, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let id = room_hash(&event);
|
let id = room_hash(&event);
|
||||||
let author = event.pubkey;
|
let author = event.pubkey;
|
||||||
let Some(public_key) = Identity::get_global(cx).profile().map(|i| i.public_key()) else {
|
|
||||||
|
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -314,7 +402,7 @@ impl ChatRegistry {
|
|||||||
this.created_at(event.created_at, cx);
|
this.created_at(event.created_at, cx);
|
||||||
|
|
||||||
// Set this room is ongoing if the new message is from current user
|
// Set this room is ongoing if the new message is from current user
|
||||||
if author == public_key {
|
if author == identity {
|
||||||
this.set_ongoing(cx);
|
this.set_ongoing(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,22 +414,17 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
// Re-sort the rooms registry by their created at
|
// Re-sort the rooms registry by their created at
|
||||||
self.sort(cx);
|
self.sort(cx);
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
} else {
|
} else {
|
||||||
let room = Room::new(&event).kind(RoomKind::Unknown);
|
let room = Room::new(&event).kind(RoomKind::Unknown);
|
||||||
let kind = room.kind;
|
let kind = room.kind;
|
||||||
|
|
||||||
// Push the new room to the front of the list
|
// Push the new room to the front of the list
|
||||||
self.rooms.insert(0, cx.new(|_| room));
|
self.add_room(cx.new(|_| room), cx);
|
||||||
|
|
||||||
|
// Notify the UI about the new room
|
||||||
|
cx.defer_in(window, move |_this, _window, cx| {
|
||||||
cx.emit(RoomEmitter::Request(kind));
|
cx.emit(RoomEmitter::Request(kind));
|
||||||
cx.notify();
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.loading = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ use std::rc::Rc;
|
|||||||
use chrono::{Local, TimeZone};
|
use chrono::{Local, TimeZone};
|
||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
use crate::room::SendError;
|
use crate::room::SendError;
|
||||||
|
|
||||||
@@ -15,54 +16,50 @@ use crate::room::SendError;
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
/// Unique identifier of the message (EventId from nostr_sdk)
|
/// Unique identifier of the message (EventId from nostr_sdk)
|
||||||
pub id: Option<EventId>,
|
pub id: EventId,
|
||||||
/// Author profile information
|
/// Author's public key
|
||||||
pub author: Option<Profile>,
|
pub author: PublicKey,
|
||||||
/// The content/text of the message
|
/// The content/text of the message
|
||||||
pub content: SharedString,
|
pub content: SharedString,
|
||||||
/// When the message was created
|
/// When the message was created
|
||||||
pub created_at: Timestamp,
|
pub created_at: Timestamp,
|
||||||
/// List of mentioned profiles in the message
|
/// List of mentioned public keys in the message
|
||||||
pub mentions: Vec<Profile>,
|
pub mentions: SmallVec<[PublicKey; 2]>,
|
||||||
/// List of EventIds this message is replying to
|
/// List of EventIds this message is replying to
|
||||||
pub replies_to: Option<Vec<EventId>>,
|
pub replies_to: Option<SmallVec<[EventId; 1]>>,
|
||||||
/// Any errors that occurred while sending this message
|
/// Any errors that occurred while sending this message
|
||||||
pub errors: Option<Vec<SendError>>,
|
pub errors: Option<SmallVec<[SendError; 1]>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder pattern implementation for constructing Message objects.
|
/// Builder pattern implementation for constructing Message objects.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug)]
|
||||||
pub struct MessageBuilder {
|
pub struct MessageBuilder {
|
||||||
id: Option<EventId>,
|
id: EventId,
|
||||||
author: Option<Profile>,
|
author: PublicKey,
|
||||||
content: Option<String>,
|
content: Option<SharedString>,
|
||||||
created_at: Option<Timestamp>,
|
created_at: Option<Timestamp>,
|
||||||
mentions: Vec<Profile>,
|
mentions: SmallVec<[PublicKey; 2]>,
|
||||||
replies_to: Option<Vec<EventId>>,
|
replies_to: Option<SmallVec<[EventId; 1]>>,
|
||||||
errors: Option<Vec<SendError>>,
|
errors: Option<SmallVec<[SendError; 1]>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageBuilder {
|
impl MessageBuilder {
|
||||||
/// Creates a new MessageBuilder with default values
|
/// Creates a new MessageBuilder with default values
|
||||||
pub fn new() -> Self {
|
pub fn new(id: EventId, author: PublicKey) -> Self {
|
||||||
Self::default()
|
Self {
|
||||||
|
id,
|
||||||
|
author,
|
||||||
|
content: None,
|
||||||
|
created_at: None,
|
||||||
|
mentions: smallvec![],
|
||||||
|
replies_to: None,
|
||||||
|
errors: None,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the message ID
|
|
||||||
pub fn id(mut self, id: EventId) -> Self {
|
|
||||||
self.id = Some(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the message author
|
|
||||||
pub fn author(mut self, author: Profile) -> Self {
|
|
||||||
self.author = Some(author);
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the message content
|
/// Sets the message content
|
||||||
pub fn content(mut self, content: String) -> Self {
|
pub fn content(mut self, content: impl Into<SharedString>) -> Self {
|
||||||
self.content = Some(content);
|
self.content = Some(content.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +70,7 @@ impl MessageBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a single mention to the message
|
/// Adds a single mention to the message
|
||||||
pub fn mention(mut self, mention: Profile) -> Self {
|
pub fn mention(mut self, mention: PublicKey) -> Self {
|
||||||
self.mentions.push(mention);
|
self.mentions.push(mention);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -81,7 +78,7 @@ impl MessageBuilder {
|
|||||||
/// Adds multiple mentions to the message
|
/// Adds multiple mentions to the message
|
||||||
pub fn mentions<I>(mut self, mentions: I) -> Self
|
pub fn mentions<I>(mut self, mentions: I) -> Self
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = Profile>,
|
I: IntoIterator<Item = PublicKey>,
|
||||||
{
|
{
|
||||||
self.mentions.extend(mentions);
|
self.mentions.extend(mentions);
|
||||||
self
|
self
|
||||||
@@ -89,7 +86,7 @@ impl MessageBuilder {
|
|||||||
|
|
||||||
/// Sets a single message this is replying to
|
/// Sets a single message this is replying to
|
||||||
pub fn reply_to(mut self, reply_to: EventId) -> Self {
|
pub fn reply_to(mut self, reply_to: EventId) -> Self {
|
||||||
self.replies_to = Some(vec![reply_to]);
|
self.replies_to = Some(smallvec![reply_to]);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +95,7 @@ impl MessageBuilder {
|
|||||||
where
|
where
|
||||||
I: IntoIterator<Item = EventId>,
|
I: IntoIterator<Item = EventId>,
|
||||||
{
|
{
|
||||||
let replies: Vec<EventId> = replies_to.into_iter().collect();
|
let replies: SmallVec<[EventId; 1]> = replies_to.into_iter().collect();
|
||||||
if !replies.is_empty() {
|
if !replies.is_empty() {
|
||||||
self.replies_to = Some(replies);
|
self.replies_to = Some(replies);
|
||||||
}
|
}
|
||||||
@@ -124,7 +121,7 @@ impl MessageBuilder {
|
|||||||
Ok(Message {
|
Ok(Message {
|
||||||
id: self.id,
|
id: self.id,
|
||||||
author: self.author,
|
author: self.author,
|
||||||
content: self.content.ok_or("Content is required")?.into(),
|
content: self.content.ok_or("Content is required")?,
|
||||||
created_at: self.created_at.unwrap_or_else(Timestamp::now),
|
created_at: self.created_at.unwrap_or_else(Timestamp::now),
|
||||||
mentions: self.mentions,
|
mentions: self.mentions,
|
||||||
replies_to: self.replies_to,
|
replies_to: self.replies_to,
|
||||||
@@ -135,8 +132,8 @@ impl MessageBuilder {
|
|||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
/// Creates a new MessageBuilder
|
/// Creates a new MessageBuilder
|
||||||
pub fn builder() -> MessageBuilder {
|
pub fn builder(id: EventId, author: PublicKey) -> MessageBuilder {
|
||||||
MessageBuilder::new()
|
MessageBuilder::new(id, author)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts the message into an Rc<RefCell<Message>>
|
/// Converts the message into an Rc<RefCell<Message>>
|
||||||
@@ -1,18 +1,24 @@
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{Local, TimeZone};
|
||||||
use common::profile::RenderProfile;
|
use common::display::DisplayProfile;
|
||||||
use global::shared_state;
|
use global::nostr_client;
|
||||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
|
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::constants::{DAYS_IN_MONTH, HOURS_IN_DAY, MINUTES_IN_HOUR, NOW, SECONDS_IN_MINUTE};
|
|
||||||
use crate::message::Message;
|
use crate::message::Message;
|
||||||
|
use crate::Registry;
|
||||||
|
|
||||||
|
pub(crate) const NOW: &str = "now";
|
||||||
|
pub(crate) const SECONDS_IN_MINUTE: i64 = 60;
|
||||||
|
pub(crate) const MINUTES_IN_HOUR: i64 = 60;
|
||||||
|
pub(crate) const HOURS_IN_DAY: i64 = 24;
|
||||||
|
pub(crate) const DAYS_IN_MONTH: i64 = 30;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Incoming(pub Message);
|
pub struct Incoming(pub Message);
|
||||||
@@ -40,7 +46,7 @@ pub struct Room {
|
|||||||
/// Picture of the room
|
/// Picture of the room
|
||||||
pub picture: Option<SharedString>,
|
pub picture: Option<SharedString>,
|
||||||
/// All members of the room
|
/// All members of the room
|
||||||
pub members: Arc<Vec<PublicKey>>,
|
pub members: SmallVec<[PublicKey; 2]>,
|
||||||
/// Kind
|
/// Kind
|
||||||
pub kind: RoomKind,
|
pub kind: RoomKind,
|
||||||
}
|
}
|
||||||
@@ -57,26 +63,17 @@ impl PartialOrd for Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for Room {}
|
|
||||||
|
|
||||||
impl PartialEq for Room {
|
impl PartialEq for Room {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.id == other.id
|
self.id == other.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Eq for Room {}
|
||||||
|
|
||||||
impl EventEmitter<Incoming> for Room {}
|
impl EventEmitter<Incoming> for Room {}
|
||||||
|
|
||||||
impl Room {
|
impl Room {
|
||||||
/// Creates a new Room instance from a Nostr event
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `event` - The Nostr event containing chat information
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// A new Room instance with information extracted from the event
|
|
||||||
pub fn new(event: &Event) -> Self {
|
pub fn new(event: &Event) -> Self {
|
||||||
let id = common::room_hash(event);
|
let id = common::room_hash(event);
|
||||||
let created_at = event.created_at;
|
let created_at = event.created_at;
|
||||||
@@ -87,7 +84,7 @@ impl Room {
|
|||||||
pubkeys.push(event.pubkey);
|
pubkeys.push(event.pubkey);
|
||||||
|
|
||||||
// Convert pubkeys into members
|
// Convert pubkeys into members
|
||||||
let members = Arc::new(pubkeys.into_iter().unique().sorted().collect());
|
let members = pubkeys.into_iter().unique().sorted().collect();
|
||||||
|
|
||||||
// Get the subject from the event's tags
|
// Get the subject from the event's tags
|
||||||
let subject = if let Some(tag) = event.tags.find(TagKind::Subject) {
|
let subject = if let Some(tag) = event.tags.find(TagKind::Subject) {
|
||||||
@@ -113,30 +110,88 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the kind of the room
|
/// Sets the kind of the room and returns the modified room
|
||||||
|
///
|
||||||
|
/// This is a builder-style method that allows chaining room modifications.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `kind` - The kind of room to set
|
/// * `kind` - The RoomKind to set for this room
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// The room with the updated kind
|
/// The modified Room instance with the new kind
|
||||||
pub fn kind(mut self, kind: RoomKind) -> Self {
|
pub fn kind(mut self, kind: RoomKind) -> Self {
|
||||||
self.kind = kind;
|
self.kind = kind;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates a human-readable representation of the time passed since room creation
|
/// Set the room kind to ongoing
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `cx` - The context to notify about the update
|
||||||
|
pub fn set_ongoing(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if self.kind != RoomKind::Ongoing {
|
||||||
|
self.kind = RoomKind::Ongoing;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the room is a group chat
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// A SharedString representing the relative time since room creation:
|
/// true if the room has more than 2 members, false otherwise
|
||||||
/// - "now" for less than a minute
|
pub fn is_group(&self) -> bool {
|
||||||
/// - "Xm" for minutes
|
self.members.len() > 2
|
||||||
/// - "Xh" for hours
|
}
|
||||||
/// - "Xd" for days
|
|
||||||
/// - Month and day (e.g. "Jan 15") for older dates
|
/// Updates the creation timestamp of the room
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `created_at` - The new Timestamp to set
|
||||||
|
/// * `cx` - The context to notify about the update
|
||||||
|
pub fn created_at(&mut self, created_at: impl Into<Timestamp>, cx: &mut Context<Self>) {
|
||||||
|
self.created_at = created_at.into();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the subject of the room
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `subject` - The new subject to set
|
||||||
|
/// * `cx` - The context to notify about the update
|
||||||
|
pub fn subject(&mut self, subject: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||||
|
self.subject = Some(subject.into());
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the picture of the room
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `picture` - The new subject to set
|
||||||
|
/// * `cx` - The context to notify about the update
|
||||||
|
pub fn picture(&mut self, picture: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||||
|
self.picture = Some(picture.into());
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a human-readable string representing how long ago the room was created
|
||||||
|
///
|
||||||
|
/// The string will be formatted differently based on the time elapsed:
|
||||||
|
/// - Less than a minute: "now"
|
||||||
|
/// - Less than an hour: "Xm" (minutes)
|
||||||
|
/// - Less than a day: "Xh" (hours)
|
||||||
|
/// - Less than a month: "Xd" (days)
|
||||||
|
/// - More than a month: "MMM DD" (month abbreviation and day)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A SharedString containing the formatted time representation
|
||||||
pub fn ago(&self) -> SharedString {
|
pub fn ago(&self) -> SharedString {
|
||||||
let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
|
let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
|
||||||
chrono::LocalResult::Single(time) => time,
|
chrono::LocalResult::Single(time) => time,
|
||||||
@@ -156,56 +211,86 @@ impl Room {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the first member in the room that isn't the current user
|
/// Gets the display name for the room
|
||||||
|
///
|
||||||
|
/// If the room has a subject set, that will be used as the display name.
|
||||||
|
/// Otherwise, it will generate a name based on the room members.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `cx` - The App context
|
/// * `cx` - The application context
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// The Profile of the first member in the room
|
/// A SharedString containing the display name
|
||||||
pub fn first_member(&self, cx: &App) -> Profile {
|
pub fn display_name(&self, cx: &App) -> SharedString {
|
||||||
let Some(account) = Identity::get_global(cx).profile() else {
|
if let Some(subject) = self.subject.clone() {
|
||||||
return shared_state().person(&self.members[0]);
|
subject
|
||||||
};
|
} else {
|
||||||
|
self.merge_name(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(public_key) = self
|
/// Gets the display image for the room
|
||||||
.members
|
///
|
||||||
|
/// The image is determined by:
|
||||||
|
/// - The room's picture if set
|
||||||
|
/// - The first member's avatar for 1:1 chats
|
||||||
|
/// - A default group image for group chats
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `cx` - The application context
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A SharedString containing the image path or URL
|
||||||
|
pub fn display_image(&self, cx: &App) -> SharedString {
|
||||||
|
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
||||||
|
|
||||||
|
if let Some(picture) = self.picture.as_ref() {
|
||||||
|
picture.clone()
|
||||||
|
} else if !self.is_group() {
|
||||||
|
self.first_member(cx).avatar_url(proxy)
|
||||||
|
} else {
|
||||||
|
"brand/group.png".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the first member of the room.
|
||||||
|
///
|
||||||
|
/// First member is always different from the current user.
|
||||||
|
pub(crate) fn first_member(&self, cx: &App) -> Profile {
|
||||||
|
let registry = Registry::read_global(cx);
|
||||||
|
|
||||||
|
if let Some(identity) = Identity::read_global(cx).public_key().as_ref() {
|
||||||
|
self.members
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|&pubkey| pubkey != &account.public_key())
|
.filter(|&pubkey| pubkey != identity)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.first()
|
.first()
|
||||||
{
|
.map(|public_key| registry.get_person(public_key, cx))
|
||||||
shared_state().person(public_key)
|
.unwrap_or(registry.get_person(identity, cx))
|
||||||
} else {
|
} else {
|
||||||
account
|
registry.get_person(&self.members[0], cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets a formatted string of member names
|
/// Merge the names of the first two members of the room.
|
||||||
///
|
pub(crate) fn merge_name(&self, cx: &App) -> SharedString {
|
||||||
/// # Arguments
|
let registry = Registry::read_global(cx);
|
||||||
///
|
|
||||||
/// * `cx` - The App context
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// A SharedString containing formatted member names:
|
|
||||||
/// - For a group chat: "name1, name2, +X" where X is the number of additional members
|
|
||||||
/// - For a direct message: just the name of the other person
|
|
||||||
pub fn names(&self, cx: &App) -> SharedString {
|
|
||||||
if self.is_group() {
|
if self.is_group() {
|
||||||
let profiles = self
|
let profiles = self
|
||||||
.members
|
.members
|
||||||
.iter()
|
.iter()
|
||||||
.map(|public_key| shared_state().person(public_key))
|
.map(|pk| registry.get_person(pk, cx))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut name = profiles
|
let mut name = profiles
|
||||||
.iter()
|
.iter()
|
||||||
.take(2)
|
.take(2)
|
||||||
.map(|profile| profile.render_name())
|
.map(|p| p.display_name())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
@@ -215,11 +300,11 @@ impl Room {
|
|||||||
|
|
||||||
name.into()
|
name.into()
|
||||||
} else {
|
} else {
|
||||||
self.first_member(cx).render_name()
|
self.first_member(cx).display_name()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the display name for the room
|
/// Loads all profiles for this room members from the database
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
@@ -227,148 +312,20 @@ impl Room {
|
|||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// A SharedString representing the display name:
|
/// A Task that resolves to Result<Vec<Profile>, Error> containing all profiles for this room
|
||||||
/// - The subject of the room if it exists
|
pub fn load_metadata(&self, cx: &mut Context<Self>) -> Task<Result<Vec<Profile>, Error>> {
|
||||||
/// - Otherwise, the formatted names of the members
|
let public_keys = self.members.clone();
|
||||||
pub fn display_name(&self, cx: &App) -> SharedString {
|
|
||||||
if let Some(subject) = self.subject.as_ref() {
|
|
||||||
subject.clone()
|
|
||||||
} else {
|
|
||||||
self.names(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the display image for the room
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `cx` - The App context
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// An Option<SharedString> containing the avatar:
|
|
||||||
/// - For a direct message: the other person's avatar
|
|
||||||
/// - For a group chat: None
|
|
||||||
pub fn display_image(&self, cx: &App) -> SharedString {
|
|
||||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
|
||||||
|
|
||||||
if let Some(picture) = self.picture.as_ref() {
|
|
||||||
picture.clone()
|
|
||||||
} else if !self.is_group() {
|
|
||||||
self.first_member(cx).render_avatar(proxy)
|
|
||||||
} else {
|
|
||||||
"brand/group.png".into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if the room is a group chat
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// true if the room has more than 2 members, false otherwise
|
|
||||||
pub fn is_group(&self) -> bool {
|
|
||||||
self.members.len() > 2
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the room kind to ongoing
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `cx` - The context to notify about the update
|
|
||||||
pub fn set_ongoing(&mut self, cx: &mut Context<Self>) {
|
|
||||||
if self.kind != RoomKind::Ongoing {
|
|
||||||
self.kind = RoomKind::Ongoing;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the creation timestamp of the room
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `created_at` - The new Timestamp to set
|
|
||||||
/// * `cx` - The context to notify about the update
|
|
||||||
pub fn created_at(&mut self, created_at: Timestamp, cx: &mut Context<Self>) {
|
|
||||||
self.created_at = created_at;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the subject of the room
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `subject` - The new subject to set
|
|
||||||
/// * `cx` - The context to notify about the update
|
|
||||||
pub fn subject(&mut self, subject: String, cx: &mut Context<Self>) {
|
|
||||||
self.subject = Some(subject.into());
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the picture of the room
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `picture` - The new subject to set
|
|
||||||
/// * `cx` - The context to notify about the update
|
|
||||||
pub fn picture(&mut self, picture: String, cx: &mut Context<Self>) {
|
|
||||||
self.picture = Some(picture.into());
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetches metadata for all members in the room
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `cx` - The context for the background task
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// A Task that resolves to Result<(), Error>
|
|
||||||
pub fn load_metadata(&self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
|
|
||||||
let public_keys = Arc::clone(&self.members);
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let database = shared_state().client().database();
|
let database = nostr_client().database();
|
||||||
|
let mut profiles = vec![];
|
||||||
|
|
||||||
for public_key in public_keys.iter().cloned() {
|
for public_key in public_keys.into_iter() {
|
||||||
if !shared_state().has_person(&public_key).await {
|
let metadata = database.metadata(public_key).await?.unwrap_or_default();
|
||||||
let metadata = database.metadata(public_key).await?;
|
profiles.push(Profile::new(public_key, metadata));
|
||||||
shared_state().insert_person(public_key, metadata).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(profiles)
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks which members have inbox relays set up
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `cx` - The App context
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// A Task that resolves to Result<Vec<(PublicKey, bool)>, Error> where
|
|
||||||
/// the boolean indicates if the member has inbox relays configured
|
|
||||||
pub fn messaging_relays(&self, cx: &App) -> Task<Result<Vec<(PublicKey, bool)>, Error>> {
|
|
||||||
let pubkeys = Arc::clone(&self.members);
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let database = shared_state().client().database();
|
|
||||||
let mut result = Vec::with_capacity(pubkeys.len());
|
|
||||||
|
|
||||||
for pubkey in pubkeys.iter() {
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::InboxRelays)
|
|
||||||
.author(*pubkey)
|
|
||||||
.limit(1);
|
|
||||||
let is_ready = database.query(filter).await?.first().is_some();
|
|
||||||
|
|
||||||
result.push((*pubkey, is_ready));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,19 +337,19 @@ impl Room {
|
|||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// A Task that resolves to Result<Vec<RoomMessage>, Error> containing
|
/// A Task that resolves to Result<Vec<RoomMessage>, Error> containing all messages for this room
|
||||||
/// all messages for this room
|
|
||||||
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<Message>, Error>> {
|
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<Message>, Error>> {
|
||||||
let pubkeys = Arc::clone(&self.members);
|
let pubkeys = self.members.clone();
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::PrivateDirectMessage)
|
.kind(Kind::PrivateDirectMessage)
|
||||||
.authors(pubkeys.to_vec())
|
.authors(self.members.clone())
|
||||||
.pubkeys(pubkeys.to_vec());
|
.pubkeys(self.members.clone());
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let mut messages = vec![];
|
let mut messages = vec![];
|
||||||
let parser = NostrParser::new();
|
let parser = NostrParser::new();
|
||||||
let database = shared_state().client().database();
|
let database = nostr_client().database();
|
||||||
|
|
||||||
// Get all events from database
|
// Get all events from database
|
||||||
let events = database
|
let events = database
|
||||||
@@ -403,7 +360,7 @@ impl Room {
|
|||||||
.filter(|ev| {
|
.filter(|ev| {
|
||||||
let mut other_pubkeys = ev.tags.public_keys().copied().collect::<Vec<_>>();
|
let mut other_pubkeys = ev.tags.public_keys().copied().collect::<Vec<_>>();
|
||||||
other_pubkeys.push(ev.pubkey);
|
other_pubkeys.push(ev.pubkey);
|
||||||
// Check if the event is from a member of the room
|
// Check if the event is belong to a member of the current room
|
||||||
common::compare(&other_pubkeys, &pubkeys)
|
common::compare(&other_pubkeys, &pubkeys)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -411,7 +368,6 @@ impl Room {
|
|||||||
for event in events.into_iter() {
|
for event in events.into_iter() {
|
||||||
let content = event.content.clone();
|
let content = event.content.clone();
|
||||||
let tokens = parser.parse(&content);
|
let tokens = parser.parse(&content);
|
||||||
let mut mentions = vec![];
|
|
||||||
let mut replies_to = vec![];
|
let mut replies_to = vec![];
|
||||||
|
|
||||||
for tag in event.tags.filter(TagKind::e()) {
|
for tag in event.tags.filter(TagKind::e()) {
|
||||||
@@ -430,7 +386,7 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let pubkey_tokens = tokens
|
let mentions = tokens
|
||||||
.filter_map(|token| match token {
|
.filter_map(|token| match token {
|
||||||
Token::Nostr(nip21) => match nip21 {
|
Token::Nostr(nip21) => match nip21 {
|
||||||
Nip21::Pubkey(pubkey) => Some(pubkey),
|
Nip21::Pubkey(pubkey) => Some(pubkey),
|
||||||
@@ -441,16 +397,8 @@ impl Room {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for pubkey in pubkey_tokens.iter() {
|
if let Ok(message) = Message::builder(event.id, event.pubkey)
|
||||||
mentions.push(shared_state().async_person(pubkey).await);
|
|
||||||
}
|
|
||||||
|
|
||||||
let author = shared_state().async_person(&event.pubkey).await;
|
|
||||||
|
|
||||||
if let Ok(message) = Message::builder()
|
|
||||||
.id(event.id)
|
|
||||||
.content(content)
|
.content(content)
|
||||||
.author(author)
|
|
||||||
.created_at(event.created_at)
|
.created_at(event.created_at)
|
||||||
.replies_to(replies_to)
|
.replies_to(replies_to)
|
||||||
.mentions(mentions)
|
.mentions(mentions)
|
||||||
@@ -476,8 +424,6 @@ impl Room {
|
|||||||
///
|
///
|
||||||
/// Processes the event and emits an Incoming to the UI when complete
|
/// Processes the event and emits an Incoming to the UI when complete
|
||||||
pub fn emit_message(&self, event: Event, _window: &mut Window, cx: &mut Context<Self>) {
|
pub fn emit_message(&self, event: Event, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let author = shared_state().person(&event.pubkey);
|
|
||||||
|
|
||||||
// Extract all mentions from content
|
// Extract all mentions from content
|
||||||
let mentions = extract_mentions(&event.content);
|
let mentions = extract_mentions(&event.content);
|
||||||
|
|
||||||
@@ -500,10 +446,8 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(message) = Message::builder()
|
if let Ok(message) = Message::builder(event.id, event.pubkey)
|
||||||
.id(event.id)
|
|
||||||
.content(event.content)
|
.content(event.content)
|
||||||
.author(author)
|
|
||||||
.created_at(event.created_at)
|
.created_at(event.created_at)
|
||||||
.replies_to(replies_to)
|
.replies_to(replies_to)
|
||||||
.mentions(mentions)
|
.mentions(mentions)
|
||||||
@@ -534,8 +478,7 @@ impl Room {
|
|||||||
replies: Option<&Vec<Message>>,
|
replies: Option<&Vec<Message>>,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Option<Message> {
|
) -> Option<Message> {
|
||||||
let author = Identity::get_global(cx).profile()?;
|
let public_key = Identity::read_global(cx).public_key()?;
|
||||||
let public_key = author.public_key();
|
|
||||||
let builder = EventBuilder::private_msg_rumor(public_key, content);
|
let builder = EventBuilder::private_msg_rumor(public_key, content);
|
||||||
|
|
||||||
// Add event reference if it's present (replying to another event)
|
// Add event reference if it's present (replying to another event)
|
||||||
@@ -543,10 +486,10 @@ impl Room {
|
|||||||
|
|
||||||
if let Some(replies) = replies {
|
if let Some(replies) = replies {
|
||||||
if replies.len() == 1 {
|
if replies.len() == 1 {
|
||||||
refs.push(Tag::event(replies[0].id.unwrap()))
|
refs.push(Tag::event(replies[0].id))
|
||||||
} else {
|
} else {
|
||||||
for message in replies.iter() {
|
for message in replies.iter() {
|
||||||
refs.push(Tag::custom(TagKind::q(), vec![message.id.unwrap()]))
|
refs.push(Tag::custom(TagKind::q(), vec![message.id]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -582,10 +525,8 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::builder()
|
Message::builder(event.id.unwrap(), public_key)
|
||||||
.id(event.id.unwrap())
|
|
||||||
.content(event.content)
|
.content(event.content)
|
||||||
.author(author)
|
|
||||||
.created_at(event.created_at)
|
.created_at(event.created_at)
|
||||||
.replies_to(replies_to)
|
.replies_to(replies_to)
|
||||||
.mentions(mentions)
|
.mentions(mentions)
|
||||||
@@ -614,11 +555,11 @@ impl Room {
|
|||||||
let replies = replies.cloned();
|
let replies = replies.cloned();
|
||||||
let subject = self.subject.clone();
|
let subject = self.subject.clone();
|
||||||
let picture = self.picture.clone();
|
let picture = self.picture.clone();
|
||||||
let public_keys = Arc::clone(&self.members);
|
let public_keys = self.members.clone();
|
||||||
let backup = AppSettings::get_global(cx).settings.backup_messages;
|
let backup = AppSettings::get_global(cx).settings.backup_messages;
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let client = shared_state().client();
|
let client = nostr_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?;
|
||||||
|
|
||||||
@@ -637,10 +578,10 @@ impl Room {
|
|||||||
// Add event reference if it's present (replying to another event)
|
// Add event reference if it's present (replying to another event)
|
||||||
if let Some(replies) = replies {
|
if let Some(replies) = replies {
|
||||||
if replies.len() == 1 {
|
if replies.len() == 1 {
|
||||||
tags.push(Tag::event(replies[0].id.unwrap()))
|
tags.push(Tag::event(replies[0].id))
|
||||||
} else {
|
} else {
|
||||||
for message in replies.iter() {
|
for message in replies.iter() {
|
||||||
tags.push(Tag::custom(TagKind::q(), vec![message.id.unwrap()]))
|
tags.push(Tag::custom(TagKind::q(), vec![message.id]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,12 +647,11 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_mentions(content: &str) -> Vec<Profile> {
|
pub(crate) fn extract_mentions(content: &str) -> Vec<PublicKey> {
|
||||||
let parser = NostrParser::new();
|
let parser = NostrParser::new();
|
||||||
let tokens = parser.parse(content);
|
let tokens = parser.parse(content);
|
||||||
let mut mentions = vec![];
|
|
||||||
|
|
||||||
let pubkey_tokens = tokens
|
tokens
|
||||||
.filter_map(|token| match token {
|
.filter_map(|token| match token {
|
||||||
Token::Nostr(nip21) => match nip21 {
|
Token::Nostr(nip21) => match nip21 {
|
||||||
Nip21::Pubkey(pubkey) => Some(pubkey),
|
Nip21::Pubkey(pubkey) => Some(pubkey),
|
||||||
@@ -720,11 +660,5 @@ pub fn extract_mentions(content: &str) -> Vec<Profile> {
|
|||||||
},
|
},
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>()
|
||||||
|
|
||||||
for pubkey in pubkey_tokens.into_iter() {
|
|
||||||
mentions.push(shared_state().person(&pubkey));
|
|
||||||
}
|
|
||||||
|
|
||||||
mentions
|
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,6 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
|
|
||||||
rust-i18n.workspace = true
|
|
||||||
i18n.workspace = true
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use global::constants::SETTINGS_D;
|
use global::{constants::SETTINGS_D, nostr_client};
|
||||||
use global::shared_state;
|
|
||||||
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};
|
||||||
|
|
||||||
i18n::init!();
|
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
let state = cx.new(AppSettings::new);
|
let state = cx.new(AppSettings::new);
|
||||||
|
|
||||||
@@ -86,14 +83,12 @@ impl AppSettings {
|
|||||||
|
|
||||||
pub(crate) fn get_settings_from_db(&self, cx: &mut Context<Self>) {
|
pub(crate) fn get_settings_from_db(&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 database = shared_state().client().database();
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::ApplicationSpecificData)
|
.kind(Kind::ApplicationSpecificData)
|
||||||
.identifier(SETTINGS_D)
|
.identifier(SETTINGS_D)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if let Some(event) = database.query(filter).await?.first_owned() {
|
if let Some(event) = nostr_client().database().query(filter).await?.first_owned() {
|
||||||
log::info!("Successfully loaded settings from database");
|
log::info!("Successfully loaded settings from database");
|
||||||
Ok(serde_json::from_str(&event.content)?)
|
Ok(serde_json::from_str(&event.content)?)
|
||||||
} else {
|
} else {
|
||||||
@@ -117,14 +112,13 @@ impl AppSettings {
|
|||||||
if let Ok(content) = serde_json::to_string(&self.settings) {
|
if let Ok(content) = serde_json::to_string(&self.settings) {
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let keys = Keys::generate();
|
let keys = Keys::generate();
|
||||||
let database = shared_state().client().database();
|
|
||||||
|
|
||||||
if let Ok(event) = EventBuilder::new(Kind::ApplicationSpecificData, content)
|
if let Ok(event) = EventBuilder::new(Kind::ApplicationSpecificData, content)
|
||||||
.tags(vec![Tag::identifier(SETTINGS_D)])
|
.tags(vec![Tag::identifier(SETTINGS_D)])
|
||||||
.sign(&keys)
|
.sign(&keys)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if let Err(e) = database.save_event(&event).await {
|
if let Err(e) = nostr_client().database().save_event(&event).await {
|
||||||
log::error!("Failed to save user settings: {e}");
|
log::error!("Failed to save user settings: {e}");
|
||||||
} else {
|
} else {
|
||||||
log::info!("New settings have been saved successfully");
|
log::info!("New settings have been saved successfully");
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use common::profile::RenderProfile;
|
use common::display::DisplayProfile;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AnyView, App, ElementId, FontWeight, HighlightStyle, InteractiveText, IntoElement,
|
AnyElement, AnyView, App, ElementId, FontWeight, HighlightStyle, InteractiveText, IntoElement,
|
||||||
SharedString, StyledText, UnderlineStyle, Window,
|
SharedString, StyledText, UnderlineStyle, Window,
|
||||||
@@ -45,7 +45,7 @@ pub struct RichText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RichText {
|
impl RichText {
|
||||||
pub fn new(content: String, profiles: &[Profile]) -> Self {
|
pub fn new(content: String, profiles: &[Option<Profile>]) -> Self {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let mut highlights = Vec::new();
|
let mut highlights = Vec::new();
|
||||||
let mut link_ranges = Vec::new();
|
let mut link_ranges = Vec::new();
|
||||||
@@ -156,7 +156,7 @@ impl RichText {
|
|||||||
|
|
||||||
pub fn render_plain_text_mut(
|
pub fn render_plain_text_mut(
|
||||||
content: &str,
|
content: &str,
|
||||||
profiles: &[Profile],
|
profiles: &[Option<Profile>],
|
||||||
text: &mut String,
|
text: &mut String,
|
||||||
highlights: &mut Vec<(Range<usize>, Highlight)>,
|
highlights: &mut Vec<(Range<usize>, Highlight)>,
|
||||||
link_ranges: &mut Vec<Range<usize>>,
|
link_ranges: &mut Vec<Range<usize>>,
|
||||||
@@ -168,7 +168,11 @@ pub fn render_plain_text_mut(
|
|||||||
// Create a profile lookup using PublicKey directly
|
// Create a profile lookup using PublicKey directly
|
||||||
let profile_lookup: HashMap<PublicKey, Profile> = profiles
|
let profile_lookup: HashMap<PublicKey, Profile> = profiles
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter_map(|profile| {
|
||||||
|
profile
|
||||||
|
.as_ref()
|
||||||
.map(|profile| (profile.public_key(), profile.clone()))
|
.map(|profile| (profile.public_key(), profile.clone()))
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Process regular URLs using linkify
|
// Process regular URLs using linkify
|
||||||
@@ -276,7 +280,7 @@ pub fn render_plain_text_mut(
|
|||||||
|
|
||||||
if let Some(profile) = profile_match {
|
if let Some(profile) = profile_match {
|
||||||
// Profile found - create a mention
|
// Profile found - create a mention
|
||||||
let display_name = format!("@{}", profile.render_name());
|
let display_name = format!("@{}", profile.display_name());
|
||||||
|
|
||||||
// Replace mention with profile name
|
// Replace mention with profile name
|
||||||
text.replace_range(range.clone(), &display_name);
|
text.replace_range(range.clone(), &display_name);
|
||||||
|
|||||||
@@ -252,15 +252,15 @@ login:
|
|||||||
pt: "Continuar com a chave privada ou Bunker URI"
|
pt: "Continuar com a chave privada ou Bunker URI"
|
||||||
ko: "개인 키 또는 Bunker URI로 계속"
|
ko: "개인 키 또는 Bunker URI로 계속"
|
||||||
approve_message:
|
approve_message:
|
||||||
en: "Approve connection request from your signer in {i} seconds"
|
en: "Approve connection request from your signer in %{i} seconds"
|
||||||
zh-CN: "在 {i} 秒内批准来自您的 signer 的连接请求"
|
zh-CN: "在 %{i} 秒内批准来自您的 signer 的连接请求"
|
||||||
zh-TW: "在 {i} 秒內批准來自您的 signer 的連接請求"
|
zh-TW: "在 %{i} 秒內批准來自您的 signer 的連接請求"
|
||||||
ru: "Подтвердите запрос на подключение от вашего signer в течение {i} секунд"
|
ru: "Подтвердите запрос на подключение от вашего signer в течение %{i} секунд"
|
||||||
vi: "Phê duyệt yêu cầu kết nối từ signer của bạn trong {i} giây"
|
vi: "Phê duyệt yêu cầu kết nối từ signer của bạn trong %{i} giây"
|
||||||
ja: "{i} 秒以内にあなたの signer からの接続リクエストを承認してください"
|
ja: "%{i} 秒以内にあなたの signer からの接続リクエストを承認してください"
|
||||||
es: "Aprueba la solicitud de conexión de tu signer en {i} segundos"
|
es: "Aprueba la solicitud de conexión de tu signer en %{i} segundos"
|
||||||
pt: "Aprove a solicitação de conexão do seu signer em {i} segundos"
|
pt: "Aprove a solicitação de conexão do seu signer em %{i} segundos"
|
||||||
ko: "{i}초 내에 signer의 연결 요청을 승인하세요"
|
ko: "%{i}초 내에 signer의 연결 요청을 승인하세요"
|
||||||
nostr_connect:
|
nostr_connect:
|
||||||
en: "Continue with Nostr Connect"
|
en: "Continue with Nostr Connect"
|
||||||
zh-CN: "继续使用 Nostr Connect"
|
zh-CN: "继续使用 Nostr Connect"
|
||||||
@@ -824,15 +824,15 @@ compose:
|
|||||||
pt: "Seus contatos recentes aparecerão aqui."
|
pt: "Seus contatos recentes aparecerão aqui."
|
||||||
ko: "최근 연락처가 여기에 표시됩니다."
|
ko: "최근 연락처가 여기에 표시됩니다."
|
||||||
contact_existed:
|
contact_existed:
|
||||||
en: "Contact already added: {name}"
|
en: "Contact already added"
|
||||||
zh-CN: "联系人已添加:{name}"
|
zh-CN: "联系人已添加"
|
||||||
zh-TW: "聯絡人已新增:{name}"
|
zh-TW: "聯絡人已新增"
|
||||||
ru: "Контакт уже добавлен: {name}"
|
ru: "Контакт уже добавлен"
|
||||||
vi: "Danh bạ đã được thêm: {name}"
|
vi: "Danh bạ đã được thêm"
|
||||||
ja: "連絡先は既に追加されています: {name}"
|
ja: "連絡先は既に追加されています"
|
||||||
es: "Contacto ya añadido: {name}"
|
es: "Contacto ya añadido"
|
||||||
pt: "Contato já adicionado: {name}"
|
pt: "Contato já adicionado"
|
||||||
ko: "이미 추가된 연락처: {name}"
|
ko: "이미 추가된 연락처"
|
||||||
receiver_required:
|
receiver_required:
|
||||||
en: "You need to add at least 1 receiver"
|
en: "You need to add at least 1 receiver"
|
||||||
zh-CN: "您需要添加至少1个收件人"
|
zh-CN: "您需要添加至少1个收件人"
|
||||||
@@ -958,15 +958,15 @@ sidebar:
|
|||||||
pt: "Pressione Enter para pesquisar"
|
pt: "Pressione Enter para pesquisar"
|
||||||
ko: "Enter 키를 눌러 검색"
|
ko: "Enter 키를 눌러 검색"
|
||||||
empty:
|
empty:
|
||||||
en: "There are no users matching query {query}"
|
en: "There are no users matching query %{query}"
|
||||||
zh-CN: "没有匹配查询 {query} 的用户"
|
zh-CN: "没有匹配查询 %{query} 的用户"
|
||||||
zh-TW: "沒有匹配查詢 {query} 的用戶"
|
zh-TW: "沒有匹配查詢 %{query} 的用戶"
|
||||||
ru: "Нет пользователей, соответствующих запросу {query}"
|
ru: "Нет пользователей, соответствующих запросу %{query}"
|
||||||
vi: "Không có người dùng phù hợp với truy vấn {query}"
|
vi: "Không có người dùng phù hợp với truy vấn %{query}"
|
||||||
ja: "クエリ {query} に一致するユーザーがいません"
|
ja: "クエリ %{query} に一致するユーザーがいません"
|
||||||
es: "No hay usuarios que coincidan con la consulta {query}"
|
es: "No hay usuarios que coincidan con la consulta %{query}"
|
||||||
pt: "Não há usuários correspondentes à consulta {query}"
|
pt: "Não há usuários correspondentes à consulta %{query}"
|
||||||
ko: "쿼리 {query}와 일치하는 사용자가 없습니다"
|
ko: "쿼리 %{query}와 일치하는 사용자가 없습니다"
|
||||||
search_in_progress:
|
search_in_progress:
|
||||||
en: "There is another search in progress"
|
en: "There is another search in progress"
|
||||||
zh-CN: "正在进行另一个搜索"
|
zh-CN: "正在进行另一个搜索"
|
||||||
|
|||||||
Reference in New Issue
Block a user