From 39c04cabade12a0c61ed12d67c2a0512c203b4a9 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 9 Mar 2026 13:58:49 +0700 Subject: [PATCH] wip --- Cargo.lock | 19 ++ Cargo.toml | 1 + crates/chat/src/lib.rs | 204 +++++++++------------ crates/common/src/display.rs | 2 +- crates/coop/src/dialogs/accounts.rs | 13 +- crates/coop/src/dialogs/connect.rs | 12 +- crates/coop/src/dialogs/import.rs | 16 +- crates/coop/src/panels/greeter.rs | 79 +------- crates/coop/src/workspace.rs | 270 ++++++++++++++-------------- crates/device/src/lib.rs | 166 ++++++++--------- crates/person/Cargo.toml | 1 + crates/person/src/lib.rs | 38 ++-- crates/person/src/person.rs | 56 +++--- crates/relay_auth/src/lib.rs | 29 +-- crates/state/Cargo.toml | 1 + crates/state/src/lib.rs | 122 ++++++------- 16 files changed, 475 insertions(+), 554 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6fd80a6..f0aa15f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4299,6 +4299,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "nostr-memory" +version = "0.44.0" +source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9" +dependencies = [ + "btreecap", + "nostr", + "nostr-database", + "tokio", +] + [[package]] name = "nostr-sdk" version = "0.44.1" @@ -4759,6 +4770,7 @@ dependencies = [ "smallvec", "smol", "state", + "urlencoding", ] [[package]] @@ -6434,6 +6446,7 @@ dependencies = [ "nostr-connect", "nostr-gossip-sqlite", "nostr-lmdb", + "nostr-memory", "nostr-sdk", "petname", "rustls", @@ -7403,6 +7416,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "usvg" version = "0.45.1" diff --git a/Cargo.toml b/Cargo.toml index b35859f..032b6b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" } # Nostr nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" } +nostr-memory = { git = "https://github.com/rust-nostr/nostr" } nostr-connect = { git = "https://github.com/rust-nostr/nostr" } nostr-blossom = { git = "https://github.com/rust-nostr/nostr" } nostr-gossip-sqlite = { git = "https://github.com/rust-nostr/nostr" } diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index eea3538..355ad01 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -10,11 +10,12 @@ use common::EventUtils; use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::skim::SkimMatcherV2; use gpui::{ - App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window, + App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task, + WeakEntity, Window, }; use nostr_sdk::prelude::*; use smallvec::{SmallVec, smallvec}; -use state::{DEVICE_GIFTWRAP, NostrRegistry, RelayState, TIMEOUT, USER_GIFTWRAP}; +use state::{DEVICE_GIFTWRAP, NostrRegistry, StateEvent, TIMEOUT, USER_GIFTWRAP}; mod message; mod room; @@ -39,6 +40,10 @@ pub enum ChatEvent { CloseRoom(u64), /// An event to notify UI about a new chat request Ping, + /// An event to notify UI that the chat registry has subscribed to messaging relays + Subscribed, + /// An error occurred + Error(SharedString), } /// Channel signal. @@ -48,41 +53,25 @@ enum Signal { Message(NewMessage), /// Eose received from relay pool 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) - } - - pub fn subscribing(&self) -> bool { - matches!(self, InboxState::Subscribing) - } + /// An error occurred + Error(SharedString), } /// Chat Registry #[derive(Debug)] pub struct ChatRegistry { - /// Relay state for messaging relay list - state: Entity, - /// Collection of all chat rooms rooms: Vec>, /// Tracking the status of unwrapping gift wrap events. tracking_flag: Arc, + /// Channel for sending signals to the UI. + signal_tx: flume::Sender, + + /// Channel for receiving signals from the UI. + signal_rx: flume::Receiver, + /// Async tasks tasks: SmallVec<[Task>; 2]>, @@ -105,36 +94,18 @@ impl ChatRegistry { /// Create a new chat registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { - let state = cx.new(|_| InboxState::default()); let nostr = NostrRegistry::global(cx); - + let (tx, rx) = flume::unbounded::(); let mut subscriptions = smallvec![]; subscriptions.push( - // Observe the nip65 state and load chat rooms on every state change - cx.observe(&nostr, |this, state, cx| { - match state.read(cx).relay_list_state { - RelayState::Idle => { - this.reset(cx); - } - RelayState::Configured => { - this.get_contact_list(cx); - this.ensure_messaging_relays(cx); - } - _ => {} - } - - // Load rooms on every state change - this.get_rooms(cx); - }), - ); - - subscriptions.push( - // Observe the nip17 state and load chat rooms on every state change - 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); + // Subscribe to the signer event + cx.subscribe(&nostr, |this, _state, event, cx| { + if let StateEvent::SignerSet = event { + this.reset(cx); + this.get_rooms(cx); + this.get_contact_list(cx); + this.get_messages(cx) } }), ); @@ -147,9 +118,10 @@ impl ChatRegistry { }); Self { - state, rooms: vec![], tracking_flag: Arc::new(AtomicBool::new(false)), + signal_rx: rx, + signal_tx: tx, tasks: smallvec![], _subscriptions: subscriptions, } @@ -167,7 +139,8 @@ impl ChatRegistry { let sub_id2 = SubscriptionId::new(USER_GIFTWRAP); // Channel for communication between nostr and gpui - let (tx, rx) = flume::bounded::(1024); + let tx = self.signal_tx.clone(); + let rx = self.signal_rx.clone(); self.tasks.push(cx.background_spawn(async move { let device_signer = signer.get_encryption_signer().await; @@ -194,19 +167,29 @@ impl ChatRegistry { // Extract the rumor from the gift wrap event match extract_rumor(&client, &device_signer, event.as_ref()).await { - Ok(rumor) => match rumor.created_at >= initialized_at { - true => { - let new_message = NewMessage::new(event.id, rumor); - let signal = Signal::Message(new_message); + Ok(rumor) => { + if rumor.tags.is_empty() { + let error: SharedString = + "Message doesn't belong to any rooms".into(); + tx.send_async(Signal::Error(error)).await?; + } - tx.send_async(signal).await?; + match rumor.created_at >= initialized_at { + true => { + let new_message = NewMessage::new(event.id, rumor); + let signal = Signal::Message(new_message); + + tx.send_async(signal).await?; + } + false => { + status.store(true, Ordering::Release); + } } - false => { - status.store(true, Ordering::Release); - } - }, + } Err(e) => { - log::warn!("Failed to unwrap the gift wrap event: {e}"); + let error: SharedString = + format!("Failed to unwrap the gift wrap event: {e}").into(); + tx.send_async(Signal::Error(error)).await?; } } } @@ -235,6 +218,11 @@ impl ChatRegistry { this.get_rooms(cx); })?; } + Signal::Error(error) => { + this.update(cx, |_this, cx| { + cx.emit(ChatEvent::Error(error)); + })?; + } }; } @@ -245,6 +233,7 @@ impl ChatRegistry { /// Tracking the status of unwrapping gift wrap events. fn tracking(&mut self, cx: &mut Context) { let status = self.tracking_flag.clone(); + let tx = self.signal_tx.clone(); self.tasks.push(cx.background_spawn(async move { let loop_duration = Duration::from_secs(15); @@ -252,6 +241,9 @@ impl ChatRegistry { loop { if status.load(Ordering::Acquire) { _ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed); + _ = tx.send_async(Signal::Eose).await; + } else { + _ = tx.send_async(Signal::Eose).await; } smol::Timer::after(loop_duration).await; } @@ -289,27 +281,29 @@ impl ChatRegistry { self.tasks.push(task); } - /// Ensure messaging relays are set up for the current user. - pub fn ensure_messaging_relays(&mut self, cx: &mut Context) { - let task = self.verify_relays(cx); - - // Set state to checking - self.set_state(InboxState::Checking, cx); + /// Get all messages for current user + fn get_messages(&mut self, cx: &mut Context) { + let task = self.subscribe(cx); self.tasks.push(cx.spawn(async move |this, cx| { - let result = task.await?; - - // Update state - this.update(cx, |this, cx| { - this.set_state(result, cx); - })?; - + match task.await { + Ok(_) => { + this.update(cx, |_this, cx| { + cx.emit(ChatEvent::Subscribed); + })?; + } + Err(e) => { + this.update(cx, |_this, cx| { + cx.emit(ChatEvent::Error(SharedString::from(e.to_string()))); + })?; + } + } Ok(()) })); } - // Verify messaging relay list for current user - fn verify_relays(&mut self, cx: &mut Context) -> Task> { + // Get messaging relay list for current user + fn get_messaging_relays(&self, cx: &App) -> Task, Error>> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); @@ -330,50 +324,25 @@ impl ChatRegistry { .await?; while let Some((_url, res)) = stream.next().await { - match res { - Ok(event) => { - return Ok(InboxState::RelayConfigured(Box::new(event))); - } - Err(e) => { - log::error!("Failed to receive relay list event: {e}"); - } + if let Ok(event) = res { + let urls: Vec = nip17::extract_owned_relay_list(event).collect(); + return Ok(urls); } } - Ok(InboxState::RelayNotAvailable) + Err(anyhow!("Messaging Relays not found")) }) } - /// Get all messages for current user - 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| { - 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(&mut self, urls: I, cx: &mut Context) -> Task> - where - I: IntoIterator, - { + fn subscribe(&self, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); - let urls = urls.into_iter().collect::>(); + let urls = self.get_messaging_relays(cx); cx.background_spawn(async move { + let urls = urls.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); @@ -400,19 +369,6 @@ 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 state(&self, cx: &App) -> InboxState { - self.state.read(cx).clone() - } - /// Get the loading status of the chat registry pub fn loading(&self) -> bool { self.tracking_flag.load(Ordering::Acquire) diff --git a/crates/common/src/display.rs b/crates/common/src/display.rs index 7b39ae0..79ed85f 100644 --- a/crates/common/src/display.rs +++ b/crates/common/src/display.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use anyhow::{anyhow, Error}; +use anyhow::{Error, anyhow}; use chrono::{Local, TimeZone}; use gpui::{Image, ImageFormat, SharedString}; use nostr_sdk::prelude::*; diff --git a/crates/coop/src/dialogs/accounts.rs b/crates/coop/src/dialogs/accounts.rs index b9fe557..5d72bd5 100644 --- a/crates/coop/src/dialogs/accounts.rs +++ b/crates/coop/src/dialogs/accounts.rs @@ -1,17 +1,17 @@ use anyhow::Error; use gpui::prelude::FluentBuilder; use gpui::{ - div, px, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, - Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, + App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, + SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, div, px, }; use nostr_sdk::prelude::*; use person::PersonRegistry; -use state::{NostrRegistry, SignerEvent}; +use state::{NostrRegistry, StateEvent}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::indicator::Indicator; -use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, WindowExtension}; +use ui::{Disableable, Icon, IconName, Sizable, WindowExtension, h_flex, v_flex}; use crate::dialogs::connect::ConnectSigner; use crate::dialogs::import::ImportKey; @@ -44,13 +44,14 @@ impl AccountSelector { let nostr = NostrRegistry::global(cx); let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| { match event { - SignerEvent::Set => { + StateEvent::SignerSet => { window.close_all_modals(cx); window.refresh(); } - SignerEvent::Error(e) => { + StateEvent::Error(e) => { this.set_error(e.to_string(), cx); } + _ => {} }; }); diff --git a/crates/coop/src/dialogs/connect.rs b/crates/coop/src/dialogs/connect.rs index 9a95d82..00b7d2c 100644 --- a/crates/coop/src/dialogs/connect.rs +++ b/crates/coop/src/dialogs/connect.rs @@ -4,13 +4,13 @@ use std::time::Duration; use common::TextUtils; use gpui::prelude::FluentBuilder; use gpui::{ - div, img, px, AppContext, Context, Entity, Image, IntoElement, ParentElement, Render, - SharedString, Styled, Subscription, Window, + AppContext, Context, Entity, Image, IntoElement, ParentElement, Render, SharedString, Styled, + Subscription, Window, div, img, px, }; use nostr_connect::prelude::*; use state::{ - CoopAuthUrlHandler, NostrRegistry, SignerEvent, CLIENT_NAME, NOSTR_CONNECT_RELAY, - NOSTR_CONNECT_TIMEOUT, + CLIENT_NAME, CoopAuthUrlHandler, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT, NostrRegistry, + StateEvent, }; use theme::ActiveTheme; use ui::v_flex; @@ -31,7 +31,7 @@ impl ConnectSigner { let error = cx.new(|_| None); let nostr = NostrRegistry::global(cx); - let app_keys = nostr.read(cx).app_keys.clone(); + let app_keys = nostr.read(cx).keys(); let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap(); @@ -55,7 +55,7 @@ impl ConnectSigner { // Subscribe to the signer event let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| { - if let SignerEvent::Error(e) = event { + if let StateEvent::Error(e) = event { this.set_error(e, cx); } }); diff --git a/crates/coop/src/dialogs/import.rs b/crates/coop/src/dialogs/import.rs index 0f861bd..0b6baf1 100644 --- a/crates/coop/src/dialogs/import.rs +++ b/crates/coop/src/dialogs/import.rs @@ -1,18 +1,18 @@ use std::time::Duration; -use anyhow::{anyhow, Error}; +use anyhow::{Error, anyhow}; use gpui::prelude::FluentBuilder; use gpui::{ - div, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, - Subscription, Task, Window, + AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, + Subscription, Task, Window, div, }; use nostr_connect::prelude::*; -use smallvec::{smallvec, SmallVec}; -use state::{CoopAuthUrlHandler, NostrRegistry, SignerEvent}; +use smallvec::{SmallVec, smallvec}; +use state::{CoopAuthUrlHandler, NostrRegistry, StateEvent}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::input::{InputEvent, InputState, TextInput}; -use ui::{v_flex, Disableable}; +use ui::{Disableable, v_flex}; #[derive(Debug)] pub struct ImportKey { @@ -60,7 +60,7 @@ impl ImportKey { subscriptions.push( // Subscribe to the nostr signer event cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| { - if let SignerEvent::Error(e) = event { + if let StateEvent::Error(e) = event { this.set_error(e, cx); } }), @@ -117,7 +117,7 @@ impl ImportKey { }; let nostr = NostrRegistry::global(cx); - let app_keys = nostr.read(cx).app_keys.clone(); + let app_keys = nostr.read(cx).keys(); let timeout = Duration::from_secs(30); // Construct the nostr connect signer diff --git a/crates/coop/src/panels/greeter.rs b/crates/coop/src/panels/greeter.rs index aa47072..208ecab 100644 --- a/crates/coop/src/panels/greeter.rs +++ b/crates/coop/src/panels/greeter.rs @@ -1,17 +1,15 @@ -use chat::{ChatRegistry, InboxState}; -use gpui::prelude::FluentBuilder; use gpui::{ - div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, - IntoElement, ParentElement, Render, SharedString, Styled, Window, + AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, + IntoElement, ParentElement, Render, SharedString, Styled, Window, div, svg, }; -use state::{NostrRegistry, RelayState}; +use state::NostrRegistry; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::dock_area::dock::DockPlacement; use ui::dock_area::panel::{Panel, PanelEvent}; -use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt}; +use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex}; -use crate::panels::{messaging_relays, profile, relay_list}; +use crate::panels::profile; use crate::workspace::Workspace; pub fn init(window: &mut Window, cx: &mut App) -> Entity { @@ -82,15 +80,6 @@ impl Render for GreeterPanel { const TITLE: &str = "Welcome to Coop!"; const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr."; - let chat = ChatRegistry::global(cx); - let nip17 = chat.read(cx).state(cx); - - let nostr = NostrRegistry::global(cx); - let nip65 = nostr.read(cx).relay_list_state.clone(); - - let required_actions = - nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable; - h_flex() .size_full() .items_center() @@ -130,64 +119,6 @@ impl Render for GreeterPanel { ), ), ) - .when(required_actions, |this| { - this.child( - v_flex() - .gap_2() - .w_full() - .child( - h_flex() - .gap_2() - .w_full() - .text_xs() - .font_semibold() - .text_color(cx.theme().text_muted) - .child(SharedString::from("Required Actions")) - .child(div().flex_1().h_px().bg(cx.theme().border)), - ) - .child( - v_flex() - .gap_2() - .w_full() - .when(nip65.not_configured(), |this| { - this.child( - Button::new("relaylist") - .icon(Icon::new(IconName::Relay)) - .label("Set up relay list") - .ghost() - .small() - .justify_start() - .on_click(move |_ev, window, cx| { - Workspace::add_panel( - relay_list::init(window, cx), - DockPlacement::Center, - window, - cx, - ); - }), - ) - }) - .when(nip17.not_configured(), |this| { - this.child( - Button::new("import") - .icon(Icon::new(IconName::Relay)) - .label("Set up messaging relays") - .ghost() - .small() - .justify_start() - .on_click(move |_ev, window, cx| { - Workspace::add_panel( - messaging_relays::init(window, cx), - DockPlacement::Center, - window, - cx, - ); - }), - ) - }), - ), - ) - }) .child( v_flex() .gap_2() diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index f2a58a9..b60253c 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use ::settings::AppSettings; -use chat::{ChatEvent, ChatRegistry, InboxState}; +use chat::{ChatEvent, ChatRegistry}; use device::DeviceRegistry; use gpui::prelude::FluentBuilder; use gpui::{ @@ -11,7 +11,7 @@ use gpui::{ use person::PersonRegistry; use serde::Deserialize; use smallvec::{SmallVec, smallvec}; -use state::{NostrRegistry, RelayState, SignerEvent}; +use state::{NostrRegistry, StateEvent}; use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry}; use title_bar::TitleBar; use ui::avatar::Avatar; @@ -96,7 +96,7 @@ impl Workspace { subscriptions.push( // Subscribe to the signer events cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| { - if let SignerEvent::Set = event { + if let StateEvent::SignerSet = event { this.set_center_layout(window, cx); } }), @@ -295,7 +295,7 @@ impl Workspace { Command::RefreshMessagingRelays => { let chat = ChatRegistry::global(cx); chat.update(cx, |this, cx| { - this.ensure_messaging_relays(cx); + //this.ensure_messaging_relays(cx); }); } Command::ToggleTheme => { @@ -534,7 +534,6 @@ impl Workspace { let signer = nostr.read(cx).signer(); let chat = ChatRegistry::global(cx); - let inbox_state = chat.read(cx).state(cx); let Some(pkey) = signer.public_key() else { return div(); @@ -584,142 +583,143 @@ 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(); + /* + .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"); + // 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()) - })) - }); + // 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![]; + // 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"); + // 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()) - })) - }); + // 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), - ) - }), - ), - ) + // 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/device/src/lib.rs b/crates/device/src/lib.rs index 62c8b6f..4cf0014 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -5,15 +5,12 @@ use std::time::Duration; use anyhow::{Context as AnyhowContext, Error, anyhow}; use gpui::{ - App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled, - Subscription, Task, Window, div, + App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement, + SharedString, Styled, Task, Window, div, }; use nostr_sdk::prelude::*; use person::PersonRegistry; -use smallvec::{SmallVec, smallvec}; -use state::{ - Announcement, DEVICE_GIFTWRAP, DeviceState, NostrRegistry, RelayState, TIMEOUT, app_name, -}; +use state::{Announcement, DEVICE_GIFTWRAP, DeviceState, NostrRegistry, TIMEOUT, app_name}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; @@ -32,6 +29,15 @@ struct GlobalDeviceRegistry(Entity); impl Global for GlobalDeviceRegistry {} +/// Device event. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum DeviceEvent { + /// A new encryption signer has been set + Set, + /// An error occurred + Error(SharedString), +} + /// Device Registry /// /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md @@ -42,11 +48,10 @@ pub struct DeviceRegistry { /// Async tasks tasks: Vec>>, - - /// Subscriptions - _subscriptions: SmallVec<[Subscription; 1]>, } +impl EventEmitter for DeviceRegistry {} + impl DeviceRegistry { /// Retrieve the global device registry state pub fn global(cx: &App) -> Entity { @@ -60,27 +65,16 @@ impl DeviceRegistry { /// Create a new device registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { - let nostr = NostrRegistry::global(cx); - let mut subscriptions = smallvec![]; + let state = DeviceState::default(); - subscriptions.push( - // Observe the NIP-65 state - cx.observe(&nostr, |this, state, cx| { - if state.read(cx).relay_list_state == RelayState::Configured { - this.get_announcement(cx); - }; - }), - ); - - // Run at the end of current cycle cx.defer_in(window, |this, window, cx| { this.handle_notifications(window, cx); + this.get_announcement(cx); }); Self { - state: DeviceState::default(), + state, tasks: vec![], - _subscriptions: subscriptions, } } @@ -123,30 +117,27 @@ impl DeviceRegistry { Ok(()) })); - self.tasks.push( - // Update GPUI states - cx.spawn_in(window, async move |this, cx| { - while let Ok(event) = rx.recv_async().await { - match event.kind { - // New request event - Kind::Custom(4454) => { - this.update_in(cx, |this, window, cx| { - this.ask_for_approval(event, window, cx); - })?; - } - // New response event - Kind::Custom(4455) => { - this.update(cx, |this, cx| { - this.extract_encryption(event, cx); - })?; - } - _ => {} + self.tasks.push(cx.spawn_in(window, async move |this, cx| { + while let Ok(event) = rx.recv_async().await { + match event.kind { + // New request event + Kind::Custom(4454) => { + this.update_in(cx, |this, window, cx| { + this.ask_for_approval(event, window, cx); + })?; } + // New response event + Kind::Custom(4455) => { + this.update(cx, |this, cx| { + this.extract_encryption(event, cx); + })?; + } + _ => {} } + } - Ok(()) - }), - ); + Ok(()) + })); } /// Get the device state @@ -191,45 +182,68 @@ impl DeviceRegistry { fn get_messages(&mut self, cx: &mut Context) { let task = self.subscribe_to_giftwrap_events(cx); - self.tasks.push(cx.spawn(async move |_this, _cx| { - task.await?; - - // Update state - + self.tasks.push(cx.spawn(async move |this, cx| { + if let Err(e) = task.await { + this.update(cx, |_this, cx| { + cx.emit(DeviceEvent::Error(SharedString::from(e.to_string()))); + })?; + } 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> { + /// Get the messaging relays for the current user + fn get_user_messaging_relays(&self, cx: &App) -> Task, Error>> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); - let Some(public_key) = signer.public_key() else { - return Task::ready(Err(anyhow!("User not found"))); - }; + cx.background_spawn(async move { + let public_key = signer.get_public_key().await?; + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&public_key, cx); - let relay_urls = profile.messaging_relays().clone(); + if let Some(event) = client.database().query(filter).await?.first_owned() { + // Extract relay URLs from the event + let urls: Vec = nip17::extract_owned_relay_list(event).collect(); + + // Ensure all relays are connected + for url in urls.iter() { + client.add_relay(url).and_connect().await?; + } + + Ok(urls) + } else { + Err(anyhow!("Relays not found")) + } + }) + } + + /// Continuously get gift wrap events for the current user in their messaging relays + fn subscribe_to_giftwrap_events(&self, cx: &App) -> Task> { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + let signer = nostr.read(cx).signer(); + let urls = self.get_user_messaging_relays(cx); cx.background_spawn(async move { + let urls = urls.await?; + let encryption = signer.get_encryption_signer().await.context("not found")?; + let public_key = encryption.get_public_key().await?; + let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let id = SubscriptionId::new(DEVICE_GIFTWRAP); // Construct target for subscription - let target: HashMap = relay_urls + let target: HashMap = urls .into_iter() .map(|relay| (relay, filter.clone())) .collect(); - let output = client.subscribe(target).with_id(id).await?; - - log::info!( - "Successfully subscribed to encryption gift-wrap messages on: {:?}", - output.success - ); + // Subscribe + client.subscribe(target).with_id(id).await?; Ok(()) }) @@ -239,16 +253,14 @@ impl DeviceRegistry { pub fn get_announcement(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); - - let Some(public_key) = signer.public_key() else { - return; - }; // Reset state before fetching announcement self.reset(cx); let task: Task> = 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 the device announcement event let filter = Filter::new() .kind(Kind::Custom(10044)) @@ -262,18 +274,12 @@ impl DeviceRegistry { .await?; while let Some((_url, res)) = stream.next().await { - match res { - Ok(event) => { - log::info!("Received device announcement event: {event:?}"); - return Ok(event); - } - Err(e) => { - log::error!("Failed to receive device announcement event: {e}"); - } + if let Ok(event) = res { + return Ok(event); } } - Err(anyhow!("Device announcement not found")) + Err(anyhow!("Announcement not found")) }); self.tasks.push(cx.spawn(async move |this, cx| { @@ -436,7 +442,7 @@ impl DeviceRegistry { let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); - let app_keys = nostr.read(cx).app_keys.clone(); + let app_keys = nostr.read(cx).keys(); let app_pubkey = app_keys.public_key(); let task: Task, Error>> = cx.background_spawn(async move { @@ -506,7 +512,7 @@ impl DeviceRegistry { /// Parse the response event for device keys from other devices fn extract_encryption(&mut self, event: Event, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let app_keys = nostr.read(cx).app_keys.clone(); + let app_keys = nostr.read(cx).keys(); let task: Task> = cx.background_spawn(async move { let root_device = event diff --git a/crates/person/Cargo.toml b/crates/person/Cargo.toml index f0239bf..8ce2e2b 100644 --- a/crates/person/Cargo.toml +++ b/crates/person/Cargo.toml @@ -15,3 +15,4 @@ smallvec.workspace = true smol.workspace = true flume.workspace = true log.workspace = true +urlencoding = "2.1.3" diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs index 8721372..3e58064 100644 --- a/crates/person/src/lib.rs +++ b/crates/person/src/lib.rs @@ -3,12 +3,12 @@ use std::collections::{HashMap, HashSet}; use std::rc::Rc; use std::time::Duration; -use anyhow::{anyhow, Error}; +use anyhow::{Error, anyhow}; use common::EventUtils; use gpui::{App, AppContext, Context, Entity, Global, Task}; use nostr_sdk::prelude::*; -use smallvec::{smallvec, SmallVec}; -use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT}; +use smallvec::{SmallVec, smallvec}; +use state::{Announcement, BOOTSTRAP_RELAYS, NostrRegistry, TIMEOUT}; mod person; @@ -36,7 +36,7 @@ pub struct PersonRegistry { persons: HashMap>, /// Set of public keys that have been seen - seen: Rc>>, + seens: Rc>>, /// Sender for requesting metadata sender: flume::Sender, @@ -63,7 +63,7 @@ impl PersonRegistry { // Channel for communication between nostr and gpui let (tx, rx) = flume::bounded::(100); - let (mta_tx, mta_rx) = flume::bounded::(100); + let (mta_tx, mta_rx) = flume::unbounded::(); let mut tasks = smallvec![]; @@ -135,7 +135,7 @@ impl PersonRegistry { Self { persons: HashMap::new(), - seen: Rc::new(RefCell::new(HashSet::new())), + seens: Rc::new(RefCell::new(HashSet::new())), sender: mta_tx, _tasks: tasks, } @@ -208,7 +208,9 @@ impl PersonRegistry { } } _ => { - get_metadata(client, std::mem::take(&mut batch)).await.ok(); + if !batch.is_empty() { + get_metadata(client, std::mem::take(&mut batch)).await.ok(); + } } } } @@ -216,32 +218,42 @@ impl PersonRegistry { /// Set profile encryption keys announcement fn set_announcement(&mut self, event: &Event, cx: &mut App) { - if let Some(person) = self.persons.get(&event.pubkey) { - let announcement = Announcement::from(event); + let announcement = Announcement::from(event); + if let Some(person) = self.persons.get(&event.pubkey) { person.update(cx, |person, cx| { person.set_announcement(announcement); cx.notify(); }); + } else { + let person = + Person::new(event.pubkey, Metadata::default()).with_announcement(announcement); + self.insert(person, cx); } } /// Set messaging relays for a person fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) { - if let Some(person) = self.persons.get(&event.pubkey) { - let urls: Vec = nip17::extract_relay_list(event).cloned().collect(); + let urls: Vec = nip17::extract_relay_list(event).cloned().collect(); + if let Some(person) = self.persons.get(&event.pubkey) { person.update(cx, |person, cx| { person.set_messaging_relays(urls); cx.notify(); }); + } else { + let person = Person::new(event.pubkey, Metadata::default()).with_messaging_relays(urls); + self.insert(person, cx); } } /// Insert batch of persons fn bulk_inserts(&mut self, persons: Vec, cx: &mut Context) { for person in persons.into_iter() { - self.persons.insert(person.public_key(), cx.new(|_| person)); + let public_key = person.public_key(); + self.persons + .entry(public_key) + .or_insert_with(|| cx.new(|_| person)); } cx.notify(); } @@ -270,7 +282,7 @@ impl PersonRegistry { } let public_key = *public_key; - let mut seen = self.seen.borrow_mut(); + let mut seen = self.seens.borrow_mut(); if seen.insert(public_key) { let sender = self.sender.clone(); diff --git a/crates/person/src/person.rs b/crates/person/src/person.rs index 0aea82f..ffa06ee 100644 --- a/crates/person/src/person.rs +++ b/crates/person/src/person.rs @@ -65,6 +65,21 @@ impl Person { } } + /// Build profile encryption keys announcement + pub fn with_announcement(mut self, announcement: Announcement) -> Self { + self.announcement = Some(announcement); + self + } + + /// Build profile messaging relays + pub fn with_messaging_relays(mut self, relays: I) -> Self + where + I: IntoIterator, + { + self.messaging_relays = relays.into_iter().collect(); + self + } + /// Get profile public key pub fn public_key(&self) -> PublicKey { self.public_key @@ -75,21 +90,11 @@ impl Person { self.metadata.clone() } - /// Set profile metadata - pub fn set_metadata(&mut self, metadata: Metadata) { - self.metadata = metadata; - } - /// Get profile encryption keys announcement pub fn announcement(&self) -> Option { self.announcement.clone() } - /// Set profile encryption keys announcement - pub fn set_announcement(&mut self, announcement: Announcement) { - self.announcement = Some(announcement); - } - /// Get profile messaging relays pub fn messaging_relays(&self) -> &Vec { &self.messaging_relays @@ -100,14 +105,6 @@ impl Person { self.messaging_relays.first().cloned() } - /// Set profile messaging relays - pub fn set_messaging_relays(&mut self, relays: I) - where - I: IntoIterator, - { - self.messaging_relays = relays.into_iter().collect(); - } - /// Get profile avatar pub fn avatar(&self) -> SharedString { self.metadata() @@ -115,8 +112,9 @@ impl Person { .as_ref() .filter(|picture| !picture.is_empty()) .map(|picture| { + let encoded_picture = urlencoding::encode(picture); let url = format!( - "{IMAGE_RESIZER}/?url={picture}&w=100&h=100&fit=cover&mask=circle&n=-1" + "{IMAGE_RESIZER}/?url={encoded_picture}&w=100&h=100&fit=cover&mask=circle&n=-1" ); url.into() }) @@ -139,6 +137,24 @@ impl Person { SharedString::from(shorten_pubkey(self.public_key(), 4)) } + + /// Set profile metadata + pub fn set_metadata(&mut self, metadata: Metadata) { + self.metadata = metadata; + } + + /// Set profile encryption keys announcement + pub fn set_announcement(&mut self, announcement: Announcement) { + self.announcement = Some(announcement); + } + + /// Set profile messaging relays + pub fn set_messaging_relays(&mut self, relays: I) + where + I: IntoIterator, + { + self.messaging_relays = relays.into_iter().collect(); + } } /// Shorten a [`PublicKey`] to a string with the first and last `len` characters @@ -148,7 +164,7 @@ pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> String { let Ok(pubkey) = public_key.to_bech32(); format!( - "{}:{}", + "{}...{}", &pubkey[0..(len + 1)], &pubkey[pubkey.len() - len..] ) diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index 3a30b3a..7bf7617 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -5,19 +5,19 @@ use std::hash::Hash; use std::rc::Rc; use std::sync::Arc; -use anyhow::{anyhow, Context as AnyhowContext, Error}; +use anyhow::{Context as AnyhowContext, Error, anyhow}; use gpui::{ App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled, Task, Window, }; use nostr_sdk::prelude::*; use settings::{AppSettings, AuthMode}; -use smallvec::{smallvec, SmallVec}; +use smallvec::{SmallVec, smallvec}; use state::NostrRegistry; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::notification::Notification; -use ui::{v_flex, Disableable, IconName, Sizable, WindowExtension}; +use ui::{Disableable, IconName, Sizable, WindowExtension, v_flex}; const AUTH_MESSAGE: &str = "Approve the authentication request to allow Coop to continue sending or receiving events."; @@ -34,7 +34,10 @@ struct AuthRequest { } impl AuthRequest { - pub fn new(challenge: impl Into, url: RelayUrl) -> Self { + pub fn new(challenge: S, url: RelayUrl) -> Self + where + S: Into, + { Self { challenge: challenge.into(), url, @@ -106,22 +109,6 @@ impl RelayAuth { tx.send_async(signal).await.ok(); } } - RelayMessage::Closed { - subscription_id, - message, - } => { - let msg = MachineReadablePrefix::parse(&message); - - if let Some(MachineReadablePrefix::AuthRequired) = msg { - if let Ok(Some(relay)) = client.relay(&relay_url).await { - // Send close message to relay - relay - .send_msg(ClientMessage::Close(subscription_id)) - .await - .ok(); - } - } - } RelayMessage::Ok { event_id, message, .. } => { @@ -330,7 +317,7 @@ impl RelayAuth { Notification::new() .custom_id(SharedString::from(&req.challenge)) .autohide(false) - .icon(IconName::Info) + .icon(IconName::Warning) .title(SharedString::from("Authentication Required")) .content(move |_window, cx| { v_flex() diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index 81736b5..1bcc8f5 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -10,6 +10,7 @@ common = { path = "../common" } nostr.workspace = true nostr-sdk.workspace = true nostr-lmdb.workspace = true +nostr-memory.workspace = true nostr-gossip-sqlite.workspace = true nostr-connect.workspace = true nostr-blossom.workspace = true diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 251a38c..8407d17 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -4,10 +4,11 @@ use std::time::Duration; use anyhow::{Context as AnyhowContext, Error, anyhow}; use common::config_dir; -use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, Window}; +use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window}; use nostr_connect::prelude::*; use nostr_gossip_sqlite::prelude::*; use nostr_lmdb::prelude::*; +use nostr_memory::prelude::*; use nostr_sdk::prelude::*; mod blossom; @@ -42,6 +43,21 @@ struct GlobalNostrRegistry(Entity); impl Global for GlobalNostrRegistry {} +/// Signer event. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum StateEvent { + /// Connecting to the bootstrapping relay + Connecting, + /// Connected to the bootstrapping relay + Connected, + /// User has not set up NIP-65 relays + RelayNotConfigured, + /// A new signer has been set + SignerSet, + /// An error occurred + Error(SharedString), +} + /// Nostr Registry #[derive(Debug)] pub struct NostrRegistry { @@ -57,15 +73,14 @@ pub struct NostrRegistry { /// App keys /// /// Used for Nostr Connect and NIP-4e operations - pub app_keys: Keys, - - /// Relay list state - pub relay_list_state: RelayState, + app_keys: Keys, /// Tasks for asynchronous operations tasks: Vec>>, } +impl EventEmitter for NostrRegistry {} + impl NostrRegistry { /// Retrieve the global nostr state pub fn global(cx: &App) -> Entity { @@ -93,25 +108,32 @@ impl NostrRegistry { .expect("Failed to initialize gossip instance") }); - // 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") - }); - - // Construct the nostr client - let client = ClientBuilder::default() + // Construct the nostr client builder + let mut builder = ClientBuilder::default() .signer(signer.clone()) .gossip(gossip) - .database(lmdb) .automatic_authentication(false) .verify_subscriptions(false) .connect_timeout(Duration::from_secs(TIMEOUT)) .sleep_when_idle(SleepWhenIdle::Enabled { timeout: Duration::from_secs(600), - }) - .build(); + }); + + // Add database if not in debug mode + if !cfg!(debug_assertions) { + // 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") + }); + builder = builder.database(lmdb); + } else { + builder = builder.database(MemoryDatabase::unbounded()) + } + + // Build the nostr client + let client = builder.build(); // Run at the end of current cycle cx.defer_in(window, |this, _window, cx| { @@ -123,7 +145,6 @@ impl NostrRegistry { signer, npubs, app_keys, - relay_list_state: RelayState::Idle, tasks: vec![], } } @@ -143,10 +164,18 @@ impl NostrRegistry { self.npubs.clone() } + /// Get the app keys + pub fn keys(&self) -> Keys { + self.app_keys.clone() + } + /// Connect to the bootstrapping relays fn connect(&mut self, cx: &mut Context) { let client = self.client(); + // Emit connecting event + cx.emit(StateEvent::Connecting); + self.tasks.push(cx.spawn(async move |this, cx| { cx.background_executor() .await_on_background(async move { @@ -167,6 +196,7 @@ impl NostrRegistry { // Update the state this.update(cx, |this, cx| { + cx.emit(StateEvent::Connected); this.get_npubs(cx); })?; @@ -177,6 +207,7 @@ impl NostrRegistry { /// Get all used npubs fn get_npubs(&mut self, cx: &mut Context) { let npubs = self.npubs.downgrade(); + let task: Task, Error>> = cx.background_spawn(async move { let dir = config_dir().join("keys"); // Ensure keys directory exists @@ -227,9 +258,8 @@ impl NostrRegistry { } }, Err(e) => { - log::error!("Failed to get npubs: {e}"); - this.update(cx, |this, cx| { - this.create_identity(cx); + this.update(cx, |_this, cx| { + cx.emit(StateEvent::Error(SharedString::from(e.to_string()))); })?; } } @@ -401,14 +431,13 @@ impl NostrRegistry { cx.notify(); } }); - // Emit signer changed event - cx.emit(SignerEvent::Set); + cx.emit(StateEvent::SignerSet); })?; } Err(e) => { this.update(cx, |_this, cx| { - cx.emit(SignerEvent::Error(e.to_string())); + cx.emit(StateEvent::Error(SharedString::from(e.to_string()))); })?; } } @@ -456,7 +485,7 @@ impl NostrRegistry { } Err(e) => { this.update(cx, |_this, cx| { - cx.emit(SignerEvent::Error(e.to_string())); + cx.emit(StateEvent::Error(SharedString::from(e.to_string()))); })?; } } @@ -495,14 +524,14 @@ impl NostrRegistry { } Err(e) => { this.update(cx, |_this, cx| { - cx.emit(SignerEvent::Error(e.to_string())); + cx.emit(StateEvent::Error(SharedString::from(e.to_string()))); })?; } } } Err(e) => { this.update(cx, |_this, cx| { - cx.emit(SignerEvent::Error(e.to_string())); + cx.emit(StateEvent::Error(SharedString::from(e.to_string()))); })?; } } @@ -666,8 +695,6 @@ impl NostrRegistry { } } -impl EventEmitter for NostrRegistry {} - /// Get or create a new app keys fn get_or_init_app_keys() -> Result { let dir = config_dir().join(".app_keys"); @@ -726,43 +753,6 @@ fn default_messaging_relays() -> Vec { ] } -/// Signer event. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum SignerEvent { - /// A new signer has been set - Set, - - /// An error occurred - Error(String), -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub enum RelayState { - #[default] - Idle, - Checking, - NotConfigured, - Configured, -} - -impl RelayState { - pub fn idle(&self) -> bool { - matches!(self, RelayState::Idle) - } - - pub fn checking(&self) -> bool { - matches!(self, RelayState::Checking) - } - - 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;