diff --git a/Cargo.lock b/Cargo.lock index 265352e..0bcc1fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1658,6 +1658,7 @@ dependencies = [ "itertools 0.13.0", "log", "nostr-sdk", + "person", "serde", "serde_json", "smallvec", @@ -4658,7 +4659,6 @@ version = "0.3.0" dependencies = [ "anyhow", "common", - "device", "flume", "gpui", "log", diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index f10eb8a..21d2188 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -50,11 +50,28 @@ enum Signal { Eose, } +/// Inbox state. +#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum InboxState { + #[default] + Idle, + Checking, + RelayNotAvailable, + RelayConfigured(Box), + Subscribing, +} + +impl InboxState { + pub fn not_configured(&self) -> bool { + matches!(self, InboxState::RelayNotAvailable) + } +} + /// Chat Registry #[derive(Debug)] pub struct ChatRegistry { /// Relay state for messaging relay list - messaging_relay_list: Entity, + state: Entity, /// Collection of all chat rooms rooms: Vec>, @@ -84,7 +101,7 @@ impl ChatRegistry { /// Create a new chat registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { - let messaging_relay_list = cx.new(|_| RelayState::default()); + let state = cx.new(|_| InboxState::default()); let nostr = NostrRegistry::global(cx); let mut subscriptions = smallvec![]; @@ -106,26 +123,28 @@ impl ChatRegistry { subscriptions.push( // Observe the nip17 state and load chat rooms on every state change - cx.observe(&messaging_relay_list, |this, state, cx| { - match state.read(cx) { - RelayState::Configured => { - this.get_messages(cx); - } - _ => { - this.get_rooms(cx); - } + cx.observe(&state, |this, state, cx| { + if let InboxState::RelayConfigured(event) = state.read(cx) { + let relay_urls: Vec<_> = nip17::extract_relay_list(event).cloned().collect(); + this.get_messages(relay_urls, cx); } }), ); - // Run at the end of current cycle + // Run at the end of the current cycle cx.defer_in(window, |this, _window, cx| { + // Handle nostr notifications this.handle_notifications(cx); + + // Track unwrap gift wrap progress this.tracking(cx); + + // Load chat rooms + this.get_rooms(cx); }); Self { - messaging_relay_list, + state, rooms: vec![], tracking_flag: Arc::new(AtomicBool::new(false)), tasks: smallvec![], @@ -170,8 +189,6 @@ impl ChatRegistry { continue; } - log::info!("Received gift wrap event: {:?}", event); - // Extract the rumor from the gift wrap event match Self::extract_rumor(&client, &device_signer, event.as_ref()).await { Ok(rumor) => match rumor.created_at >= initialized_at { @@ -238,17 +255,19 @@ impl ChatRegistry { })); } + /// Ensure messaging relays are set up for the current user. fn ensure_messaging_relays(&mut self, cx: &mut Context) { - let state = self.messaging_relay_list.downgrade(); let task = self.verify_relays(cx); - self.tasks.push(cx.spawn(async move |_this, cx| { + // Set state to checking + self.set_state(InboxState::Checking, cx); + + self.tasks.push(cx.spawn(async move |this, cx| { let result = task.await?; // Update state - state.update(cx, |this, cx| { - *this = result; - cx.notify(); + this.update(cx, |this, cx| { + this.set_state(result, cx); })?; Ok(()) @@ -256,13 +275,12 @@ impl ChatRegistry { } // Verify messaging relay list for current user - fn verify_relays(&mut self, cx: &mut Context) -> Task> { + fn verify_relays(&mut self, cx: &mut Context) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); let public_key = signer.public_key().unwrap(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); cx.background_spawn(async move { @@ -287,8 +305,7 @@ impl ChatRegistry { while let Some((_url, res)) = stream.next().await { match res { Ok(event) => { - log::info!("Received relay list event: {event:?}"); - return Ok(RelayState::Configured); + return Ok(InboxState::RelayConfigured(Box::new(event))); } Err(e) => { log::error!("Failed to receive relay list event: {e}"); @@ -296,41 +313,54 @@ impl ChatRegistry { } } - Ok(RelayState::NotConfigured) + Ok(InboxState::RelayNotAvailable) }) } /// Get all messages for current user - fn get_messages(&mut self, cx: &mut Context) { - let task = self.subscribe_to_giftwrap_events(cx); + fn get_messages(&mut self, relay_urls: I, cx: &mut Context) + where + I: IntoIterator, + { + let task = self.subscribe(relay_urls, cx); - self.tasks.push(cx.spawn(async move |_this, _cx| { + self.tasks.push(cx.spawn(async move |this, cx| { task.await?; // Update state + this.update(cx, |this, cx| { + this.set_state(InboxState::Subscribing, cx); + })?; Ok(()) })); } /// Continuously get gift wrap events for the current user in their messaging relays - fn subscribe_to_giftwrap_events(&mut self, cx: &mut Context) -> Task> { + fn subscribe(&mut self, urls: I, cx: &mut Context) -> Task> + where + I: IntoIterator, + { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); - let public_key = signer.public_key().unwrap(); - - let messaging_relays = nostr.read(cx).messaging_relays(&public_key, cx); + let urls = urls.into_iter().collect::>(); cx.background_spawn(async move { - let urls = messaging_relays.await; + let public_key = signer.get_public_key().await?; let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let id = SubscriptionId::new(USER_GIFTWRAP); + // Ensure relay connections + for url in urls.iter() { + client.add_relay(url).and_connect().await?; + } + // Construct target for subscription - let target: HashMap<&RelayUrl, Filter> = - urls.iter().map(|relay| (relay, filter.clone())).collect(); + let target: HashMap = urls + .into_iter() + .map(|relay| (relay, filter.clone())) + .collect(); let output = client.subscribe(target).with_id(id).await?; @@ -343,9 +373,17 @@ impl ChatRegistry { }) } + /// Set the state of the inbox + fn set_state(&mut self, state: InboxState, cx: &mut Context) { + self.state.update(cx, |this, cx| { + *this = state; + cx.notify(); + }); + } + /// Get the relay state - pub fn relay_state(&self, cx: &App) -> RelayState { - self.messaging_relay_list.read(cx).clone() + pub fn state(&self, cx: &App) -> InboxState { + self.state.read(cx).clone() } /// Get the loading status of the chat registry @@ -491,16 +529,21 @@ impl ChatRegistry { pub fn get_rooms(&mut self, cx: &mut Context) { let task = self.get_rooms_from_database(cx); - cx.spawn(async move |this, cx| { - let rooms = task.await.ok()?; + self.tasks.push(cx.spawn(async move |this, cx| { + match task.await { + Ok(rooms) => { + this.update(cx, |this, cx| { + this.extend_rooms(rooms, cx); + this.sort(cx); + })?; + } + Err(e) => { + log::error!("Failed to load rooms: {}", e); + } + }; - this.update(cx, move |this, cx| { - this.extend_rooms(rooms, cx); - this.sort(cx); - }) - .ok() - }) - .detach(); + Ok(()) + })); } /// Create a task to load rooms from the database diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 499dd82..f307595 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -1216,25 +1216,24 @@ impl ChatPanel { let encryption = matches!(signer_kind, SignerKind::Encryption); let user = matches!(signer_kind, SignerKind::User); - this.check_side(ui::Side::Right) - .menu_with_check_and_disabled( - "Auto", - auto, - Box::new(Command::ChangeSigner(SignerKind::Auto)), - auto, - ) - .menu_with_check_and_disabled( - "Decoupled Encryption Key", - encryption, - Box::new(Command::ChangeSigner(SignerKind::Encryption)), - encryption, - ) - .menu_with_check_and_disabled( - "User Identity", - user, - Box::new(Command::ChangeSigner(SignerKind::User)), - user, - ) + this.menu_with_check_and_disabled( + "Auto", + auto, + Box::new(Command::ChangeSigner(SignerKind::Auto)), + auto, + ) + .menu_with_check_and_disabled( + "Decoupled Encryption Key", + encryption, + Box::new(Command::ChangeSigner(SignerKind::Encryption)), + encryption, + ) + .menu_with_check_and_disabled( + "User Identity", + user, + Box::new(Command::ChangeSigner(SignerKind::User)), + user, + ) }) } @@ -1339,8 +1338,8 @@ impl Render for ChatPanel { h_flex() .pl_1() .gap_1() - .child(self.render_encryption_menu(window, cx)) .child(self.render_emoji_menu(window, cx)) + .child(self.render_encryption_menu(window, cx)) .child( Button::new("send") .icon(IconName::PaperPlaneFill) diff --git a/crates/coop/src/panels/greeter.rs b/crates/coop/src/panels/greeter.rs index 45c90e8..99cc7d6 100644 --- a/crates/coop/src/panels/greeter.rs +++ b/crates/coop/src/panels/greeter.rs @@ -1,4 +1,4 @@ -use chat::ChatRegistry; +use chat::{ChatRegistry, InboxState}; use gpui::prelude::FluentBuilder; use gpui::{ div, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, @@ -83,15 +83,16 @@ impl Render for GreeterPanel { const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr."; let chat = ChatRegistry::global(cx); - let nip17_state = chat.read(cx).relay_state(cx); + let nip17 = chat.read(cx).state(cx); let nostr = NostrRegistry::global(cx); - let nip65_state = nostr.read(cx).relay_list_state(); + let nip65 = nostr.read(cx).relay_list_state(); + let signer = nostr.read(cx).signer(); let owned = signer.owned(); let required_actions = - nip65_state == RelayState::NotConfigured || nip17_state == RelayState::NotConfigured; + nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable; h_flex() .size_full() @@ -152,7 +153,7 @@ impl Render for GreeterPanel { v_flex() .gap_2() .w_full() - .when(nip65_state.not_configured(), |this| { + .when(nip65.not_configured(), |this| { this.child( Button::new("relaylist") .icon(Icon::new(IconName::Relay)) @@ -170,7 +171,7 @@ impl Render for GreeterPanel { }), ) }) - .when(nip17_state.not_configured(), |this| { + .when(nip17.not_configured(), |this| { this.child( Button::new("import") .icon(Icon::new(IconName::Relay)) diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index 2eac3ab..532ee81 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use chat::{ChatEvent, ChatRegistry}; +use chat::{ChatEvent, ChatRegistry, InboxState}; use gpui::prelude::FluentBuilder; use gpui::{ div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, @@ -233,13 +233,13 @@ impl Workspace { ), _ => this, }) - .map(|this| match chat.read(cx).relay_state(cx) { - RelayState::Checking => { + .map(|this| match chat.read(cx).state(cx) { + InboxState::Checking => { this.child(div().text_xs().text_color(cx.theme().text_muted).child( SharedString::from("Fetching user's messaging relay list..."), )) } - RelayState::NotConfigured => this.child( + InboxState::RelayNotAvailable => this.child( h_flex() .h_6() .w_full() diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index e45b973..051b838 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -7,6 +7,7 @@ publish.workspace = true [dependencies] common = { path = "../common" } state = { path = "../state" } +person = { path = "../person" } gpui.workspace = true nostr-sdk.workspace = true diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 85e6866..28aceb4 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -4,12 +4,11 @@ use std::time::Duration; use anyhow::{anyhow, Context as AnyhowContext, Error}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window}; use nostr_sdk::prelude::*; +use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; -use state::{app_name, NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT}; - -mod device; - -pub use device::*; +use state::{ + app_name, Announcement, DeviceState, NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT, +}; const IDENTIFIER: &str = "coop:device"; @@ -218,16 +217,17 @@ impl DeviceRegistry { let signer = nostr.read(cx).signer(); let public_key = signer.public_key().unwrap(); - let messaging_relays = nostr.read(cx).messaging_relays(&public_key, cx); + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get(&public_key, cx); + let relay_urls = profile.messaging_relays().clone(); cx.background_spawn(async move { - let relay_urls = messaging_relays.await; let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let id = SubscriptionId::new(DEVICE_GIFTWRAP); // Construct target for subscription - let target: HashMap<&RelayUrl, Filter> = relay_urls - .iter() + let target: HashMap = relay_urls + .into_iter() .map(|relay| (relay, filter.clone())) .collect(); diff --git a/crates/person/Cargo.toml b/crates/person/Cargo.toml index 9d59298..f0239bf 100644 --- a/crates/person/Cargo.toml +++ b/crates/person/Cargo.toml @@ -7,7 +7,6 @@ publish.workspace = true [dependencies] common = { path = "../common" } state = { path = "../state" } -device = { path = "../device" } gpui.workspace = true nostr-sdk.workspace = true diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs index baa81fc..054c221 100644 --- a/crates/person/src/lib.rs +++ b/crates/person/src/lib.rs @@ -5,11 +5,10 @@ use std::time::Duration; use anyhow::{anyhow, Error}; use common::EventUtils; -use device::Announcement; use gpui::{App, AppContext, Context, Entity, Global, Task}; use nostr_sdk::prelude::*; use smallvec::{smallvec, SmallVec}; -use state::{NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT}; +use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT}; mod person; diff --git a/crates/person/src/person.rs b/crates/person/src/person.rs index e852fd5..9c9c582 100644 --- a/crates/person/src/person.rs +++ b/crates/person/src/person.rs @@ -1,9 +1,9 @@ use std::cmp::Ordering; use std::hash::{Hash, Hasher}; -use device::Announcement; use gpui::SharedString; use nostr_sdk::prelude::*; +use state::Announcement; const IMAGE_RESIZER: &str = "https://wsrv.nl"; diff --git a/crates/device/src/device.rs b/crates/state/src/device.rs similarity index 100% rename from crates/device/src/device.rs rename to crates/state/src/device.rs diff --git a/crates/state/src/gossip.rs b/crates/state/src/gossip.rs index 1399827..5cbe245 100644 --- a/crates/state/src/gossip.rs +++ b/crates/state/src/gossip.rs @@ -5,10 +5,7 @@ use nostr_sdk::prelude::*; /// Gossip #[derive(Debug, Clone, Default)] pub struct Gossip { - /// Gossip relays for each public key relays: HashMap)>>, - /// Messaging relays for each public key - messaging_relays: HashMap>, } impl Gossip { @@ -69,39 +66,5 @@ impl Gossip { }) .take(3), ); - - log::info!("Updating gossip relays for: {}", event.pubkey); - } - - /// Get messaging relays for a given public key - pub fn messaging_relays(&self, public_key: &PublicKey) -> Vec { - self.messaging_relays - .get(public_key) - .cloned() - .unwrap_or_default() - .into_iter() - .collect() - } - - /// Insert messaging relays for a public key - pub fn insert_messaging_relays(&mut self, event: &Event) { - self.messaging_relays - .entry(event.pubkey) - .or_default() - .extend( - event - .tags - .iter() - .filter_map(|tag| { - if let Some(TagStandard::Relay(url)) = tag.as_standardized() { - Some(url.to_owned()) - } else { - None - } - }) - .take(3), - ); - - log::info!("Updating messaging relays for: {}", event.pubkey); } } diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 0f20266..4ab7667 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -11,12 +11,14 @@ use nostr_lmdb::prelude::*; use nostr_sdk::prelude::*; mod constants; +mod device; mod gossip; mod nip05; mod nip96; mod signer; pub use constants::*; +pub use device::*; pub use gossip::*; pub use nip05::*; pub use nip96::*; @@ -181,18 +183,16 @@ impl NostrRegistry { } = notification { // Skip if the event has already been processed - if !processed_events.insert(event.id) { - continue; - } - - match event.kind { - Kind::RelayList => { - tx.send_async(event.into_owned()).await?; + if processed_events.insert(event.id) { + match event.kind { + Kind::RelayList => { + tx.send_async(event.into_owned()).await?; + } + Kind::InboxRelays => { + tx.send_async(event.into_owned()).await?; + } + _ => {} } - Kind::InboxRelays => { - tx.send_async(event.into_owned()).await?; - } - _ => {} } } } @@ -205,20 +205,11 @@ impl NostrRegistry { self.tasks.push(cx.spawn(async move |_this, cx| { while let Ok(event) = rx.recv_async().await { - match event.kind { - Kind::RelayList => { - gossip.update(cx, |this, cx| { - this.insert_relays(&event); - cx.notify(); - })?; - } - Kind::InboxRelays => { - gossip.update(cx, |this, cx| { - this.insert_messaging_relays(&event); - cx.notify(); - })?; - } - _ => {} + if let Kind::RelayList = event.kind { + gossip.update(cx, |this, cx| { + this.insert_relays(&event); + cx.notify(); + })?; } } @@ -256,15 +247,6 @@ impl NostrRegistry { self.relay_list_state.clone() } - /// Get a relay hint (messaging relay) for a given public key - pub fn relay_hint(&self, public_key: &PublicKey, cx: &App) -> Option { - 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> { let client = self.client(); @@ -295,21 +277,6 @@ impl NostrRegistry { }) } - /// Get a list of messaging relays for a given public key - pub fn messaging_relays(&self, public_key: &PublicKey, cx: &App) -> Task> { - 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).and_connect().await.ok(); - } - - relays - }) - } - /// Set the connected status of the client fn set_connected(&mut self, cx: &mut Context) { self.connected = true;