Continue redesign for the v1 stable release #5

Merged
reya merged 11 commits from redesign-2 into master 2026-02-12 08:32:17 +00:00
17 changed files with 1081 additions and 872 deletions
Showing only changes of commit 253d04f988 - Show all commits

406
Cargo.lock generated
View File

@@ -85,6 +85,12 @@ dependencies = [
"equator",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -530,6 +536,15 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "atoi"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
dependencies = [
"num-traits",
]
[[package]]
name = "atomic"
version = "0.5.3"
@@ -1349,6 +1364,12 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-random"
version = "0.1.18"
@@ -1599,6 +1620,21 @@ dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -1693,6 +1729,17 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204"
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"pem-rfc7468",
"zeroize",
]
[[package]]
name = "derive_more"
version = "0.99.20"
@@ -1741,6 +1788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"subtle",
]
@@ -1845,6 +1893,12 @@ dependencies = [
"ui",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "downcast-rs"
version = "1.2.1"
@@ -1904,6 +1958,9 @@ name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
dependencies = [
"serde",
]
[[package]]
name = "embed-resource"
@@ -2008,7 +2065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -2021,6 +2078,17 @@ dependencies = [
"svg_fmt",
]
[[package]]
name = "etcetera"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.48.0",
]
[[package]]
name = "euclid"
version = "0.22.13"
@@ -2409,6 +2477,17 @@ dependencies = [
"futures-util",
]
[[package]]
name = "futures-intrusive"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
dependencies = [
"futures-core",
"lock_api",
"parking_lot",
]
[[package]]
name = "futures-io"
version = "0.3.31"
@@ -2811,6 +2890,8 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
@@ -2820,6 +2901,15 @@ version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "hashlink"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.15.5",
]
[[package]]
name = "heck"
version = "0.4.1"
@@ -3538,6 +3628,17 @@ dependencies = [
"redox_syscall 0.7.0",
]
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linicon"
version = "2.3.0"
@@ -3982,7 +4083,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "nostr"
version = "0.44.1"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd"
source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [
"aes",
"base64",
@@ -4007,7 +4108,7 @@ dependencies = [
[[package]]
name = "nostr-connect"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd"
source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [
"async-utility",
"futures-core",
@@ -4020,7 +4121,7 @@ dependencies = [
[[package]]
name = "nostr-database"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd"
source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [
"btreecap",
"flatbuffers",
@@ -4032,27 +4133,26 @@ dependencies = [
[[package]]
name = "nostr-gossip"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd"
source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [
"nostr",
]
[[package]]
name = "nostr-gossip-memory"
name = "nostr-gossip-sqlite"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd"
source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [
"indexmap",
"lru",
"nostr",
"nostr-gossip",
"sqlx",
"tokio",
]
[[package]]
name = "nostr-lmdb"
version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd"
source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [
"async-utility",
"flume",
@@ -4066,7 +4166,7 @@ dependencies = [
[[package]]
name = "nostr-sdk"
version = "0.44.1"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd"
source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [
"async-utility",
"async-wsocket",
@@ -4097,7 +4197,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -4591,6 +4691,15 @@ dependencies = [
"hmac",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -4727,6 +4836,27 @@ dependencies = [
"futures-io",
]
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "pkg-config"
version = "0.3.32"
@@ -5007,7 +5137,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -5421,6 +5551,26 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "rsa"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [
"const-oid",
"digest",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rust-embed"
version = "8.11.0"
@@ -5516,7 +5666,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -6029,6 +6179,16 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core 0.6.4",
]
[[package]]
name = "simd-adler32"
version = "0.3.8"
@@ -6089,6 +6249,9 @@ name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
[[package]]
name = "smol"
@@ -6150,6 +6313,204 @@ dependencies = [
"bitflags 2.10.0",
]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "sqlx"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
dependencies = [
"sqlx-core",
"sqlx-macros",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
]
[[package]]
name = "sqlx-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [
"base64",
"bytes",
"crc",
"crossbeam-queue",
"either",
"event-listener 5.4.1",
"futures-core",
"futures-intrusive",
"futures-io",
"futures-util",
"hashbrown 0.15.5",
"hashlink",
"indexmap",
"log",
"memchr",
"once_cell",
"percent-encoding",
"serde",
"serde_json",
"sha2",
"smallvec",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tracing",
"url",
]
[[package]]
name = "sqlx-macros"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
dependencies = [
"proc-macro2",
"quote",
"sqlx-core",
"sqlx-macros-core",
"syn 2.0.114",
]
[[package]]
name = "sqlx-macros-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
dependencies = [
"dotenvy",
"either",
"heck 0.5.0",
"hex",
"once_cell",
"proc-macro2",
"quote",
"serde",
"serde_json",
"sha2",
"sqlx-core",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.114",
"tokio",
"url",
]
[[package]]
name = "sqlx-mysql"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [
"atoi",
"base64",
"bitflags 2.10.0",
"byteorder",
"bytes",
"crc",
"digest",
"dotenvy",
"either",
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"generic-array",
"hex",
"hkdf",
"hmac",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"percent-encoding",
"rand 0.8.5",
"rsa",
"serde",
"sha1",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.18",
"tracing",
"whoami",
]
[[package]]
name = "sqlx-postgres"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [
"atoi",
"base64",
"bitflags 2.10.0",
"byteorder",
"crc",
"dotenvy",
"etcetera",
"futures-channel",
"futures-core",
"futures-util",
"hex",
"hkdf",
"hmac",
"home",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"rand 0.8.5",
"serde",
"serde_json",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.18",
"tracing",
"whoami",
]
[[package]]
name = "sqlx-sqlite"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
dependencies = [
"atoi",
"flume",
"futures-channel",
"futures-core",
"futures-executor",
"futures-intrusive",
"futures-util",
"libsqlite3-sys",
"log",
"percent-encoding",
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror 2.0.18",
"tracing",
"url",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -6201,7 +6562,7 @@ dependencies = [
"gpui_tokio",
"log",
"nostr-connect",
"nostr-gossip-memory",
"nostr-gossip-sqlite",
"nostr-lmdb",
"nostr-sdk",
"petname",
@@ -6228,6 +6589,17 @@ dependencies = [
"float-cmp",
]
[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"unicode-properties",
]
[[package]]
name = "strsim"
version = "0.11.1"
@@ -6546,7 +6918,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.3",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -7753,7 +8125,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.48.0",
]
[[package]]

View File

@@ -19,7 +19,7 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" }
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] }
nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" }
nostr-gossip-sqlite = { git = "https://github.com/rust-nostr/nostr" }
# Others
anyhow = "1.0.44"

View File

@@ -98,7 +98,7 @@ impl ChatRegistry {
/// Create a new chat registry instance
fn new(cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity();
let nip17_state = nostr.read(cx).nip17_state();
let device = DeviceRegistry::global(cx);
let device_signer = device.read(cx).device_signer.clone();
@@ -114,8 +114,8 @@ impl ChatRegistry {
subscriptions.push(
// Observe the identity
cx.observe(&identity, |this, state, cx| {
if state.read(cx).messaging_relays_state() == RelayState::Set {
cx.observe(&nip17_state, |this, state, cx| {
if state.read(cx) == &RelayState::Configured {
// Handle nostr notifications
this.handle_notifications(cx);
// Track unwrapping progress
@@ -536,9 +536,9 @@ impl ChatRegistry {
}
// Set this room is ongoing if the new message is from current user
if author == nostr.read(cx).identity().read(cx).public_key() {
this.set_ongoing(cx);
}
// if author == nostr.read(cx).identity().read(cx).public_key() {
// this.set_ongoing(cx);
// }
// Emit the new message to the room
this.emit_message(message, cx);

View File

@@ -216,28 +216,6 @@ impl Room {
self.members.clone()
}
/// Returns the members of the room with their messaging relays
pub fn members_with_relays(&self, cx: &App) -> Task<Vec<(PublicKey, Vec<RelayUrl>)>> {
let nostr = NostrRegistry::global(cx);
let mut tasks = vec![];
for member in self.members.iter() {
let task = nostr.read(cx).messaging_relays(member, cx);
tasks.push((*member, task));
}
cx.background_spawn(async move {
let mut results = vec![];
for (public_key, task) in tasks.into_iter() {
let urls = task.await;
results.push((public_key, urls));
}
results
})
}
/// Checks if the room has more than two members (group)
pub fn is_group(&self) -> bool {
self.members.len() > 2
@@ -266,17 +244,7 @@ impl Room {
/// Display member is always different from the current user.
pub fn display_member(&self, cx: &App) -> Person {
let persons = PersonRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
let target_member = self
.members
.iter()
.find(|&member| member != &public_key)
.or_else(|| self.members.first())
.expect("Room should have at least one member");
persons.read(cx).get(target_member, cx)
persons.read(cx).get(&self.members[0], cx)
}
/// Merge the names of the first two members of the room.
@@ -377,68 +345,79 @@ impl Room {
})
}
/// Create a new message event (unsigned)
pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent {
/// Create a new unsigned message event
pub fn create_message(
&self,
content: &str,
replies: Vec<EventId>,
cx: &App,
) -> Task<Result<UnsignedEvent, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Get current user
let public_key = nostr.read(cx).identity().read(cx).public_key();
// Get room's subject
let subject = self.subject.clone();
let content = content.to_string();
let mut tags = vec![];
let mut member_and_relay_hints = HashMap::new();
// Add receivers
//
// NOTE: current user will be removed from the list of receivers
// Populate the hashmap with member and relay hint tasks
for member in self.members.iter() {
// Get relay hint if available
let relay_url = nostr.read(cx).relay_hint(member, cx);
// Construct a public key tag with relay hint
let tag = TagStandard::PublicKey {
public_key: member.to_owned(),
relay_url,
alias: None,
uppercase: false,
};
tags.push(Tag::from_standardized_without_cell(tag));
let hint = nostr.read(cx).relay_hint(member, cx);
member_and_relay_hints.insert(member.to_owned(), hint);
}
// Add subject tag if it's present
if let Some(value) = subject {
tags.push(Tag::from_standardized_without_cell(TagStandard::Subject(
value.to_string(),
)));
}
cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
// Add reply/quote tag
if replies.len() == 1 {
tags.push(Tag::event(replies[0]))
} else {
for id in replies {
let tag = TagStandard::Quote {
event_id: id.to_owned(),
relay_url: None,
public_key: None,
// List of event tags for each receiver
let mut tags = vec![];
for (member, task) in member_and_relay_hints.into_iter() {
// Skip current user
if member == public_key {
continue;
}
// Get relay hint if available
let relay_url = task.await;
// Construct a public key tag with relay hint
let tag = TagStandard::PublicKey {
public_key: member,
relay_url,
alias: None,
uppercase: false,
};
tags.push(Tag::from_standardized_without_cell(tag))
tags.push(Tag::from_standardized_without_cell(tag));
}
}
// Construct a direct message event
//
// WARNING: never sign and send this event to relays
let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content)
.tags(tags)
.build(public_key);
// Add subject tag if present
if let Some(value) = subject {
tags.push(Tag::from_standardized_without_cell(TagStandard::Subject(
value.to_string(),
)));
}
// Ensure the event id has been generated
event.ensure_id();
// Add all reply tags
for id in replies {
tags.push(Tag::event(id))
}
event
// Construct a direct message event
//
// WARNING: never sign and send this event to relays
// TODO
let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content)
.tags(tags)
.build(Keys::generate().public_key());
// Ensure the event ID has been generated
event.ensure_id();
Ok(event)
})
}
/// Create a task to send a message to all room members
@@ -450,46 +429,27 @@ impl Room {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Get current user's public key and relays
let current_user = nostr.read(cx).identity().read(cx).public_key();
let current_user_relays = nostr.read(cx).messaging_relays(&current_user, cx);
let mut members = self.members();
let rumor = rumor.to_owned();
// Get all members and their messaging relays
let task = self.members_with_relays(cx);
cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let current_user_relays = current_user_relays.await;
let mut members = task.await;
let current_user = signer.get_public_key().await?;
// Remove the current user's public key from the list of receivers
// the current user will be handled separately
members.retain(|(this, _)| this != &current_user);
members.retain(|this| this != &current_user);
// Collect the send reports
let mut reports: Vec<SendReport> = vec![];
for (receiver, relays) in members.into_iter() {
// Check if there are any relays to send the message to
if relays.is_empty() {
reports.push(SendReport::new(receiver).relays_not_found());
continue;
}
// Ensure relay connection
for url in relays.iter() {
client.add_relay(url).await?;
client.connect_relay(url).await?;
}
for receiver in members.into_iter() {
// Construct the gift wrap event
let event =
EventBuilder::gift_wrap(signer, &receiver, rumor.clone(), vec![]).await?;
// Send the gift wrap event to the messaging relays
match client.send_event(&event).to(&relays).await {
match client.send_event(&event).to_nip17().await {
Ok(output) => {
let id = output.id().to_owned();
let auth = output.failed.iter().any(|(_, s)| s.starts_with("auth-"));
@@ -531,20 +491,8 @@ impl Room {
// Only send a backup message to current user if sent successfully to others
if reports.iter().all(|r| r.is_sent_success()) {
// Check if there are any relays to send the event to
if current_user_relays.is_empty() {
reports.push(SendReport::new(current_user).relays_not_found());
return Ok(reports);
}
// Ensure relay connection
for url in current_user_relays.iter() {
client.add_relay(url).await?;
client.connect_relay(url).await?;
}
// Send the event to the messaging relays
match client.send_event_to(current_user_relays, &event).await {
match client.send_event(&event).to_nip17().await {
Ok(output) => {
reports.push(SendReport::new(current_user).status(output));
}

View File

@@ -1,7 +1,7 @@
use std::collections::HashSet;
use std::time::Duration;
pub use actions::*;
use anyhow::Error;
use chat::{Message, RenderedMessage, Room, RoomEvent, RoomKind, SendReport};
use common::{nip96_upload, RenderedTimestamp};
use dock::panel::{Panel, PanelEvent};
@@ -244,27 +244,21 @@ impl ChatPanel {
return;
}
// Get the current room entity
let Some(room) = self.room.upgrade().map(|this| this.read(cx)) else {
return;
};
// Get replies_to if it's present
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
// Create a temporary message for optimistic update
let rumor = room.create_message(&content, replies.as_ref(), cx);
let rumor_id = rumor.id.unwrap();
// Create a task for sending the message in the background
let send_message = room.send_message(&rumor, cx);
// Get a task to create temporary message for optimistic update
let Ok(get_rumor) = self
.room
.read_with(cx, |this, cx| this.create_message(&content, replies, cx))
else {
return;
};
// Optimistically update message list
cx.spawn_in(window, async move |this, cx| {
// Wait for the delay
cx.background_executor()
.timer(Duration::from_millis(100))
.await;
let task: Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
let mut rumor = get_rumor.await?;
let rumor_id = rumor.id();
// Update the message list and reset the states
this.update_in(cx, |this, window, cx| {
@@ -280,43 +274,50 @@ impl ChatPanel {
// Update the message list
this.insert_message(&rumor, true, cx);
})
.ok();
})
.detach();
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
let result = send_message.await;
if let Ok(task) = this
.room
.read_with(cx, |this, cx| this.send_message(&rumor, cx))
{
this.tasks.push(cx.spawn_in(window, async move |this, cx| {
let result = task.await;
this.update_in(cx, |this, window, cx| {
match result {
Ok(reports) => {
// Update room's status
this.room
.update(cx, |this, cx| {
if this.kind != RoomKind::Ongoing {
// Update the room kind to ongoing,
// but keep the room kind if send failed
if reports.iter().all(|r| !r.is_sent_success()) {
this.kind = RoomKind::Ongoing;
cx.notify();
}
this.update_in(cx, |this, window, cx| {
match result {
Ok(reports) => {
// Update room's status
this.room
.update(cx, |this, cx| {
if this.kind != RoomKind::Ongoing {
// Update the room kind to ongoing,
// but keep the room kind if send failed
if reports.iter().all(|r| !r.is_sent_success()) {
this.kind = RoomKind::Ongoing;
cx.notify();
}
}
})
.ok();
// Insert the sent reports
this.reports_by_id.insert(rumor_id, reports);
cx.notify();
}
})
.ok();
// Insert the sent reports
this.reports_by_id.insert(rumor_id, reports);
cx.notify();
}
Err(e) => {
window.push_notification(e.to_string(), cx);
}
Err(e) => {
window.push_notification(e.to_string(), cx);
}
}
})
.ok();
}))
}
})
.ok();
}));
})?;
Ok(())
});
task.detach();
}
/// Insert a message into the chat panel

View File

@@ -169,7 +169,6 @@ impl CommandBar {
fn search(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity();
let query = self.find_input.read(cx).value();
// Return if the query is empty
@@ -191,7 +190,7 @@ impl CommandBar {
// Block the input until the search completes
self.set_finding(true, window, cx);
let find_users = if identity.read(cx).owned {
let find_users = if nostr.read(cx).owned_signer() {
nostr.read(cx).wot_search(&query, cx)
} else {
nostr.read(cx).search(&query, cx)
@@ -245,17 +244,28 @@ impl CommandBar {
fn create(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let chat = ChatRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
let async_chat = chat.downgrade();
let nostr = NostrRegistry::global(cx);
let signer_pkey = nostr.read(cx).signer_pkey(cx);
// Get all selected public keys
let receivers = self.selected(cx);
chat.update(cx, |this, cx| {
let room = cx.new(|_| Room::new(public_key, receivers));
this.emit_room(room.downgrade(), cx);
let task: Task<Result<(), Error>> = cx.spawn_in(window, async move |_this, cx| {
let public_key = signer_pkey.await?;
async_chat.update_in(cx, |this, window, cx| {
let room = cx.new(|_| Room::new(public_key, receivers));
this.emit_room(room.downgrade(), cx);
window.close_modal(cx);
})?;
Ok(())
});
window.close_modal(cx);
task.detach();
}
fn select(&mut self, pkey: PublicKey, cx: &mut Context<Self>) {

View File

@@ -29,6 +29,26 @@ impl GreeterPanel {
focus_handle: cx.focus_handle(),
}
}
fn add_profile_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let signer_pkey = nostr.read(cx).signer_pkey(cx);
cx.spawn_in(window, async move |_this, cx| {
if let Ok(public_key) = signer_pkey.await {
cx.update(|window, cx| {
Workspace::add_panel(
profile::init(public_key, window, cx),
DockPlacement::Center,
window,
cx,
);
})
.ok();
}
})
.detach();
}
}
impl Panel for GreeterPanel {
@@ -62,12 +82,11 @@ impl Render for GreeterPanel {
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity();
let nip65_state = nostr.read(cx).nip65_state();
let nip17_state = nostr.read(cx).nip17_state();
let relay_list_state = identity.read(cx).relay_list_state();
let messaging_relay_state = identity.read(cx).messaging_relays_state();
let required_actions =
relay_list_state == RelayState::NotSet || messaging_relay_state == RelayState::NotSet;
let required_actions = nip65_state.read(cx) == &RelayState::NotConfigured
|| nip17_state.read(cx) == &RelayState::NotConfigured;
h_flex()
.size_full()
@@ -128,7 +147,7 @@ impl Render for GreeterPanel {
v_flex()
.gap_2()
.w_full()
.when(relay_list_state == RelayState::NotSet, |this| {
.when(nip65_state.read(cx).not_configured(), |this| {
this.child(
Button::new("relaylist")
.icon(Icon::new(IconName::Relay))
@@ -146,31 +165,28 @@ impl Render for GreeterPanel {
}),
)
})
.when(
messaging_relay_state == RelayState::NotSet,
|this| {
this.child(
Button::new("import")
.icon(Icon::new(IconName::Relay))
.label("Set up messaging relays")
.ghost()
.small()
.no_center()
.on_click(move |_ev, window, cx| {
Workspace::add_panel(
messaging_relays::init(window, cx),
DockPlacement::Center,
window,
cx,
);
}),
)
},
),
.when(nip17_state.read(cx).not_configured(), |this| {
this.child(
Button::new("import")
.icon(Icon::new(IconName::Relay))
.label("Set up messaging relays")
.ghost()
.small()
.no_center()
.on_click(move |_ev, window, cx| {
Workspace::add_panel(
messaging_relays::init(window, cx),
DockPlacement::Center,
window,
cx,
);
}),
)
}),
),
)
})
.when(!identity.read(cx).owned, |this| {
.when(!nostr.read(cx).owned_signer(), |this| {
this.child(
v_flex()
.gap_2()
@@ -257,14 +273,9 @@ impl Render for GreeterPanel {
.ghost()
.small()
.no_center()
.on_click(move |_ev, window, cx| {
Workspace::add_panel(
profile::init(window, cx),
DockPlacement::Center,
window,
cx,
);
}),
.on_click(cx.listener(move |this, _ev, window, cx| {
this.add_profile_panel(window, cx)
})),
)
.child(
Button::new("invite")

View File

@@ -156,29 +156,20 @@ impl MessagingRelayPanel {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let relays = self.relays.clone();
let tags: Vec<Tag> = self
.relays
.iter()
.map(|relay| Tag::relay(relay.clone()))
.collect();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
let tags: Vec<Tag> = relays
.iter()
.map(|relay| Tag::relay(relay.clone()))
.collect();
// Construct nip17 event builder
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
let event = client.sign_event_builder(builder).await?;
// Set messaging relays
client.send_event(&event).to(urls).await?;
// Connect to messaging relays
for relay in relays.iter() {
client.add_relay(relay).await.ok();
client.connect_relay(relay).await.ok();
}
client.send_event(&event).to_nip65().await?;
Ok(())
});

View File

@@ -22,8 +22,8 @@ use ui::input::{InputState, TextInput};
use ui::notification::Notification;
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ProfilePanel> {
cx.new(|cx| ProfilePanel::new(window, cx))
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<ProfilePanel> {
cx.new(|cx| ProfilePanel::new(public_key, window, cx))
}
#[derive(Debug)]
@@ -31,6 +31,9 @@ pub struct ProfilePanel {
name: SharedString,
focus_handle: FocusHandle,
/// User's public key
public_key: PublicKey,
/// User's name text input
name_input: Entity<InputState>,
@@ -51,13 +54,10 @@ pub struct ProfilePanel {
}
impl ProfilePanel {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
let website_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me"));
// Hidden input for avatar url
let avatar_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me/a.jpg"));
// Use multi-line input for bio
let bio_input = cx.new(|cx| {
InputState::new(window, cx)
@@ -66,13 +66,10 @@ impl ProfilePanel {
.placeholder("A short introduce about you.")
});
// Get user's profile and update inputs
cx.defer_in(window, move |this, window, cx| {
let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
// Set all input's values with current profile
this.set_profile(profile, window, cx);
});
@@ -80,6 +77,7 @@ impl ProfilePanel {
Self {
name: "Update Profile".into(),
focus_handle: cx.focus_handle(),
public_key,
name_input,
avatar_input,
bio_input,
@@ -209,7 +207,7 @@ impl ProfilePanel {
fn set_metadata(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
let public_key = self.public_key;
// Get the old metadata
let persons = PersonRegistry::global(cx);
@@ -289,9 +287,7 @@ impl Focusable for ProfilePanel {
impl Render for ProfilePanel {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
let shorten_pkey = SharedString::from(shorten_pubkey(public_key, 8));
let shorten_pkey = SharedString::from(shorten_pubkey(self.public_key, 8));
// Get the avatar
let avatar_input = self.avatar_input.read(cx).value();
@@ -390,7 +386,7 @@ impl Render for ProfilePanel {
.ghost()
.on_click(cx.listener(move |this, _ev, window, cx| {
this.copy(
public_key.to_bech32().unwrap(),
this.public_key.to_bech32().unwrap(),
window,
cx,
);

View File

@@ -9,10 +9,11 @@ use gpui::{
div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement,
ParentElement, Render, SharedString, Styled, Subscription, Window,
};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
use theme::{ActiveTheme, Theme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
use theme::{ActiveTheme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
use titlebar::TitleBar;
use ui::avatar::Avatar;
use ui::{h_flex, v_flex, Icon, IconName, Root, Sizable, WindowExtension};
@@ -35,6 +36,9 @@ pub struct Workspace {
/// App's Command Bar
command_bar: Entity<CommandBar>,
/// Current User
current_user: Entity<Option<PublicKey>>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 3]>,
}
@@ -42,20 +46,23 @@ pub struct Workspace {
impl Workspace {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx);
let current_user = cx.new(|_| None);
let nostr = NostrRegistry::global(cx);
let nip65_state = nostr.read(cx).nip65_state();
// Titlebar
let titlebar = cx.new(|_| TitleBar::new());
// Command bar
let command_bar = cx.new(|cx| CommandBar::new(window, cx));
// Dock
let dock =
cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar));
let mut subscriptions = smallvec![];
subscriptions.push(
// Automatically sync theme with system appearance
window.observe_window_appearance(|window, cx| {
Theme::sync_system_appearance(Some(window), cx);
}),
);
subscriptions.push(
// Observe all events emitted by the chat registry
cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| {
@@ -100,6 +107,15 @@ impl Workspace {
}),
);
subscriptions.push(
// Observe the NIP-65 state
cx.observe(&nip65_state, move |this, state, cx| {
if state.read(cx).idle() {
this.get_current_user(cx);
}
}),
);
// Set the default layout for app's dock
cx.defer_in(window, |this, window, cx| {
this.set_layout(window, cx);
@@ -109,6 +125,7 @@ impl Workspace {
titlebar,
dock,
command_bar,
current_user,
_subscriptions: subscriptions,
}
}
@@ -173,18 +190,35 @@ impl Workspace {
});
}
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
fn get_current_user(&self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity();
let client = nostr.read(cx).client();
let current_user = self.current_user.downgrade();
cx.spawn(async move |_this, cx| {
if let Some(signer) = client.signer() {
if let Ok(public_key) = signer.get_public_key().await {
current_user
.update(cx, |this, cx| {
*this = Some(public_key);
cx.notify();
})
.ok();
}
}
})
.detach();
}
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
h_flex()
.h(TITLEBAR_HEIGHT)
.flex_1()
.justify_between()
.gap_2()
.when_some(identity.read(cx).public_key, |this, public_key| {
.when_some(self.current_user.read(cx).as_ref(), |this, public_key| {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
let profile = persons.read(cx).get(public_key, cx);
this.child(
h_flex()

View File

@@ -40,7 +40,7 @@ pub struct DeviceRegistry {
tasks: Vec<Task<Result<(), Error>>>,
/// Subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
_subscriptions: SmallVec<[Subscription; 2]>,
}
impl DeviceRegistry {
@@ -58,7 +58,8 @@ impl DeviceRegistry {
fn new(cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let identity = nostr.read(cx).identity();
let nip65_state = nostr.read(cx).nip65_state();
let nip17_state = nostr.read(cx).nip17_state();
let device_signer = cx.new(|_| None);
let requests = cx.new(|_| HashSet::default());
@@ -70,21 +71,26 @@ impl DeviceRegistry {
let mut tasks = vec![];
subscriptions.push(
// Observe the identity entity
cx.observe(&identity, |this, state, cx| {
match state.read(cx).relay_list_state() {
RelayState::Initial => {
// Observe the NIP-65 state
cx.observe(&nip65_state, |this, state, cx| {
match state.read(cx) {
RelayState::Idle => {
this.reset(cx);
}
RelayState::Set => {
RelayState::Configured => {
this.get_announcement(cx);
if state.read(cx).messaging_relays_state() == RelayState::Set {
this.get_messages(cx);
}
}
_ => {}
}
};
}),
);
subscriptions.push(
// Observe the NIP-17 state
cx.observe(&nip17_state, |this, state, cx| {
if state.read(cx) == &RelayState::Configured {
this.get_messages(cx);
};
}),
);
@@ -265,29 +271,26 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let device_signer = self.device_signer.read(cx).clone();
let messaging_relays = nostr.read(cx).messaging_relays(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
let messaging_relays = nostr.read(cx).messaging_relays(&public_key, cx);
cx.background_spawn(async move {
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = messaging_relays.await;
let user_signer = client.signer().context("Signer not found")?;
let public_key = user_signer.get_public_key().await?;
// Get messages with dekey
if let Some(signer) = device_signer.as_ref() {
if let Ok(pkey) = signer.get_public_key().await {
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(pkey);
let id = SubscriptionId::new(DEVICE_GIFTWRAP);
let device_pkey = signer.get_public_key().await?;
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(device_pkey);
let id = SubscriptionId::new(DEVICE_GIFTWRAP);
// Construct target for subscription
let target = urls
.iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>();
// Construct target for subscription
let target = urls
.iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>();
if let Err(e) = client.subscribe(target).with_id(id).await {
log::error!("Failed to subscribe to gift wrap events: {e}");
}
}
client.subscribe(target).with_id(id).await?;
}
// Get messages with user key
@@ -300,11 +303,12 @@ impl DeviceRegistry {
.map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>();
if let Err(e) = client.subscribe(target).with_id(id).await {
log::error!("Failed to subscribe to gift wrap events: {e}");
}
})
.detach();
client.subscribe(target).with_id(id).await?;
Ok(())
});
task.detach();
}
/// Get device announcement for current user
@@ -312,11 +316,9 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let task: Task<Result<Event, Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
// Construct the filter for the device announcement event
let filter = Filter::new()
@@ -324,14 +326,8 @@ impl DeviceRegistry {
.author(public_key)
.limit(1);
// Construct target for subscription
let target = urls
.iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>();
let mut stream = client
.stream_events(target)
.stream_events(filter)
.timeout(Duration::from_secs(TIMEOUT))
.await?;
@@ -373,16 +369,12 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
// Generate a new device keys
let keys = Keys::generate();
let secret = keys.secret_key().to_secret_hex();
let n = keys.public_key();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
// Construct an announcement event
let builder = EventBuilder::new(Kind::Custom(10044), "").tags(vec![
Tag::custom(TagKind::custom("n"), vec![n]),
@@ -391,7 +383,7 @@ impl DeviceRegistry {
let event = client.sign_event_builder(builder).await?;
// Publish announcement
client.send_event(&event).to(urls).await?;
client.send_event(&event).to_nip65().await?;
// Save device keys to the database
Self::set_keys(&client, &secret).await?;
@@ -459,11 +451,9 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
// Construct a filter for device key requests
let filter = Filter::new()
@@ -471,14 +461,8 @@ impl DeviceRegistry {
.author(public_key)
.since(Timestamp::now());
// Construct target for subscription
let target = urls
.iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>();
// Subscribe to the device key requests on user's write relays
client.subscribe(target).await?;
client.subscribe(filter).await?;
Ok(())
});
@@ -491,11 +475,9 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
// Construct a filter for device key requests
let filter = Filter::new()
@@ -503,14 +485,8 @@ impl DeviceRegistry {
.author(public_key)
.since(Timestamp::now());
// Construct target for subscription
let target = urls
.iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>();
// Subscribe to the device key requests on user's write relays
client.subscribe(target).await?;
client.subscribe(filter).await?;
Ok(())
});
@@ -523,9 +499,6 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let app_keys = nostr.read(cx).app_keys().clone();
let app_pubkey = app_keys.public_key();
@@ -557,8 +530,6 @@ impl DeviceRegistry {
Ok(Some(keys))
}
None => {
let urls = write_relays.await;
// Construct an event for device key request
let builder = EventBuilder::new(Kind::Custom(4454), "").tags(vec![
Tag::client(app_name()),
@@ -567,7 +538,7 @@ impl DeviceRegistry {
let event = client.sign_event_builder(builder).await?;
// Send the event to write relays
client.send_event(&event).to(urls).await?;
client.send_event(&event).to_nip65().await?;
Ok(None)
}
@@ -640,11 +611,7 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
let signer = client.signer().context("Signer not found")?;
// Get device keys
@@ -673,7 +640,7 @@ impl DeviceRegistry {
let event = client.sign_event_builder(builder).await?;
// Send the response event to the user's relay list
client.send_event(&event).to(urls).await?;
client.send_event(&event).to_nip65().await?;
Ok(())
});

View File

@@ -138,16 +138,36 @@ impl RelayAuth {
let mut notifications = client.notifications();
while let Some(notification) = notifications.next().await {
if let ClientNotification::Message {
message: RelayMessage::Auth { challenge },
relay_url,
} = notification
{
let request = AuthRequest::new(challenge, relay_url);
match notification {
ClientNotification::Message { relay_url, message } => {
match message {
RelayMessage::Auth { challenge } => {
let request = AuthRequest::new(challenge, relay_url);
if let Err(e) = tx.send_async(request).await {
log::error!("Failed to send auth request: {}", e);
if let Err(e) = tx.send_async(request).await {
log::error!("Failed to send auth request: {}", e);
}
}
RelayMessage::Ok {
event_id, message, ..
} => {
let msg = MachineReadablePrefix::parse(&message);
let mut tracker = tracker().write().await;
// Handle authentication messages
if let Some(MachineReadablePrefix::AuthRequired) = msg {
// Keep track of events that need to be resent after authentication
tracker.add_to_pending(event_id, relay_url);
} else {
// Keep track of events sent by Coop
tracker.sent(event_id)
}
}
_ => {}
}
}
ClientNotification::Shutdown => break,
_ => {}
}
}
}

View File

@@ -1,6 +1,6 @@
use std::collections::{HashMap, HashSet};
use anyhow::{anyhow, Error};
use anyhow::{anyhow, Context as AnyhowContext, Error};
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
@@ -121,7 +121,7 @@ pub struct AppSettings {
_subscriptions: SmallVec<[Subscription; 1]>,
/// Background tasks
_tasks: SmallVec<[Task<()>; 1]>,
tasks: SmallVec<[Task<()>; 1]>,
}
impl AppSettings {
@@ -163,8 +163,8 @@ impl AppSettings {
Self {
values: Settings::default(),
tasks,
_subscriptions: subscriptions,
_tasks: tasks,
}
}
@@ -172,7 +172,6 @@ impl AppSettings {
fn get_from_database(cx: &App) -> Task<Result<Settings, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let current_user = nostr.read(cx).identity().read(cx).public_key;
cx.background_spawn(async move {
// Construct a filter to get the latest settings
@@ -181,9 +180,12 @@ impl AppSettings {
.identifier(SETTINGS_IDENTIFIER)
.limit(1);
if let Some(public_key) = current_user {
// Push author to the filter
filter = filter.author(public_key);
// If the signer is available, get settings belonging to the current user
if let Some(signer) = client.signer() {
if let Ok(public_key) = signer.get_public_key().await {
// Push author to the filter
filter = filter.author(public_key);
}
}
if let Some(event) = client.database().query(filter).await?.first_owned() {
@@ -198,7 +200,7 @@ impl AppSettings {
pub fn load(&mut self, cx: &mut Context<Self>) {
let task = Self::get_from_database(cx);
self._tasks.push(
self.tasks.push(
// Run task in the background
cx.spawn(async move |this, cx| {
if let Ok(settings) = task.await {
@@ -216,10 +218,12 @@ impl AppSettings {
pub fn save(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
if let Ok(content) = serde_json::to_string(&self.values) {
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
.tag(Tag::identifier(SETTINGS_IDENTIFIER))
.build(public_key)

View File

@@ -10,7 +10,7 @@ common = { path = "../common" }
nostr-sdk.workspace = true
nostr-lmdb.workspace = true
nostr-connect.workspace = true
nostr-gossip-memory.workspace = true
nostr-gossip-sqlite.workspace = true
gpui.workspace = true
gpui_tokio.workspace = true

View File

@@ -1,101 +0,0 @@
use nostr_sdk::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RelayState {
#[default]
Initial,
NotSet,
Set,
}
impl RelayState {
pub fn is_initial(&self) -> bool {
matches!(self, RelayState::Initial)
}
}
/// Identity
#[derive(Debug, Default)]
pub struct Identity {
/// Signer's public key
pub public_key: Option<PublicKey>,
/// Whether the identity is owned by the user
pub owned: bool,
/// Status of the current user NIP-65 relays
relay_list: RelayState,
/// Status of the current user NIP-17 relays
messaging_relays: RelayState,
}
impl AsRef<Identity> for Identity {
fn as_ref(&self) -> &Identity {
self
}
}
impl Identity {
pub fn new() -> Self {
Self {
public_key: None,
owned: true,
relay_list: RelayState::default(),
messaging_relays: RelayState::default(),
}
}
/// Resets the relay states to their default values.
pub fn reset_relay_state(&mut self) {
self.relay_list = RelayState::default();
self.messaging_relays = RelayState::default();
}
/// Sets the state of the NIP-65 relays.
pub fn set_relay_list_state(&mut self, state: RelayState) {
self.relay_list = state;
}
/// Returns the state of the NIP-65 relays.
pub fn relay_list_state(&self) -> RelayState {
self.relay_list
}
/// Sets the state of the NIP-17 relays.
pub fn set_messaging_relays_state(&mut self, state: RelayState) {
self.messaging_relays = state;
}
/// Returns the state of the NIP-17 relays.
pub fn messaging_relays_state(&self) -> RelayState {
self.messaging_relays
}
/// Force getting the public key of the identity.
///
/// Panics if the public key is not set.
pub fn public_key(&self) -> PublicKey {
self.public_key.unwrap()
}
/// Returns true if the identity has a public key.
pub fn has_public_key(&self) -> bool {
self.public_key.is_some()
}
/// Sets the public key of the identity.
pub fn set_public_key(&mut self, public_key: PublicKey) {
self.public_key = Some(public_key);
}
/// Unsets the public key of the identity.
pub fn unset_public_key(&mut self) {
self.public_key = None;
}
/// Sets whether the identity is owned by the user.
pub fn set_owned(&mut self, owned: bool) {
self.owned = owned;
}
}

View File

@@ -1,31 +1,29 @@
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::os::unix::fs::PermissionsExt;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Error};
use anyhow::{anyhow, Context as AnyhowContext, Error};
use common::{config_dir, CLIENT_NAME};
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
use gpui_tokio::Tokio;
use nostr_connect::prelude::*;
use nostr_gossip_sqlite::prelude::*;
use nostr_lmdb::NostrLmdb;
use nostr_sdk::prelude::*;
mod device;
mod event;
mod gossip;
mod identity;
mod nip05;
mod signer;
pub use device::*;
pub use event::*;
pub use gossip::*;
pub use identity::*;
pub use nip05::*;
pub use signer::*;
use crate::identity::Identity;
/// Default timeout for subscription
pub const TIMEOUT: u64 = 3;
/// Default delay for searching
@@ -55,9 +53,19 @@ pub const BOOTSTRAP_RELAYS: [&str; 4] = [
];
pub fn init(cx: &mut App) {
// rustls uses the `aws_lc_rs` provider by default
// This only errors if the default provider has already
// been installed. We can ignore this `Result`.
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.ok();
// Initialize the tokio runtime
gpui_tokio::init(cx);
// Initialize the event tracker
let _tracker = tracker();
NostrRegistry::set_global(cx.new(NostrRegistry::new), cx);
}
@@ -74,23 +82,26 @@ pub struct NostrRegistry {
/// Nostr signer
signer: Arc<CoopSigner>,
/// By default, Coop generates a new signer for new users.
///
/// This flag indicates whether the signer is user-owned or Coop-generated.
owned_signer: bool,
/// NIP-65 relay state
nip65: Entity<RelayState>,
/// NIP-17 relay state
nip17: Entity<RelayState>,
/// App keys
///
/// Used for Nostr Connect and NIP-4e operations
app_keys: Keys,
/// Current identity (user's public key)
///
/// Set by the current Nostr signer
identity: Entity<Identity>,
/// Gossip implementation
gossip: Entity<Gossip>,
/// Tasks for asynchronous operations
tasks: Vec<Task<Result<(), Error>>>,
/// Subscriptions
/// Event subscriptions
_subscriptions: Vec<Subscription>,
}
@@ -107,27 +118,39 @@ impl NostrRegistry {
/// Create a new nostr instance
fn new(cx: &mut Context<Self>) -> Self {
// rustls uses the `aws_lc_rs` provider by default
// This only errors if the default provider has already
// been installed. We can ignore this `Result`.
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.ok();
// Construct the lmdb
// Construct the nostr lmdb instance
let lmdb = cx.foreground_executor().block_on(async move {
NostrLmdb::open(config_dir().join("nostr"))
.await
.expect("Failed to initialize database")
});
// Use tokio to spawn a task to build the gossip instance
let build_gossip_sqlite = Tokio::spawn(cx, async move {
NostrGossipSqlite::open(config_dir().join("gossip"))
.await
.expect("Failed to initialize gossip")
});
// Initialize the nostr gossip instance
let gossip = cx.foreground_executor().block_on(async move {
build_gossip_sqlite
.await
.expect("Failed to initialize gossip")
});
// Construct the nostr signer
let keys = Keys::generate();
let signer = Arc::new(CoopSigner::new(keys));
let app_keys = Self::create_or_init_app_keys().unwrap_or(Keys::generate());
let signer = Arc::new(CoopSigner::new(app_keys.clone()));
// Construct the relay states entity
let nip65 = cx.new(|_| RelayState::default());
let nip17 = cx.new(|_| RelayState::default());
// Construct the nostr client
let client = ClientBuilder::default()
.signer(signer.clone())
.gossip(gossip)
.database(lmdb)
.automatic_authentication(false)
.verify_subscriptions(false)
@@ -136,83 +159,23 @@ impl NostrRegistry {
})
.build();
// Construct the event tracker
let _tracker = tracker();
// Get the app keys
let app_keys = Self::create_or_init_app_keys().unwrap();
// Construct the gossip entity
let gossip = cx.new(|_| Gossip::default());
let async_gossip = gossip.downgrade();
// Construct the identity entity
let identity = cx.new(|_| Identity::new());
// Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Event>(2048);
let mut subscriptions = vec![];
let mut tasks = vec![];
subscriptions.push(
// Observe the identity entity
cx.observe(&identity, |this, state, cx| {
if state.read(cx).has_public_key() {
match state.read(cx).relay_list_state() {
RelayState::Initial => {
this.get_relay_list(cx);
}
RelayState::Set => {
if state.read(cx).messaging_relays_state() == RelayState::Initial {
this.get_profile(cx);
this.get_messaging_relays(cx);
};
}
_ => {}
}
// Observe the NIP-65 state
cx.observe(&nip65, |this, state, cx| {
if state.read(cx).configured() {
this.get_profile(cx);
this.get_messaging_relays(cx);
}
}),
);
tasks.push(
// Handle nostr notifications
cx.background_spawn({
let client = client.clone();
async move { Self::handle_notifications(&client, &tx).await }
}),
);
tasks.push(
// Update GPUI states
cx.spawn(async move |_this, cx| {
while let Ok(event) = rx.recv_async().await {
match event.kind {
Kind::RelayList => {
async_gossip.update(cx, |this, cx| {
this.insert_relays(&event);
cx.notify();
})?;
}
Kind::InboxRelays => {
async_gossip.update(cx, |this, cx| {
this.insert_messaging_relays(&event);
cx.notify();
})?;
}
_ => {}
}
}
Ok(())
}),
);
cx.defer(|cx| {
let nostr = NostrRegistry::global(cx);
nostr.update(cx, |this, cx| {
this.connect(cx);
this.get_identity(cx);
});
});
@@ -220,89 +183,188 @@ impl NostrRegistry {
Self {
client,
signer,
owned_signer: false,
nip65,
nip17,
app_keys,
identity,
gossip,
tasks: vec![],
_subscriptions: subscriptions,
tasks,
}
}
/// Handle nostr notifications
async fn handle_notifications(client: &Client, tx: &flume::Sender<Event>) -> Result<(), Error> {
// Add bootstrap relay to the relay pool
for url in BOOTSTRAP_RELAYS.into_iter() {
client.add_relay(url).await?;
}
/// Connect to all bootstrap relays
fn connect(&mut self, cx: &mut Context<Self>) {
let client = self.client();
// Add search relay to the relay pool
for url in SEARCH_RELAYS.into_iter() {
client.add_relay(url).await?;
}
self.tasks.push(cx.background_spawn(async move {
// Add bootstrap relay to the relay pool
for url in BOOTSTRAP_RELAYS.into_iter() {
client.add_relay(url).await?;
}
// Add wot relay to the relay pool
for url in WOT_RELAYS.into_iter() {
client.add_relay(url).await?;
}
// Add search relay to the relay pool
for url in SEARCH_RELAYS.into_iter() {
client.add_relay(url).await?;
}
// Connect to all added relays
client.connect().await;
// Add wot relay to the relay pool
for url in WOT_RELAYS.into_iter() {
client.add_relay(url).await?;
}
// Handle nostr notifications
let mut notifications = client.notifications();
let mut processed_events = HashSet::new();
// Connect to all added relays
client.connect().await;
while let Some(notification) = notifications.next().await {
if let ClientNotification::Message { message, relay_url } = notification {
match message {
RelayMessage::Event {
event,
subscription_id,
} => {
if !processed_events.insert(event.id) {
// Skip if the event has already been processed
continue;
}
Ok(())
}));
}
match event.kind {
Kind::RelayList => {
// Automatically get messaging relays for each member when the user opens a room
if subscription_id.as_str().starts_with("room-") {
Self::get_adv_events_by(client, event.as_ref()).await?;
}
/// Get the nostr client
pub fn client(&self) -> Client {
self.client.clone()
}
tx.send_async(event.into_owned()).await?;
}
Kind::InboxRelays => {
tx.send_async(event.into_owned()).await?;
}
_ => {}
}
}
RelayMessage::Ok {
event_id, message, ..
} => {
let msg = MachineReadablePrefix::parse(&message);
let mut tracker = tracker().write().await;
/// Get the app keys
pub fn app_keys(&self) -> &Keys {
&self.app_keys
}
// Handle authentication messages
if let Some(MachineReadablePrefix::AuthRequired) = msg {
// Keep track of events that need to be resent after authentication
tracker.add_to_pending(event_id, relay_url);
} else {
// Keep track of events sent by Coop
tracker.sent(event_id)
}
}
_ => {}
/// Returns whether the current signer is owned by user
pub fn owned_signer(&self) -> bool {
self.owned_signer
}
/// Set whether the current signer is owned by user
pub fn set_owned_signer(&mut self, owned: bool, cx: &mut Context<Self>) {
self.owned_signer = owned;
cx.notify();
}
/// Get the NIP-65 state
pub fn nip65_state(&self) -> Entity<RelayState> {
self.nip65.clone()
}
/// Get the NIP-17 state
pub fn nip17_state(&self) -> Entity<RelayState> {
self.nip17.clone()
}
/// Get current signer's public key
pub fn signer_pkey(&self, cx: &App) -> Task<Result<PublicKey, Error>> {
let client = self.client();
cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
Ok(public_key)
})
}
/// Get a relay hint (messaging relay) for a given public key
///
/// Used for building chat messages
pub fn relay_hint(&self, public_key: &PublicKey, cx: &App) -> Task<Option<RelayUrl>> {
let client = self.client();
let public_key = public_key.to_owned();
cx.background_spawn(async move {
let filter = Filter::new()
.author(public_key)
.kind(Kind::InboxRelays)
.limit(1);
if let Ok(events) = client.database().query(filter).await {
if let Some(event) = events.first_owned() {
let relays: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
return relays.first().cloned();
}
}
}
Ok(())
None
})
}
/// Automatically get messaging relays and encryption announcement from a received relay list
/// Get a list of messaging relays with current signer's public key
pub fn messaging_relays(&self, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
cx.background_spawn(async move {
let Ok(signer) = client.signer().context("Signer not found") else {
return vec![];
};
let Ok(public_key) = signer.get_public_key().await else {
return vec![];
};
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
client
.database()
.query(filter)
.await
.ok()
.and_then(|events| events.first_owned())
.map(|event| nip17::extract_owned_relay_list(event).collect())
.unwrap_or_default()
})
}
/// Reset all relay states
pub fn reset_relay_states(&mut self, cx: &mut Context<Self>) {
self.nip65.update(cx, |this, cx| {
*this = RelayState::default();
cx.notify();
});
self.nip17.update(cx, |this, cx| {
*this = RelayState::default();
cx.notify();
});
}
/// Set the signer for the nostr client and verify the public key
pub fn set_signer<T>(&mut self, new: T, owned: bool, cx: &mut Context<Self>)
where
T: NostrSigner + 'static,
{
let client = self.client();
let signer = self.signer.clone();
// Create a task to update the signer and verify the public key
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
// Update signer
signer.switch(new).await;
// Verify signer
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
log::info!("Signer's public key: {public_key}");
Ok(())
});
self.tasks.push(cx.spawn(async move |this, cx| {
// set signer
task.await?;
// Update states
this.update(cx, |this, cx| {
this.reset_relay_states(cx);
this.get_relay_list(cx);
this.set_owned_signer(owned, cx);
})?;
Ok(())
}));
}
/*
async fn get_adv_events_by(client: &Client, event: &Event) -> Result<(), Error> {
// Subscription options
let opts = SubscribeAutoCloseOptions::default()
@@ -348,6 +410,7 @@ impl NostrRegistry {
Ok(())
}
*/
/// Get or create a new app keys
fn create_or_init_app_keys() -> Result<Keys, Error> {
@@ -377,124 +440,15 @@ impl NostrRegistry {
Ok(keys)
}
/// Get the nostr client
pub fn client(&self) -> Client {
self.client.clone()
}
/// Get the app keys
pub fn app_keys(&self) -> &Keys {
&self.app_keys
}
/// Get current identity
pub fn identity(&self) -> Entity<Identity> {
self.identity.clone()
}
/// Get a relay hint (messaging relay) for a given public key
pub fn relay_hint(&self, public_key: &PublicKey, cx: &App) -> Option<RelayUrl> {
self.gossip
.read(cx)
.messaging_relays(public_key)
.first()
.cloned()
}
/// Get a list of write relays for a given public key
pub fn write_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
let relays = self.gossip.read(cx).write_relays(public_key);
cx.background_spawn(async move {
// Ensure relay connections
for url in relays.iter() {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
relays
})
}
/// Get a list of read relays for a given public key
pub fn read_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
let relays = self.gossip.read(cx).read_relays(public_key);
cx.background_spawn(async move {
// Ensure relay connections
for url in relays.iter() {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
relays
})
}
/// Get a list of messaging relays for a given public key
pub fn messaging_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
let relays = self.gossip.read(cx).messaging_relays(public_key);
cx.background_spawn(async move {
// Ensure relay connections
for url in relays.iter() {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
relays
})
}
/// Set the signer for the nostr client and verify the public key
pub fn set_signer<T>(&mut self, new: T, owned: bool, cx: &mut Context<Self>)
where
T: NostrSigner + 'static,
{
let identity = self.identity.downgrade();
let signer = self.signer.clone();
// Create a task to update the signer and verify the public key
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
// Update signer
signer.switch(new).await;
// Verify signer
let public_key = signer.get_public_key().await?;
log::info!("test: {public_key:?}");
Ok(public_key)
});
self.tasks.push(cx.spawn(async move |_this, cx| {
match task.await {
Ok(public_key) => {
identity.update(cx, |this, cx| {
this.set_public_key(public_key);
this.reset_relay_state();
this.set_owned(owned);
cx.notify();
})?;
}
Err(e) => {
log::error!("Failed to set signer: {e}");
}
};
Ok(())
}));
}
// Get relay list for current user
fn get_relay_list(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let async_identity = self.identity.downgrade();
let public_key = self.identity().read(cx).public_key();
let nip65 = self.nip65.downgrade();
let task: Task<Result<RelayState, Error>> = cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::RelayList)
.author(public_key)
@@ -531,7 +485,7 @@ impl NostrRegistry {
// Subscribe to the relay list events
client.subscribe(target).await?;
return Ok(RelayState::Set);
return Ok(RelayState::Configured);
}
Err(e) => {
log::error!("Failed to receive relay list event: {e}");
@@ -539,15 +493,15 @@ impl NostrRegistry {
}
}
Ok(RelayState::NotSet)
Ok(RelayState::NotConfigured)
});
self.tasks.push(cx.spawn(async move |_this, cx| {
match task.await {
Ok(state) => {
async_identity
Ok(new_state) => {
nip65
.update(cx, |this, cx| {
this.set_relay_list_state(state);
*this = new_state;
cx.notify();
})
.ok();
@@ -561,19 +515,78 @@ impl NostrRegistry {
}));
}
/// Get messaging relays for current user
fn get_messaging_relays(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let nip17 = self.nip17.downgrade();
let task: Task<Result<RelayState, Error>> = cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
// Construct the filter for inbox relays
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
// Stream events from the write relays
let mut stream = client
.stream_events(filter)
.timeout(Duration::from_secs(TIMEOUT))
.await?;
while let Some((_url, res)) = stream.next().await {
match res {
Ok(event) => {
log::info!("Received messaging relays event: {event:?}");
// Construct a filter to continuously receive relay list events
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.since(Timestamp::now());
// Subscribe to the relay list events
client.subscribe(filter).await?;
return Ok(RelayState::Configured);
}
Err(e) => {
log::error!("Failed to get messaging relays: {e}");
}
}
}
Ok(RelayState::NotConfigured)
});
self.tasks.push(cx.spawn(async move |_this, cx| {
match task.await {
Ok(new_state) => {
nip17
.update(cx, |this, cx| {
*this = new_state;
cx.notify();
})
.ok();
}
Err(e) => {
log::error!("Failed to get messaging relays: {e}");
}
}
Ok(())
}));
}
/// Get profile and contact list for current user
fn get_profile(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let public_key = self.identity().read(cx).public_key();
let write_relays = self.write_relays(&public_key, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let mut urls = write_relays.await;
urls.extend(
BOOTSTRAP_RELAYS
.iter()
.filter_map(|url| RelayUrl::parse(url).ok()),
);
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
// Construct subscription options
let opts = SubscribeAutoCloseOptions::default()
@@ -592,13 +605,10 @@ impl NostrRegistry {
.limit(1)
.author(public_key);
// Construct targets for subscription
let target = urls
.into_iter()
.map(|relay| (relay, vec![metadata.clone(), contact_list.clone()]))
.collect::<HashMap<_, _>>();
client.subscribe(target).close_on(opts).await?;
client
.subscribe(vec![metadata, contact_list])
.close_on(opts)
.await?;
Ok(())
});
@@ -606,90 +616,14 @@ impl NostrRegistry {
task.detach();
}
/// Get messaging relays for current user
fn get_messaging_relays(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let async_identity = self.identity.downgrade();
let public_key = self.identity().read(cx).public_key();
let write_relays = self.write_relays(&public_key, cx);
let task: Task<Result<RelayState, Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
// Construct the filter for inbox relays
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
// Construct targets for subscription
let target = urls
.iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>();
// Stream events from the write relays
let mut stream = client
.stream_events(target)
.timeout(Duration::from_secs(TIMEOUT))
.await?;
while let Some((_url, res)) = stream.next().await {
match res {
Ok(event) => {
log::info!("Received messaging relays event: {event:?}");
// Construct a filter to continuously receive relay list events
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.since(Timestamp::now());
// Construct targets for subscription
let target = urls
.iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>();
// Subscribe to the relay list events
client.subscribe(target).await?;
return Ok(RelayState::Set);
}
Err(e) => {
log::error!("Failed to get messaging relays: {e}");
}
}
}
Ok(RelayState::NotSet)
});
self.tasks.push(cx.spawn(async move |_this, cx| {
match task.await {
Ok(state) => {
async_identity
.update(cx, |this, cx| {
this.set_messaging_relays_state(state);
cx.notify();
})
.ok();
}
Err(e) => {
log::error!("Failed to get messaging relays: {e}");
}
}
Ok(())
}));
}
/// Get contact list for the current user
pub fn get_contact_list(&self, cx: &App) -> Task<Result<Vec<PublicKey>, Error>> {
let client = self.client();
let public_key = self.identity().read(cx).public_key();
cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
let contacts = client.database().contacts_public_keys(public_key).await?;
let results = contacts.into_iter().collect();
@@ -700,19 +634,15 @@ impl NostrRegistry {
/// Set the metadata for the current user
pub fn set_metadata(&self, metadata: &Metadata, cx: &App) -> Task<Result<(), Error>> {
let client = self.client();
let public_key = self.identity().read(cx).public_key();
let write_relays = self.write_relays(&public_key, cx);
let metadata = metadata.clone();
cx.background_spawn(async move {
let urls = write_relays.await;
// Build and sign the metadata event
let builder = EventBuilder::metadata(&metadata);
let event = client.sign_event_builder(builder).await?;
// Send event to user's write relayss
client.send_event(&event).to(urls).await?;
client.send_event(&event).to_nip65().await?;
Ok(())
})
@@ -1007,6 +937,29 @@ impl NostrRegistry {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RelayState {
#[default]
Idle,
Checking,
NotConfigured,
Configured,
}
impl RelayState {
pub fn idle(&self) -> bool {
matches!(self, RelayState::Idle)
}
pub fn not_configured(&self) -> bool {
matches!(self, RelayState::NotConfigured)
}
pub fn configured(&self) -> bool {
matches!(self, RelayState::Configured)
}
}
#[derive(Debug, Clone)]
pub struct CoopAuthUrlHandler;

View File

@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::result::Result;
use std::sync::Arc;
use nostr_sdk::prelude::*;
@@ -19,10 +20,12 @@ impl CoopSigner {
}
}
async fn get(&self) -> Arc<dyn NostrSigner> {
/// Get the current signer.
pub async fn get(&self) -> Arc<dyn NostrSigner> {
self.signer.read().await.clone()
}
/// Switch the current signer to a new signer.
pub async fn switch<T>(&self, new: T)
where
T: IntoNostrSigner,
@@ -33,40 +36,40 @@ impl CoopSigner {
}
impl NostrSigner for CoopSigner {
#[allow(mismatched_lifetime_syntaxes)]
fn backend(&self) -> SignerBackend {
SignerBackend::Custom(Cow::Borrowed("custom"))
}
fn get_public_key(&self) -> BoxedFuture<Result<PublicKey, SignerError>> {
Box::pin(async move { Ok(self.get().await.get_public_key().await?) })
fn get_public_key<'a>(&'a self) -> BoxedFuture<'a, Result<PublicKey, SignerError>> {
Box::pin(async move { self.get().await.get_public_key().await })
}
fn sign_event(
&self,
fn sign_event<'a>(
&'a self,
unsigned: UnsignedEvent,
) -> BoxedFuture<std::result::Result<Event, SignerError>> {
Box::pin(async move { Ok(self.get().await.sign_event(unsigned).await?) })
) -> BoxedFuture<'a, Result<Event, SignerError>> {
Box::pin(async move { self.get().await.sign_event(unsigned).await })
}
fn nip04_encrypt<'a>(
&'a self,
public_key: &'a PublicKey,
content: &'a str,
) -> BoxedFuture<'a, std::result::Result<String, SignerError>> {
Box::pin(async move { Ok(self.get().await.nip04_encrypt(public_key, content).await?) })
) -> BoxedFuture<'a, Result<String, SignerError>> {
Box::pin(async move { self.get().await.nip04_encrypt(public_key, content).await })
}
fn nip04_decrypt<'a>(
&'a self,
public_key: &'a PublicKey,
encrypted_content: &'a str,
) -> BoxedFuture<'a, std::result::Result<String, SignerError>> {
) -> BoxedFuture<'a, Result<String, SignerError>> {
Box::pin(async move {
Ok(self
.get()
self.get()
.await
.nip04_decrypt(public_key, encrypted_content)
.await?)
.await
})
}
@@ -74,15 +77,15 @@ impl NostrSigner for CoopSigner {
&'a self,
public_key: &'a PublicKey,
content: &'a str,
) -> BoxedFuture<'a, std::result::Result<String, SignerError>> {
Box::pin(async move { Ok(self.get().await.nip44_encrypt(public_key, content).await?) })
) -> BoxedFuture<'a, Result<String, SignerError>> {
Box::pin(async move { self.get().await.nip44_encrypt(public_key, content).await })
}
fn nip44_decrypt<'a>(
&'a self,
public_key: &'a PublicKey,
payload: &'a str,
) -> BoxedFuture<'a, std::result::Result<String, SignerError>> {
Box::pin(async move { Ok(self.get().await.nip44_decrypt(public_key, payload).await?) })
) -> BoxedFuture<'a, Result<String, SignerError>> {
Box::pin(async move { self.get().await.nip44_decrypt(public_key, payload).await })
}
}