From 4b4f6913bb9124720ab8463c63f0628524d2432e Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 9 Mar 2026 20:21:15 +0700 Subject: [PATCH] wip --- crates/coop/src/workspace.rs | 257 +++++++++++++---------------------- crates/state/src/gossip.rs | 83 ----------- crates/state/src/lib.rs | 206 +++++++++++++++++++--------- 3 files changed, 231 insertions(+), 315 deletions(-) delete mode 100644 crates/state/src/gossip.rs diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index b60253c..1a38572 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -1,3 +1,5 @@ +use std::cell::Cell; +use std::rc::Rc; use std::sync::Arc; use ::settings::AppSettings; @@ -21,7 +23,7 @@ use ui::dock_area::panel::PanelView; use ui::dock_area::{ClosePanel, DockArea, DockItem}; use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::notification::Notification; -use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; +use ui::{Disableable, IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; use crate::dialogs::{accounts, settings}; use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list}; @@ -96,9 +98,39 @@ impl Workspace { subscriptions.push( // Subscribe to the signer events cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| { - if let StateEvent::SignerSet = event { - this.set_center_layout(window, cx); - } + match event { + StateEvent::Connecting => { + let note = Notification::new() + .message("Connecting to the bootstrap relay...") + .title("Relays") + .icon(IconName::Relay); + + window.push_notification(note, cx); + } + StateEvent::Connected => { + let note = Notification::new() + .message("Connected to the bootstrap relay") + .title("Relays") + .icon(IconName::Relay); + + window.push_notification(note, cx); + } + StateEvent::RelayNotConfigured => { + this.relay_notification(window, cx); + } + StateEvent::RelayConnected => { + let note = Notification::new() + .message("Connected to user's relay list") + .title("Relays") + .icon(IconName::Relay); + + window.push_notification(note, cx); + } + StateEvent::SignerSet => { + this.set_center_layout(window, cx); + } + _ => {} + }; }), ); @@ -283,27 +315,16 @@ impl Workspace { this.get_announcement(cx); }); } - Command::RefreshRelayList => { - let nostr = NostrRegistry::global(cx); - nostr.update(cx, |this, cx| { - //this.ensure_relay_list(cx); - }); - } Command::ResetEncryption => { self.confirm_reset_encryption(window, cx); } - Command::RefreshMessagingRelays => { - let chat = ChatRegistry::global(cx); - chat.update(cx, |this, cx| { - //this.ensure_messaging_relays(cx); - }); - } Command::ToggleTheme => { self.theme_selector(window, cx); } Command::ToggleAccount => { self.account_selector(window, cx); } + _ => {} } } @@ -450,7 +471,61 @@ impl Workspace { }); } - fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { + fn relay_notification(&mut self, window: &mut Window, cx: &mut Context) { + const BODY: &str = "Coop cannot found your gossip relay list. \ + Maybe you haven't set it yet or relay not responsed"; + + let nostr = NostrRegistry::global(cx); + let signer = nostr.read(cx).signer(); + + let Some(public_key) = signer.public_key() else { + return; + }; + + let entity = nostr.downgrade(); + let loading = Rc::new(Cell::new(false)); + + let note = Notification::new() + .autohide(false) + .icon(IconName::Relay) + .title("Gossip Relays are required") + .content(move |_window, cx| { + v_flex() + .text_sm() + .text_color(cx.theme().text_muted) + .child(SharedString::from(BODY)) + .into_any_element() + }) + .action(move |_window, _cx| { + let entity = entity.clone(); + let public_key = public_key.to_owned(); + + Button::new("retry") + .label("Retry") + .small() + .primary() + .loading(loading.get()) + .disabled(loading.get()) + .on_click({ + let loading = Rc::clone(&loading); + + move |_ev, _window, cx| { + // Set loading state to true + loading.set(true); + // Retry + entity + .update(cx, |this, cx| { + this.ensure_relay_list(&public_key, cx); + }) + .ok(); + } + }) + }); + + window.push_notification(note, cx); + } + + fn titlebar_left(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let nostr = NostrRegistry::global(cx); let signer = nostr.read(cx).signer(); let current_user = signer.public_key(); @@ -529,16 +604,7 @@ impl Workspace { }) } - fn titlebar_right(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { - let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - - let chat = ChatRegistry::global(cx); - - let Some(pkey) = signer.public_key() else { - return div(); - }; - + fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .when(!cx.theme().platform.is_mac(), |this| this.pr_2()) .gap_3() @@ -583,143 +649,6 @@ impl Workspace { ) }), ) - /* - .child( - h_flex() - .gap_2() - .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .map(|this| match inbox_state { - InboxState::Checking => this.child(div().child( - SharedString::from("Fetching user's messaging relay list..."), - )), - InboxState::RelayNotAvailable => { - this.child(div().text_color(cx.theme().warning_active).child( - SharedString::from( - "User hasn't configured a messaging relay list", - ), - )) - } - _ => this, - }), - ) - .child( - Button::new("inbox") - .icon(IconName::Inbox) - .tooltip("Inbox") - .small() - .ghost() - .when(inbox_state.subscribing(), |this| this.indicator()) - .dropdown_menu(move |this, _window, cx| { - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&pkey, cx); - let urls: Vec = profile - .messaging_relays() - .iter() - .map(|url| SharedString::from(url.to_string())) - .collect(); - - // Header - let menu = this.min_w(px(260.)).label("Messaging Relays"); - - // Content - let menu = urls.into_iter().fold(menu, |this, url| { - this.item(PopupMenuItem::element(move |_window, _cx| { - h_flex() - .px_1() - .w_full() - .gap_2() - .text_sm() - .child( - div().size_1p5().rounded_full().bg(gpui::green()), - ) - .child(url.clone()) - })) - }); - - // Footer - menu.separator() - .menu_with_icon( - "Reload", - IconName::Refresh, - Box::new(Command::RefreshMessagingRelays), - ) - .menu_with_icon( - "Update relays", - IconName::Settings, - Box::new(Command::ShowMessaging), - ) - }), - ), - ) - .child( - h_flex() - .gap_2() - .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .map(|this| match nostr.read(cx).relay_list_state { - RelayState::Checking => this - .child(div().child(SharedString::from( - "Fetching user's relay list...", - ))), - RelayState::NotConfigured => { - this.child(div().text_color(cx.theme().warning_active).child( - SharedString::from("User hasn't configured a relay list"), - )) - } - _ => this, - }), - ) - .child( - Button::new("relay-list") - .icon(IconName::Relay) - .tooltip("User's relay list") - .small() - .ghost() - .when(nostr.read(cx).relay_list_state.configured(), |this| { - this.indicator() - }) - .dropdown_menu(move |this, _window, cx| { - let nostr = NostrRegistry::global(cx); - let urls: Vec = vec![]; - - // Header - let menu = this.min_w(px(260.)).label("Relays"); - - // Content - let menu = urls.into_iter().fold(menu, |this, url| { - this.item(PopupMenuItem::element(move |_window, _cx| { - h_flex() - .px_1() - .w_full() - .gap_2() - .text_sm() - .child( - div().size_1p5().rounded_full().bg(gpui::green()), - ) - .child(url.clone()) - })) - }); - - // Footer - menu.separator() - .menu_with_icon( - "Reload", - IconName::Refresh, - Box::new(Command::RefreshRelayList), - ) - .menu_with_icon( - "Update relay list", - IconName::Settings, - Box::new(Command::ShowRelayList), - ) - }), - ), - ) */ } } diff --git a/crates/state/src/gossip.rs b/crates/state/src/gossip.rs deleted file mode 100644 index 805a1ac..0000000 --- a/crates/state/src/gossip.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -use gpui::SharedString; -use nostr_sdk::prelude::*; - -/// Gossip -#[derive(Debug, Clone, Default)] -pub struct Gossip { - relays: HashMap)>>, -} - -impl Gossip { - pub fn read_only_relays(&self, public_key: &PublicKey) -> Vec { - self.relays - .get(public_key) - .map(|relays| { - relays - .iter() - .map(|(url, _)| url.to_string().into()) - .collect() - }) - .unwrap_or_default() - } - - /// Get read relays for a given public key - pub fn read_relays(&self, public_key: &PublicKey) -> Vec { - self.relays - .get(public_key) - .map(|relays| { - relays - .iter() - .filter_map(|(url, metadata)| { - if metadata.is_none() || metadata == &Some(RelayMetadata::Read) { - Some(url.to_owned()) - } else { - None - } - }) - .collect() - }) - .unwrap_or_default() - } - - /// Get write relays for a given public key - pub fn write_relays(&self, public_key: &PublicKey) -> Vec { - self.relays - .get(public_key) - .map(|relays| { - relays - .iter() - .filter_map(|(url, metadata)| { - if metadata.is_none() || metadata == &Some(RelayMetadata::Write) { - Some(url.to_owned()) - } else { - None - } - }) - .collect() - }) - .unwrap_or_default() - } - - /// Insert gossip relays for a public key - pub fn insert_relays(&mut self, event: &Event) { - self.relays.entry(event.pubkey).or_default().extend( - event - .tags - .iter() - .filter_map(|tag| { - if let Some(TagStandard::RelayMetadata { - relay_url, - metadata, - }) = tag.clone().to_standardized() - { - Some((relay_url, metadata)) - } else { - None - } - }) - .take(3), - ); - } -} diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 8407d17..54bd4be 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -14,14 +14,12 @@ use nostr_sdk::prelude::*; mod blossom; mod constants; mod device; -mod gossip; mod nip05; mod signer; pub use blossom::*; pub use constants::*; pub use device::*; -pub use gossip::*; pub use nip05::*; pub use signer::*; @@ -52,6 +50,8 @@ pub enum StateEvent { Connected, /// User has not set up NIP-65 relays RelayNotConfigured, + /// Connected to NIP-65 relays + RelayConnected, /// A new signer has been set SignerSet, /// An error occurred @@ -76,7 +76,7 @@ pub struct NostrRegistry { app_keys: Keys, /// Tasks for asynchronous operations - tasks: Vec>>, + tasks: Vec>, } impl EventEmitter for NostrRegistry {} @@ -173,35 +173,42 @@ impl NostrRegistry { fn connect(&mut self, cx: &mut Context) { let client = self.client(); + let task: Task> = cx.background_spawn(async move { + // Add search relay to the relay pool + for url in SEARCH_RELAYS.into_iter() { + client.add_relay(url).await?; + } + + // Add bootstrap relay to the relay pool + for url in BOOTSTRAP_RELAYS.into_iter() { + client.add_relay(url).await?; + } + + // Connect to all added relays + client.connect().and_wait(Duration::from_secs(2)).await; + + Ok(()) + }); + // Emit connecting event cx.emit(StateEvent::Connecting); - self.tasks.push(cx.spawn(async move |this, cx| { - cx.background_executor() - .await_on_background(async move { - // Add search relay to the relay pool - for url in SEARCH_RELAYS.into_iter() { - client.add_relay(url).await.ok(); - } - - // Add bootstrap relay to the relay pool - for url in BOOTSTRAP_RELAYS.into_iter() { - client.add_relay(url).await.ok(); - } - - // Connect to all added relays - client.connect().and_wait(Duration::from_secs(2)).await; - }) - .await; - - // Update the state - this.update(cx, |this, cx| { - cx.emit(StateEvent::Connected); - this.get_npubs(cx); - })?; - - Ok(()) - })); + self.tasks + .push(cx.spawn(async move |this, cx| match task.await { + Ok(_) => { + this.update(cx, |this, cx| { + cx.emit(StateEvent::Connected); + this.get_npubs(cx); + }) + .ok(); + } + Err(e) => { + this.update(cx, |_this, cx| { + cx.emit(StateEvent::Error(SharedString::from(e.to_string()))); + }) + .ok(); + } + })); } /// Get all used npubs @@ -247,24 +254,26 @@ impl NostrRegistry { true => { this.update(cx, |this, cx| { this.create_identity(cx); - })?; + }) + .ok(); } false => { // TODO: auto login - npubs.update(cx, |this, cx| { - this.extend(public_keys); - cx.notify(); - })?; + npubs + .update(cx, |this, cx| { + this.extend(public_keys); + cx.notify(); + }) + .ok(); } }, Err(e) => { this.update(cx, |_this, cx| { cx.emit(StateEvent::Error(SharedString::from(e.to_string()))); - })?; + }) + .ok(); } } - - Ok(()) })); } @@ -337,15 +346,20 @@ impl NostrRegistry { }); self.tasks.push(cx.spawn(async move |this, cx| { - // Wait for the task to complete - task.await?; - - // Set signer - this.update(cx, |this, cx| { - this.set_signer(keys, cx); - })?; - - Ok(()) + match task.await { + Ok(_) => { + this.update(cx, |this, cx| { + this.set_signer(keys, cx); + }) + .ok(); + } + Err(e) => { + this.update(cx, |_this, cx| { + cx.emit(StateEvent::Error(SharedString::from(e.to_string()))); + }) + .ok(); + } + }; })); } @@ -424,6 +438,7 @@ impl NostrRegistry { Ok(public_key) => { // Update states this.update(cx, |this, cx| { + this.ensure_relay_list(&public_key, cx); // Add public key to npubs if not already present this.npubs.update(cx, |this, cx| { if !this.contains(&public_key) { @@ -433,16 +448,16 @@ impl NostrRegistry { }); // Emit signer changed event cx.emit(StateEvent::SignerSet); - })?; + }) + .ok(); } Err(e) => { this.update(cx, |_this, cx| { cx.emit(StateEvent::Error(SharedString::from(e.to_string()))); - })?; + }) + .ok(); } - } - - Ok(()) + }; })); } @@ -454,16 +469,15 @@ impl NostrRegistry { self.tasks.push(cx.spawn(async move |this, cx| { let key_path = keys_dir.join(format!("{}.npub", npub)); - smol::fs::remove_file(key_path).await?; + smol::fs::remove_file(key_path).await.ok(); this.update(cx, |this, cx| { this.npubs().update(cx, |this, cx| { this.retain(|k| k != &public_key); cx.notify(); }); - })?; - - Ok(()) + }) + .ok(); })); } @@ -481,16 +495,16 @@ impl NostrRegistry { Ok(_) => { this.update(cx, |this, cx| { this.set_signer(keys, cx); - })?; + }) + .ok(); } Err(e) => { this.update(cx, |_this, cx| { cx.emit(StateEvent::Error(SharedString::from(e.to_string()))); - })?; + }) + .ok(); } - } - - Ok(()) + }; })); } @@ -512,32 +526,88 @@ impl NostrRegistry { match task.await { Ok((public_key, uri)) => { let username = public_key.to_bech32().unwrap(); - let write_credential = this.read_with(cx, |_this, cx| { - cx.write_credentials(&username, "nostrconnect", uri.to_string().as_bytes()) - })?; + let write_credential = this + .read_with(cx, |_this, cx| { + cx.write_credentials( + &username, + "nostrconnect", + uri.to_string().as_bytes(), + ) + }) + .unwrap(); match write_credential.await { Ok(_) => { this.update(cx, |this, cx| { this.set_signer(nip46, cx); - })?; + }) + .ok(); } Err(e) => { this.update(cx, |_this, cx| { cx.emit(StateEvent::Error(SharedString::from(e.to_string()))); - })?; + }) + .ok(); } } } Err(e) => { this.update(cx, |_this, cx| { cx.emit(StateEvent::Error(SharedString::from(e.to_string()))); - })?; + }) + .ok(); + } + }; + })); + } + + pub fn ensure_relay_list(&mut self, public_key: &PublicKey, cx: &mut Context) { + let task = self.get_event(public_key, Kind::RelayList, cx); + + self.tasks.push(cx.spawn(async move |this, cx| { + match task.await { + Ok(_) => { + this.update(cx, |_this, cx| { + cx.emit(StateEvent::RelayConnected); + }) + .ok(); + } + Err(e) => { + this.update(cx, |_this, cx| { + cx.emit(StateEvent::RelayNotConfigured); + cx.emit(StateEvent::Error(SharedString::from(e.to_string()))); + }) + .ok(); + } + }; + })); + } + + /// Get an event with the given author and kind. + pub fn get_event( + &self, + author: &PublicKey, + kind: Kind, + cx: &App, + ) -> Task> { + let client = self.client(); + let public_key = *author; + + cx.background_spawn(async move { + let filter = Filter::new().kind(kind).author(public_key).limit(1); + let mut stream = client + .stream_events(filter) + .timeout(Duration::from_millis(800)) + .await?; + + while let Some((_url, res)) = stream.next().await { + if let Ok(event) = res { + return Ok(event); } } - Ok(()) - })); + Err(anyhow!("No event found")) + }) } /// Get the public key of a NIP-05 address