From d87bcfbd65d3bd278bbeecae03e4e34cbe4173bd Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 11 Nov 2025 11:02:37 +0700 Subject: [PATCH] chore: follow up on #203 --- crates/chat/src/room.rs | 43 ++++++++++---- crates/chat_ui/src/lib.rs | 18 +++--- crates/encryption/src/lib.rs | 59 ++---------------- crates/person/src/lib.rs | 1 + crates/relay_auth/src/lib.rs | 3 +- crates/settings/src/lib.rs | 112 +++++++++++++++++++++-------------- crates/state/src/lib.rs | 50 +++++++++++++++- 7 files changed, 164 insertions(+), 122 deletions(-) diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index 11f0c1d1..ecb5a0b7 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -391,6 +391,10 @@ impl Room { /// Create a new message event (unsigned) pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent { + let nostr = NostrRegistry::global(cx); + let cache = nostr.read(cx).cache_manager(); + let cache = cache.read_blocking(); + // Get current user let account = Account::global(cx); let public_key = account.read(cx).public_key(); @@ -405,7 +409,9 @@ impl Room { // NOTE: current user will be removed from the list of receivers for member in self.members.iter() { // Get relay hint if available - let relay_url = None; + let relay_url = cache + .relay(member) + .and_then(|urls| urls.iter().nth(0).cloned()); // Construct a public key tag with relay hint let tag = TagStandard::PublicKey { @@ -464,7 +470,7 @@ impl Room { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let cache_manager = nostr.read(cx).cache_manager(); + let cache = nostr.read(cx).cache_manager(); let tracker = nostr.read(cx).tracker(); let rumor = rumor.to_owned(); @@ -475,7 +481,7 @@ impl Room { cx.background_spawn(async move { let signer_kind = opts.signer_kind; - let cache = cache_manager.read().await; + let cache = cache.read().await; let tracker = tracker.read().await; // Get the encryption public key @@ -508,6 +514,12 @@ impl Room { continue; } + // Ensure connection to all messaging relays + for url in urls.iter() { + client.add_relay(url).await?; + client.connect_relay(url).await?; + } + // Get user's encryption public key if available let encryption = cache .announcement(&member) @@ -589,15 +601,22 @@ impl Room { // Check if there are any relays to send the event to if urls.is_empty() { reports.push(SendReport::new(user_pubkey).relays_not_found()); - } else { - // Send the event to the messaging relays - match client.send_event_to(urls, &event).await { - Ok(output) => { - reports.push(SendReport::new(user_pubkey).status(output)); - } - Err(e) => { - reports.push(SendReport::new(user_pubkey).error(e.to_string())); - } + return Ok(reports); + } + + // Ensure connection to all messaging relays + for url in urls.iter() { + client.add_relay(url).await?; + client.connect_relay(url).await?; + } + + // Send the event to the messaging relays + match client.send_event_to(urls, &event).await { + Ok(output) => { + reports.push(SendReport::new(user_pubkey).status(output)); + } + Err(e) => { + reports.push(SendReport::new(user_pubkey).error(e.to_string())); } } } else { diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 25e4374b..e1486a59 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -100,6 +100,15 @@ impl ChatPanel { let mut subscriptions = smallvec![]; let mut tasks = smallvec![]; + tasks.push( + // Get messaging relays and encryption keys announcement for each member + cx.background_spawn(async move { + if let Err(e) = connect.await { + log::error!("Failed to initialize room: {}", e); + } + }), + ); + tasks.push( // Load all messages belonging to this room cx.spawn_in(window, async move |this, cx| { @@ -119,15 +128,6 @@ impl ChatPanel { }), ); - tasks.push( - // Get messaging relays and encryption keys announcement for each member - cx.background_spawn(async move { - if let Err(e) = connect.await { - log::error!("Failed to initialize room: {}", e); - } - }), - ); - subscriptions.push( // Subscribe to input events cx.subscribe_in( diff --git a/crates/encryption/src/lib.rs b/crates/encryption/src/lib.rs index 52694aa6..bf231444 100644 --- a/crates/encryption/src/lib.rs +++ b/crates/encryption/src/lib.rs @@ -9,7 +9,7 @@ use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use nostr_sdk::prelude::*; pub use signer::*; use smallvec::{smallvec, SmallVec}; -use state::{Announcement, NostrRegistry, Response}; +use state::{Announcement, NostrRegistry}; mod signer; @@ -211,7 +211,7 @@ impl Encryption { .limit(1); if let Some(event) = client.database().query(filter).await?.first() { - Ok(Self::extract_announcement(event)?) + Ok(NostrRegistry::extract_announcement(event)?) } else { Err(anyhow!("Announcement not found")) } @@ -299,8 +299,8 @@ impl Encryption { continue; }; - if Self::is_self_authored(&client, &event).await { - if let Ok(announcement) = Self::extract_announcement(&event) { + if NostrRegistry::is_self_authored(&client, &event).await { + if let Ok(announcement) = NostrRegistry::extract_announcement(&event) { tx.send_async(announcement).await.ok(); } } @@ -517,7 +517,7 @@ impl Encryption { continue; } - if let Ok(response) = Self::extract_response(&client, &event).await { + if let Ok(response) = NostrRegistry::extract_response(&client, &event).await { let public_key = response.public_key(); let payload = response.payload(); @@ -579,53 +579,4 @@ impl Encryption { cx.notify(); }); } - - /// Extract an encryption keys announcement from an event. - fn extract_announcement(event: &Event) -> Result { - let public_key = event - .tags - .iter() - .find(|tag| tag.kind().as_str() == "n" || tag.kind().as_str() == "pubkey") - .and_then(|tag| tag.content()) - .and_then(|c| PublicKey::parse(c).ok()) - .context("Cannot parse public key from the event's tags")?; - - let client_name = event - .tags - .find(TagKind::Client) - .and_then(|tag| tag.content()) - .map(|c| c.to_string()) - .context("Cannot parse client name from the event's tags")?; - - Ok(Announcement::new(event.id, client_name, public_key)) - } - - /// Extract an encryption keys response from an event. - async fn extract_response(client: &Client, event: &Event) -> Result { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - if event.pubkey != public_key { - return Err(anyhow!("Event does not belong to current user")); - } - - let client_pubkey = event - .tags - .find(TagKind::custom("P")) - .and_then(|tag| tag.content()) - .and_then(|c| PublicKey::parse(c).ok()) - .context("Cannot parse public key from the event's tags")?; - - Ok(Response::new(event.content.clone(), client_pubkey)) - } - - /// Check if event is published by current user - async fn is_self_authored(client: &Client, event: &Event) -> bool { - if let Ok(signer) = client.signer().await { - if let Ok(public_key) = signer.get_public_key().await { - return public_key == event.pubkey; - } - } - false - } } diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs index c430a0cf..7797f73c 100644 --- a/crates/person/src/lib.rs +++ b/crates/person/src/lib.rs @@ -45,6 +45,7 @@ impl PersonRegistry { // Handle notifications cx.spawn({ let client = Arc::clone(&client); + async move |this, cx| { let mut notifications = client.notifications(); log::info!("Listening for notifications"); diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index 8fed6233..221bb2b2 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -85,10 +85,11 @@ impl RelayAuth { subscriptions.push( // Observe the current state cx.observe_in(&entity, window, |this, _, window, cx| { + let settings = AppSettings::global(cx); let auto_auth = AppSettings::get_auto_auth(cx); for req in this.requests.clone().into_iter() { - let is_authenticated = AppSettings::read_global(cx).is_authenticated(&req.url); + let is_authenticated = settings.read(cx).is_authenticated(&req.url); if auto_auth && is_authenticated { // Automatically authenticate if the relay is authenticated before diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 67c7b933..900c1003 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -1,4 +1,4 @@ -use anyhow::anyhow; +use anyhow::{anyhow, Error}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; @@ -8,17 +8,7 @@ use state::NostrRegistry; const SETTINGS_IDENTIFIER: &str = "coop:settings"; pub fn init(cx: &mut App) { - let state = cx.new(AppSettings::new); - - // Observe for state changes and save settings to database - state.update(cx, |this, cx| { - this._subscriptions - .push(cx.observe(&state, |this, _state, cx| { - this.set_settings(cx); - })); - }); - - AppSettings::set_global(state, cx); + AppSettings::set_global(cx.new(AppSettings::new), cx) } macro_rules! setting_accessors { @@ -27,7 +17,7 @@ macro_rules! setting_accessors { $( paste::paste! { pub fn [](cx: &App) -> $type { - Self::read_global(cx).setting_values.$field.clone() + Self::global(cx).read(cx).setting_values.$field.clone() } pub fn [](value: $type, cx: &mut App) { @@ -51,10 +41,9 @@ setting_accessors! { pub contact_bypass: bool, pub auto_login: bool, pub auto_auth: bool, - pub disable_keyring: bool, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Settings { pub media_server: Url, pub proxy_user_avatars: bool, @@ -64,7 +53,6 @@ pub struct Settings { pub contact_bypass: bool, pub auto_login: bool, pub auto_auth: bool, - pub disable_keyring: bool, pub authenticated_relays: Vec, } @@ -79,7 +67,6 @@ impl Default for Settings { contact_bypass: true, auto_login: false, auto_auth: true, - disable_keyring: false, authenticated_relays: vec![], } } @@ -97,7 +84,12 @@ impl Global for GlobalAppSettings {} pub struct AppSettings { setting_values: Settings, + + // Event subscriptions _subscriptions: SmallVec<[Subscription; 1]>, + + // Background tasks + _tasks: SmallVec<[Task<()>; 1]>, } impl AppSettings { @@ -106,62 +98,91 @@ impl AppSettings { cx.global::().0.clone() } - /// Retrieve the Settings instance - pub fn read_global(cx: &App) -> &Self { - cx.global::().0.read(cx) - } - /// Set the Global Settings instance pub(crate) fn set_global(state: Entity, cx: &mut App) { cx.set_global(GlobalAppSettings(state)); } - fn new(_cx: &mut Context) -> Self { + fn new(cx: &mut Context) -> Self { + let load_settings = Self::_load_settings(false, cx); + + let mut tasks = smallvec![]; + let mut subscriptions = smallvec![]; + + subscriptions.push( + // Observe and automatically save settings on changes + cx.observe_self(|this, cx| { + this.set_settings(cx); + }), + ); + + tasks.push( + // Load the initial settings + cx.spawn(async move |this, cx| { + if let Ok(settings) = load_settings.await { + this.update(cx, |this, cx| { + this.setting_values = settings; + cx.notify(); + }) + .ok(); + } + }), + ); + Self { setting_values: Settings::default(), - _subscriptions: smallvec![], + _subscriptions: subscriptions, + _tasks: tasks, } } - pub fn load_settings(&self, cx: &mut Context) { + pub fn load_settings(&mut self, cx: &mut Context) { + let task = Self::_load_settings(true, cx); + + self._tasks.push( + // Run task in the background + cx.spawn(async move |this, cx| { + if let Ok(settings) = task.await { + this.update(cx, |this, cx| { + this.setting_values = settings; + cx.notify(); + }) + .ok(); + } + }), + ); + } + + fn _load_settings(user: bool, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let task: Task> = cx.background_spawn(async move { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - let filter = Filter::new() + cx.background_spawn(async move { + let mut filter = Filter::new() .kind(Kind::ApplicationSpecificData) .identifier(SETTINGS_IDENTIFIER) - .author(public_key) .limit(1); + if user { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + filter = filter.author(public_key); + } + if let Some(event) = client.database().query(filter).await?.first_owned() { Ok(serde_json::from_str(&event.content).unwrap_or(Settings::default())) } else { Err(anyhow!("Not found")) } - }); - - cx.spawn(async move |this, cx| { - if let Ok(settings) = task.await { - this.update(cx, |this, cx| { - this.setting_values = settings; - cx.notify(); - }) - .ok(); - } }) - .detach(); } - pub fn set_settings(&self, cx: &mut Context) { + pub fn set_settings(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); if let Ok(content) = serde_json::to_string(&self.setting_values) { - let task: Task> = cx.background_spawn(async move { + let task: Task> = cx.background_spawn(async move { let signer = client.signer().await?; let public_key = signer.get_public_key().await?; @@ -180,14 +201,17 @@ impl AppSettings { } } + /// Check if auto authentication is enabled pub fn is_auto_auth(&self) -> bool { !self.setting_values.authenticated_relays.is_empty() && self.setting_values.auto_auth } + /// Check if a relay is authenticated pub fn is_authenticated(&self, url: &RelayUrl) -> bool { self.setting_values.authenticated_relays.contains(url) } + /// Push a relay to the authenticated relays list pub fn push_relay(&mut self, relay_url: &RelayUrl, cx: &mut Context) { if !self.is_authenticated(relay_url) { self.setting_values diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 2ab832fa..d34c5ed2 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use std::sync::Arc; use std::time::Duration; -use anyhow::{anyhow, Error}; +use anyhow::{anyhow, Context as AnyhowContext, Error}; use common::{config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; use gpui::{App, AppContext, Context, Entity, Global, Task}; use nostr_gossip_memory::prelude::*; @@ -193,6 +193,13 @@ impl NostrRegistry { let mut cache = cache.write().await; cache.insert_relay(event.pubkey, urls); } + Kind::Custom(10044) => { + // Cache the announcement event + if let Ok(announcement) = Self::extract_announcement(&event) { + let mut cache = cache.write().await; + cache.insert_announcement(event.pubkey, Some(announcement)); + } + } Kind::ContactList => { if Self::is_self_authored(client, &event).await { let pubkeys: Vec<_> = event.tags.public_keys().copied().collect(); @@ -226,7 +233,7 @@ impl NostrRegistry { } /// Check if event is published by current user - async fn is_self_authored(client: &Client, event: &Event) -> bool { + pub async fn is_self_authored(client: &Client, event: &Event) -> bool { if let Ok(signer) = client.signer().await { if let Ok(public_key) = signer.get_public_key().await { return public_key == event.pubkey; @@ -295,6 +302,45 @@ impl NostrRegistry { Ok(()) } + /// Extract an encryption keys announcement from an event. + pub fn extract_announcement(event: &Event) -> Result { + let public_key = event + .tags + .iter() + .find(|tag| tag.kind().as_str() == "n" || tag.kind().as_str() == "pubkey") + .and_then(|tag| tag.content()) + .and_then(|c| PublicKey::parse(c).ok()) + .context("Cannot parse public key from the event's tags")?; + + let client_name = event + .tags + .find(TagKind::Client) + .and_then(|tag| tag.content()) + .map(|c| c.to_string()) + .context("Cannot parse client name from the event's tags")?; + + Ok(Announcement::new(event.id, client_name, public_key)) + } + + /// Extract an encryption keys response from an event. + pub async fn extract_response(client: &Client, event: &Event) -> Result { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + if event.pubkey != public_key { + return Err(anyhow!("Event does not belong to current user")); + } + + let client_pubkey = event + .tags + .find(TagKind::custom("P")) + .and_then(|tag| tag.content()) + .and_then(|c| PublicKey::parse(c).ok()) + .context("Cannot parse public key from the event's tags")?; + + Ok(Response::new(event.content.clone(), client_pubkey)) + } + /// Returns a reference to the nostr client. pub fn client(&self) -> Arc { Arc::clone(&self.client)