This commit is contained in:
2026-03-09 13:58:49 +07:00
parent aec32e450a
commit 39c04cabad
16 changed files with 475 additions and 554 deletions

19
Cargo.lock generated
View File

@@ -4299,6 +4299,17 @@ dependencies = [
"tracing", "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]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.44.1" version = "0.44.1"
@@ -4759,6 +4770,7 @@ dependencies = [
"smallvec", "smallvec",
"smol", "smol",
"state", "state",
"urlencoding",
] ]
[[package]] [[package]]
@@ -6434,6 +6446,7 @@ dependencies = [
"nostr-connect", "nostr-connect",
"nostr-gossip-sqlite", "nostr-gossip-sqlite",
"nostr-lmdb", "nostr-lmdb",
"nostr-memory",
"nostr-sdk", "nostr-sdk",
"petname", "petname",
"rustls", "rustls",
@@ -7403,6 +7416,12 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "usvg" name = "usvg"
version = "0.45.1" version = "0.45.1"

View File

@@ -20,6 +20,7 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" }
# Nostr # Nostr
nostr-lmdb = { git = "https://github.com/rust-nostr/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-connect = { git = "https://github.com/rust-nostr/nostr" }
nostr-blossom = { 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" } nostr-gossip-sqlite = { git = "https://github.com/rust-nostr/nostr" }

View File

@@ -10,11 +10,12 @@ use common::EventUtils;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::skim::SkimMatcherV2;
use gpui::{ 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 nostr_sdk::prelude::*;
use smallvec::{SmallVec, smallvec}; 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 message;
mod room; mod room;
@@ -39,6 +40,10 @@ pub enum ChatEvent {
CloseRoom(u64), CloseRoom(u64),
/// An event to notify UI about a new chat request /// An event to notify UI about a new chat request
Ping, Ping,
/// An event to notify UI that the chat registry has subscribed to messaging relays
Subscribed,
/// An error occurred
Error(SharedString),
} }
/// Channel signal. /// Channel signal.
@@ -48,41 +53,25 @@ enum Signal {
Message(NewMessage), Message(NewMessage),
/// Eose received from relay pool /// Eose received from relay pool
Eose, Eose,
} /// An error occurred
Error(SharedString),
/// Inbox state.
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum InboxState {
#[default]
Idle,
Checking,
RelayNotAvailable,
RelayConfigured(Box<Event>),
Subscribing,
}
impl InboxState {
pub fn not_configured(&self) -> bool {
matches!(self, InboxState::RelayNotAvailable)
}
pub fn subscribing(&self) -> bool {
matches!(self, InboxState::Subscribing)
}
} }
/// Chat Registry /// Chat Registry
#[derive(Debug)] #[derive(Debug)]
pub struct ChatRegistry { pub struct ChatRegistry {
/// Relay state for messaging relay list
state: Entity<InboxState>,
/// Collection of all chat rooms /// Collection of all chat rooms
rooms: Vec<Entity<Room>>, rooms: Vec<Entity<Room>>,
/// Tracking the status of unwrapping gift wrap events. /// Tracking the status of unwrapping gift wrap events.
tracking_flag: Arc<AtomicBool>, tracking_flag: Arc<AtomicBool>,
/// Channel for sending signals to the UI.
signal_tx: flume::Sender<Signal>,
/// Channel for receiving signals from the UI.
signal_rx: flume::Receiver<Signal>,
/// Async tasks /// Async tasks
tasks: SmallVec<[Task<Result<(), Error>>; 2]>, tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
@@ -105,36 +94,18 @@ impl ChatRegistry {
/// Create a new chat registry instance /// Create a new chat registry instance
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let state = cx.new(|_| InboxState::default());
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let (tx, rx) = flume::unbounded::<Signal>();
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
subscriptions.push( subscriptions.push(
// Observe the nip65 state and load chat rooms on every state change // Subscribe to the signer event
cx.observe(&nostr, |this, state, cx| { cx.subscribe(&nostr, |this, _state, event, cx| {
match state.read(cx).relay_list_state { if let StateEvent::SignerSet = event {
RelayState::Idle => {
this.reset(cx); 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); this.get_rooms(cx);
}), this.get_contact_list(cx);
); this.get_messages(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);
} }
}), }),
); );
@@ -147,9 +118,10 @@ impl ChatRegistry {
}); });
Self { Self {
state,
rooms: vec![], rooms: vec![],
tracking_flag: Arc::new(AtomicBool::new(false)), tracking_flag: Arc::new(AtomicBool::new(false)),
signal_rx: rx,
signal_tx: tx,
tasks: smallvec![], tasks: smallvec![],
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
@@ -167,7 +139,8 @@ impl ChatRegistry {
let sub_id2 = SubscriptionId::new(USER_GIFTWRAP); let sub_id2 = SubscriptionId::new(USER_GIFTWRAP);
// Channel for communication between nostr and gpui // Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Signal>(1024); let tx = self.signal_tx.clone();
let rx = self.signal_rx.clone();
self.tasks.push(cx.background_spawn(async move { self.tasks.push(cx.background_spawn(async move {
let device_signer = signer.get_encryption_signer().await; let device_signer = signer.get_encryption_signer().await;
@@ -194,7 +167,14 @@ impl ChatRegistry {
// Extract the rumor from the gift wrap event // Extract the rumor from the gift wrap event
match extract_rumor(&client, &device_signer, event.as_ref()).await { match extract_rumor(&client, &device_signer, event.as_ref()).await {
Ok(rumor) => match rumor.created_at >= initialized_at { 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?;
}
match rumor.created_at >= initialized_at {
true => { true => {
let new_message = NewMessage::new(event.id, rumor); let new_message = NewMessage::new(event.id, rumor);
let signal = Signal::Message(new_message); let signal = Signal::Message(new_message);
@@ -204,9 +184,12 @@ impl ChatRegistry {
false => { false => {
status.store(true, Ordering::Release); status.store(true, Ordering::Release);
} }
}, }
}
Err(e) => { 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); 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. /// Tracking the status of unwrapping gift wrap events.
fn tracking(&mut self, cx: &mut Context<Self>) { fn tracking(&mut self, cx: &mut Context<Self>) {
let status = self.tracking_flag.clone(); let status = self.tracking_flag.clone();
let tx = self.signal_tx.clone();
self.tasks.push(cx.background_spawn(async move { self.tasks.push(cx.background_spawn(async move {
let loop_duration = Duration::from_secs(15); let loop_duration = Duration::from_secs(15);
@@ -252,6 +241,9 @@ impl ChatRegistry {
loop { loop {
if status.load(Ordering::Acquire) { if status.load(Ordering::Acquire) {
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed); _ = 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; smol::Timer::after(loop_duration).await;
} }
@@ -289,27 +281,29 @@ impl ChatRegistry {
self.tasks.push(task); self.tasks.push(task);
} }
/// Ensure messaging relays are set up for the current user. /// Get all messages for current user
pub fn ensure_messaging_relays(&mut self, cx: &mut Context<Self>) { fn get_messages(&mut self, cx: &mut Context<Self>) {
let task = self.verify_relays(cx); let task = self.subscribe(cx);
// Set state to checking
self.set_state(InboxState::Checking, cx);
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
let result = task.await?; match task.await {
Ok(_) => {
// Update state this.update(cx, |_this, cx| {
this.update(cx, |this, cx| { cx.emit(ChatEvent::Subscribed);
this.set_state(result, cx);
})?; })?;
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(ChatEvent::Error(SharedString::from(e.to_string())));
})?;
}
}
Ok(()) Ok(())
})); }));
} }
// Verify messaging relay list for current user // Get messaging relay list for current user
fn verify_relays(&mut self, cx: &mut Context<Self>) -> Task<Result<InboxState, Error>> { fn get_messaging_relays(&self, cx: &App) -> Task<Result<Vec<RelayUrl>, Error>> {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
@@ -330,50 +324,25 @@ impl ChatRegistry {
.await?; .await?;
while let Some((_url, res)) = stream.next().await { while let Some((_url, res)) = stream.next().await {
match res { if let Ok(event) = res {
Ok(event) => { let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
return Ok(InboxState::RelayConfigured(Box::new(event))); return Ok(urls);
}
Err(e) => {
log::error!("Failed to receive relay list event: {e}");
}
} }
} }
Ok(InboxState::RelayNotAvailable) Err(anyhow!("Messaging Relays not found"))
}) })
} }
/// Get all messages for current user
fn get_messages<I>(&mut self, relay_urls: I, cx: &mut Context<Self>)
where
I: IntoIterator<Item = RelayUrl>,
{
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 /// Continuously get gift wrap events for the current user in their messaging relays
fn subscribe<I>(&mut self, urls: I, cx: &mut Context<Self>) -> Task<Result<(), Error>> fn subscribe(&self, cx: &App) -> Task<Result<(), Error>> {
where
I: IntoIterator<Item = RelayUrl>,
{
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let urls = urls.into_iter().collect::<Vec<_>>(); let urls = self.get_messaging_relays(cx);
cx.background_spawn(async move { cx.background_spawn(async move {
let urls = urls.await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(USER_GIFTWRAP); 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>) {
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 /// Get the loading status of the chat registry
pub fn loading(&self) -> bool { pub fn loading(&self) -> bool {
self.tracking_flag.load(Ordering::Acquire) self.tracking_flag.load(Ordering::Acquire)

View File

@@ -1,6 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, Error}; use anyhow::{Error, anyhow};
use chrono::{Local, TimeZone}; use chrono::{Local, TimeZone};
use gpui::{Image, ImageFormat, SharedString}; use gpui::{Image, ImageFormat, SharedString};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;

View File

@@ -1,17 +1,17 @@
use anyhow::Error; use anyhow::Error;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, px, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render,
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, div, px,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
use state::{NostrRegistry, SignerEvent}; use state::{NostrRegistry, StateEvent};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::indicator::Indicator; 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::connect::ConnectSigner;
use crate::dialogs::import::ImportKey; use crate::dialogs::import::ImportKey;
@@ -44,13 +44,14 @@ impl AccountSelector {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| { let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| {
match event { match event {
SignerEvent::Set => { StateEvent::SignerSet => {
window.close_all_modals(cx); window.close_all_modals(cx);
window.refresh(); window.refresh();
} }
SignerEvent::Error(e) => { StateEvent::Error(e) => {
this.set_error(e.to_string(), cx); this.set_error(e.to_string(), cx);
} }
_ => {}
}; };
}); });

View File

@@ -4,13 +4,13 @@ use std::time::Duration;
use common::TextUtils; use common::TextUtils;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, px, AppContext, Context, Entity, Image, IntoElement, ParentElement, Render, AppContext, Context, Entity, Image, IntoElement, ParentElement, Render, SharedString, Styled,
SharedString, Styled, Subscription, Window, Subscription, Window, div, img, px,
}; };
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use state::{ use state::{
CoopAuthUrlHandler, NostrRegistry, SignerEvent, CLIENT_NAME, NOSTR_CONNECT_RELAY, CLIENT_NAME, CoopAuthUrlHandler, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT, NostrRegistry,
NOSTR_CONNECT_TIMEOUT, StateEvent,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::v_flex; use ui::v_flex;
@@ -31,7 +31,7 @@ impl ConnectSigner {
let error = cx.new(|_| None); let error = cx.new(|_| None);
let nostr = NostrRegistry::global(cx); 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 timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap(); let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
@@ -55,7 +55,7 @@ impl ConnectSigner {
// Subscribe to the signer event // Subscribe to the signer event
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| { 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); this.set_error(e, cx);
} }
}); });

View File

@@ -1,18 +1,18 @@
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{Error, anyhow};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Task, Window, Subscription, Task, Window, div,
}; };
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{SmallVec, smallvec};
use state::{CoopAuthUrlHandler, NostrRegistry, SignerEvent}; use state::{CoopAuthUrlHandler, NostrRegistry, StateEvent};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::{v_flex, Disableable}; use ui::{Disableable, v_flex};
#[derive(Debug)] #[derive(Debug)]
pub struct ImportKey { pub struct ImportKey {
@@ -60,7 +60,7 @@ impl ImportKey {
subscriptions.push( subscriptions.push(
// Subscribe to the nostr signer event // Subscribe to the nostr signer event
cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| { 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); this.set_error(e, cx);
} }
}), }),
@@ -117,7 +117,7 @@ impl ImportKey {
}; };
let nostr = NostrRegistry::global(cx); 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); let timeout = Duration::from_secs(30);
// Construct the nostr connect signer // Construct the nostr connect signer

View File

@@ -1,17 +1,15 @@
use chat::{ChatRegistry, InboxState};
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Window, IntoElement, ParentElement, Render, SharedString, Styled, Window, div, svg,
}; };
use state::{NostrRegistry, RelayState}; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::dock::DockPlacement; use ui::dock_area::dock::DockPlacement;
use ui::dock_area::panel::{Panel, PanelEvent}; 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; use crate::workspace::Workspace;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
@@ -82,15 +80,6 @@ impl Render for GreeterPanel {
const TITLE: &str = "Welcome to Coop!"; const TITLE: &str = "Welcome to Coop!";
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr."; 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() h_flex()
.size_full() .size_full()
.items_center() .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( .child(
v_flex() v_flex()
.gap_2() .gap_2()

View File

@@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use ::settings::AppSettings; use ::settings::AppSettings;
use chat::{ChatEvent, ChatRegistry, InboxState}; use chat::{ChatEvent, ChatRegistry};
use device::DeviceRegistry; use device::DeviceRegistry;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
@@ -11,7 +11,7 @@ use gpui::{
use person::PersonRegistry; use person::PersonRegistry;
use serde::Deserialize; use serde::Deserialize;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
use state::{NostrRegistry, RelayState, SignerEvent}; use state::{NostrRegistry, StateEvent};
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry}; use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
use title_bar::TitleBar; use title_bar::TitleBar;
use ui::avatar::Avatar; use ui::avatar::Avatar;
@@ -96,7 +96,7 @@ impl Workspace {
subscriptions.push( subscriptions.push(
// Subscribe to the signer events // Subscribe to the signer events
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| { 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); this.set_center_layout(window, cx);
} }
}), }),
@@ -295,7 +295,7 @@ impl Workspace {
Command::RefreshMessagingRelays => { Command::RefreshMessagingRelays => {
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
chat.update(cx, |this, cx| { chat.update(cx, |this, cx| {
this.ensure_messaging_relays(cx); //this.ensure_messaging_relays(cx);
}); });
} }
Command::ToggleTheme => { Command::ToggleTheme => {
@@ -534,7 +534,6 @@ impl Workspace {
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let inbox_state = chat.read(cx).state(cx);
let Some(pkey) = signer.public_key() else { let Some(pkey) = signer.public_key() else {
return div(); return div();
@@ -584,6 +583,7 @@ impl Workspace {
) )
}), }),
) )
/*
.child( .child(
h_flex() h_flex()
.gap_2() .gap_2()
@@ -719,7 +719,7 @@ impl Workspace {
) )
}), }),
), ),
) ) */
} }
} }

View File

@@ -5,15 +5,12 @@ use std::time::Duration;
use anyhow::{Context as AnyhowContext, Error, anyhow}; use anyhow::{Context as AnyhowContext, Error, anyhow};
use gpui::{ use gpui::{
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled, App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement,
Subscription, Task, Window, div, SharedString, Styled, Task, Window, div,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
use smallvec::{SmallVec, smallvec}; use state::{Announcement, DEVICE_GIFTWRAP, DeviceState, NostrRegistry, TIMEOUT, app_name};
use state::{
Announcement, DEVICE_GIFTWRAP, DeviceState, NostrRegistry, RelayState, TIMEOUT, app_name,
};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
@@ -32,6 +29,15 @@ struct GlobalDeviceRegistry(Entity<DeviceRegistry>);
impl Global for GlobalDeviceRegistry {} 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 /// Device Registry
/// ///
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
@@ -42,11 +48,10 @@ pub struct DeviceRegistry {
/// Async tasks /// Async tasks
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<Result<(), Error>>>,
/// Subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
} }
impl EventEmitter<DeviceEvent> for DeviceRegistry {}
impl DeviceRegistry { impl DeviceRegistry {
/// Retrieve the global device registry state /// Retrieve the global device registry state
pub fn global(cx: &App) -> Entity<Self> { pub fn global(cx: &App) -> Entity<Self> {
@@ -60,27 +65,16 @@ impl DeviceRegistry {
/// Create a new device registry instance /// Create a new device registry instance
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx); let state = DeviceState::default();
let mut subscriptions = smallvec![];
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| { cx.defer_in(window, |this, window, cx| {
this.handle_notifications(window, cx); this.handle_notifications(window, cx);
this.get_announcement(cx);
}); });
Self { Self {
state: DeviceState::default(), state,
tasks: vec![], tasks: vec![],
_subscriptions: subscriptions,
} }
} }
@@ -123,9 +117,7 @@ impl DeviceRegistry {
Ok(()) Ok(())
})); }));
self.tasks.push( self.tasks.push(cx.spawn_in(window, async move |this, cx| {
// Update GPUI states
cx.spawn_in(window, async move |this, cx| {
while let Ok(event) = rx.recv_async().await { while let Ok(event) = rx.recv_async().await {
match event.kind { match event.kind {
// New request event // New request event
@@ -145,8 +137,7 @@ impl DeviceRegistry {
} }
Ok(()) Ok(())
}), }));
);
} }
/// Get the device state /// Get the device state
@@ -191,45 +182,68 @@ impl DeviceRegistry {
fn get_messages(&mut self, cx: &mut Context<Self>) { fn get_messages(&mut self, cx: &mut Context<Self>) {
let task = self.subscribe_to_giftwrap_events(cx); let task = self.subscribe_to_giftwrap_events(cx);
self.tasks.push(cx.spawn(async move |_this, _cx| { self.tasks.push(cx.spawn(async move |this, cx| {
task.await?; if let Err(e) = task.await {
this.update(cx, |_this, cx| {
// Update state cx.emit(DeviceEvent::Error(SharedString::from(e.to_string())));
})?;
}
Ok(()) Ok(())
})); }));
} }
/// Continuously get gift wrap events for the current user in their messaging relays /// Get the messaging relays for the current user
fn subscribe_to_giftwrap_events(&mut self, cx: &mut Context<Self>) -> Task<Result<(), Error>> { fn get_user_messaging_relays(&self, cx: &App) -> Task<Result<Vec<RelayUrl>, Error>> {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else { cx.background_spawn(async move {
return Task::ready(Err(anyhow!("User not found"))); 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); if let Some(event) = client.database().query(filter).await?.first_owned() {
let profile = persons.read(cx).get(&public_key, cx); // Extract relay URLs from the event
let relay_urls = profile.messaging_relays().clone(); let urls: Vec<RelayUrl> = 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<Result<(), Error>> {
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 { 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 filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(DEVICE_GIFTWRAP); let id = SubscriptionId::new(DEVICE_GIFTWRAP);
// Construct target for subscription // Construct target for subscription
let target: HashMap<RelayUrl, Filter> = relay_urls let target: HashMap<RelayUrl, Filter> = urls
.into_iter() .into_iter()
.map(|relay| (relay, filter.clone())) .map(|relay| (relay, filter.clone()))
.collect(); .collect();
let output = client.subscribe(target).with_id(id).await?; // Subscribe
client.subscribe(target).with_id(id).await?;
log::info!(
"Successfully subscribed to encryption gift-wrap messages on: {:?}",
output.success
);
Ok(()) Ok(())
}) })
@@ -239,16 +253,14 @@ impl DeviceRegistry {
pub fn get_announcement(&mut self, cx: &mut Context<Self>) { pub fn get_announcement(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); 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 // Reset state before fetching announcement
self.reset(cx); self.reset(cx);
let task: Task<Result<Event, Error>> = cx.background_spawn(async move { let task: Task<Result<Event, Error>> = cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
// Construct the filter for the device announcement event // Construct the filter for the device announcement event
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::Custom(10044)) .kind(Kind::Custom(10044))
@@ -262,18 +274,12 @@ impl DeviceRegistry {
.await?; .await?;
while let Some((_url, res)) = stream.next().await { while let Some((_url, res)) = stream.next().await {
match res { if let Ok(event) = res {
Ok(event) => {
log::info!("Received device announcement event: {event:?}");
return Ok(event); return Ok(event);
} }
Err(e) => {
log::error!("Failed to receive device announcement event: {e}");
}
}
} }
Err(anyhow!("Device announcement not found")) Err(anyhow!("Announcement not found"))
}); });
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
@@ -436,7 +442,7 @@ impl DeviceRegistry {
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); 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 app_pubkey = app_keys.public_key();
let task: Task<Result<Option<Keys>, Error>> = cx.background_spawn(async move { let task: Task<Result<Option<Keys>, Error>> = cx.background_spawn(async move {
@@ -506,7 +512,7 @@ impl DeviceRegistry {
/// Parse the response event for device keys from other devices /// Parse the response event for device keys from other devices
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) { fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let app_keys = nostr.read(cx).app_keys.clone(); let app_keys = nostr.read(cx).keys();
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move { let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
let root_device = event let root_device = event

View File

@@ -15,3 +15,4 @@ smallvec.workspace = true
smol.workspace = true smol.workspace = true
flume.workspace = true flume.workspace = true
log.workspace = true log.workspace = true
urlencoding = "2.1.3"

View File

@@ -3,12 +3,12 @@ use std::collections::{HashMap, HashSet};
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{Error, anyhow};
use common::EventUtils; use common::EventUtils;
use gpui::{App, AppContext, Context, Entity, Global, Task}; use gpui::{App, AppContext, Context, Entity, Global, Task};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{SmallVec, smallvec};
use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT}; use state::{Announcement, BOOTSTRAP_RELAYS, NostrRegistry, TIMEOUT};
mod person; mod person;
@@ -36,7 +36,7 @@ pub struct PersonRegistry {
persons: HashMap<PublicKey, Entity<Person>>, persons: HashMap<PublicKey, Entity<Person>>,
/// Set of public keys that have been seen /// Set of public keys that have been seen
seen: Rc<RefCell<HashSet<PublicKey>>>, seens: Rc<RefCell<HashSet<PublicKey>>>,
/// Sender for requesting metadata /// Sender for requesting metadata
sender: flume::Sender<PublicKey>, sender: flume::Sender<PublicKey>,
@@ -63,7 +63,7 @@ impl PersonRegistry {
// Channel for communication between nostr and gpui // Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Dispatch>(100); let (tx, rx) = flume::bounded::<Dispatch>(100);
let (mta_tx, mta_rx) = flume::bounded::<PublicKey>(100); let (mta_tx, mta_rx) = flume::unbounded::<PublicKey>();
let mut tasks = smallvec![]; let mut tasks = smallvec![];
@@ -135,7 +135,7 @@ impl PersonRegistry {
Self { Self {
persons: HashMap::new(), persons: HashMap::new(),
seen: Rc::new(RefCell::new(HashSet::new())), seens: Rc::new(RefCell::new(HashSet::new())),
sender: mta_tx, sender: mta_tx,
_tasks: tasks, _tasks: tasks,
} }
@@ -208,40 +208,52 @@ impl PersonRegistry {
} }
} }
_ => { _ => {
if !batch.is_empty() {
get_metadata(client, std::mem::take(&mut batch)).await.ok(); get_metadata(client, std::mem::take(&mut batch)).await.ok();
} }
} }
} }
} }
}
/// Set profile encryption keys announcement /// Set profile encryption keys announcement
fn set_announcement(&mut self, event: &Event, cx: &mut App) { 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.update(cx, |person, cx| {
person.set_announcement(announcement); person.set_announcement(announcement);
cx.notify(); cx.notify();
}); });
} else {
let person =
Person::new(event.pubkey, Metadata::default()).with_announcement(announcement);
self.insert(person, cx);
} }
} }
/// Set messaging relays for a person /// Set messaging relays for a person
fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) { fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) {
if let Some(person) = self.persons.get(&event.pubkey) {
let urls: Vec<RelayUrl> = nip17::extract_relay_list(event).cloned().collect(); let urls: Vec<RelayUrl> = nip17::extract_relay_list(event).cloned().collect();
if let Some(person) = self.persons.get(&event.pubkey) {
person.update(cx, |person, cx| { person.update(cx, |person, cx| {
person.set_messaging_relays(urls); person.set_messaging_relays(urls);
cx.notify(); cx.notify();
}); });
} else {
let person = Person::new(event.pubkey, Metadata::default()).with_messaging_relays(urls);
self.insert(person, cx);
} }
} }
/// Insert batch of persons /// Insert batch of persons
fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) { fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
for person in persons.into_iter() { 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(); cx.notify();
} }
@@ -270,7 +282,7 @@ impl PersonRegistry {
} }
let public_key = *public_key; let public_key = *public_key;
let mut seen = self.seen.borrow_mut(); let mut seen = self.seens.borrow_mut();
if seen.insert(public_key) { if seen.insert(public_key) {
let sender = self.sender.clone(); let sender = self.sender.clone();

View File

@@ -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<I>(mut self, relays: I) -> Self
where
I: IntoIterator<Item = RelayUrl>,
{
self.messaging_relays = relays.into_iter().collect();
self
}
/// Get profile public key /// Get profile public key
pub fn public_key(&self) -> PublicKey { pub fn public_key(&self) -> PublicKey {
self.public_key self.public_key
@@ -75,21 +90,11 @@ impl Person {
self.metadata.clone() self.metadata.clone()
} }
/// Set profile metadata
pub fn set_metadata(&mut self, metadata: Metadata) {
self.metadata = metadata;
}
/// Get profile encryption keys announcement /// Get profile encryption keys announcement
pub fn announcement(&self) -> Option<Announcement> { pub fn announcement(&self) -> Option<Announcement> {
self.announcement.clone() self.announcement.clone()
} }
/// Set profile encryption keys announcement
pub fn set_announcement(&mut self, announcement: Announcement) {
self.announcement = Some(announcement);
}
/// Get profile messaging relays /// Get profile messaging relays
pub fn messaging_relays(&self) -> &Vec<RelayUrl> { pub fn messaging_relays(&self) -> &Vec<RelayUrl> {
&self.messaging_relays &self.messaging_relays
@@ -100,14 +105,6 @@ impl Person {
self.messaging_relays.first().cloned() self.messaging_relays.first().cloned()
} }
/// Set profile messaging relays
pub fn set_messaging_relays<I>(&mut self, relays: I)
where
I: IntoIterator<Item = RelayUrl>,
{
self.messaging_relays = relays.into_iter().collect();
}
/// Get profile avatar /// Get profile avatar
pub fn avatar(&self) -> SharedString { pub fn avatar(&self) -> SharedString {
self.metadata() self.metadata()
@@ -115,8 +112,9 @@ impl Person {
.as_ref() .as_ref()
.filter(|picture| !picture.is_empty()) .filter(|picture| !picture.is_empty())
.map(|picture| { .map(|picture| {
let encoded_picture = urlencoding::encode(picture);
let url = format!( 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() url.into()
}) })
@@ -139,6 +137,24 @@ impl Person {
SharedString::from(shorten_pubkey(self.public_key(), 4)) 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<I>(&mut self, relays: I)
where
I: IntoIterator<Item = RelayUrl>,
{
self.messaging_relays = relays.into_iter().collect();
}
} }
/// Shorten a [`PublicKey`] to a string with the first and last `len` characters /// 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(); let Ok(pubkey) = public_key.to_bech32();
format!( format!(
"{}:{}", "{}...{}",
&pubkey[0..(len + 1)], &pubkey[0..(len + 1)],
&pubkey[pubkey.len() - len..] &pubkey[pubkey.len() - len..]
) )

View File

@@ -5,19 +5,19 @@ use std::hash::Hash;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, Context as AnyhowContext, Error}; use anyhow::{Context as AnyhowContext, Error, anyhow};
use gpui::{ use gpui::{
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled, App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
Task, Window, Task, Window,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use settings::{AppSettings, AuthMode}; use settings::{AppSettings, AuthMode};
use smallvec::{smallvec, SmallVec}; use smallvec::{SmallVec, smallvec};
use state::NostrRegistry; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::notification::Notification; use ui::notification::Notification;
use ui::{v_flex, Disableable, IconName, Sizable, WindowExtension}; use ui::{Disableable, IconName, Sizable, WindowExtension, v_flex};
const AUTH_MESSAGE: &str = const AUTH_MESSAGE: &str =
"Approve the authentication request to allow Coop to continue sending or receiving events."; "Approve the authentication request to allow Coop to continue sending or receiving events.";
@@ -34,7 +34,10 @@ struct AuthRequest {
} }
impl AuthRequest { impl AuthRequest {
pub fn new(challenge: impl Into<String>, url: RelayUrl) -> Self { pub fn new<S>(challenge: S, url: RelayUrl) -> Self
where
S: Into<String>,
{
Self { Self {
challenge: challenge.into(), challenge: challenge.into(),
url, url,
@@ -106,22 +109,6 @@ impl RelayAuth {
tx.send_async(signal).await.ok(); 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 { RelayMessage::Ok {
event_id, message, .. event_id, message, ..
} => { } => {
@@ -330,7 +317,7 @@ impl RelayAuth {
Notification::new() Notification::new()
.custom_id(SharedString::from(&req.challenge)) .custom_id(SharedString::from(&req.challenge))
.autohide(false) .autohide(false)
.icon(IconName::Info) .icon(IconName::Warning)
.title(SharedString::from("Authentication Required")) .title(SharedString::from("Authentication Required"))
.content(move |_window, cx| { .content(move |_window, cx| {
v_flex() v_flex()

View File

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

View File

@@ -4,10 +4,11 @@ use std::time::Duration;
use anyhow::{Context as AnyhowContext, Error, anyhow}; use anyhow::{Context as AnyhowContext, Error, anyhow};
use common::config_dir; 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_connect::prelude::*;
use nostr_gossip_sqlite::prelude::*; use nostr_gossip_sqlite::prelude::*;
use nostr_lmdb::prelude::*; use nostr_lmdb::prelude::*;
use nostr_memory::prelude::*;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
mod blossom; mod blossom;
@@ -42,6 +43,21 @@ struct GlobalNostrRegistry(Entity<NostrRegistry>);
impl Global for GlobalNostrRegistry {} 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 /// Nostr Registry
#[derive(Debug)] #[derive(Debug)]
pub struct NostrRegistry { pub struct NostrRegistry {
@@ -57,15 +73,14 @@ pub struct NostrRegistry {
/// App keys /// App keys
/// ///
/// Used for Nostr Connect and NIP-4e operations /// Used for Nostr Connect and NIP-4e operations
pub app_keys: Keys, app_keys: Keys,
/// Relay list state
pub relay_list_state: RelayState,
/// Tasks for asynchronous operations /// Tasks for asynchronous operations
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<Result<(), Error>>>,
} }
impl EventEmitter<StateEvent> for NostrRegistry {}
impl NostrRegistry { impl NostrRegistry {
/// Retrieve the global nostr state /// Retrieve the global nostr state
pub fn global(cx: &App) -> Entity<Self> { pub fn global(cx: &App) -> Entity<Self> {
@@ -93,25 +108,32 @@ impl NostrRegistry {
.expect("Failed to initialize gossip instance") .expect("Failed to initialize gossip instance")
}); });
// Construct the nostr client builder
let mut builder = ClientBuilder::default()
.signer(signer.clone())
.gossip(gossip)
.automatic_authentication(false)
.verify_subscriptions(false)
.connect_timeout(Duration::from_secs(TIMEOUT))
.sleep_when_idle(SleepWhenIdle::Enabled {
timeout: Duration::from_secs(600),
});
// Add database if not in debug mode
if !cfg!(debug_assertions) {
// Construct the nostr lmdb instance // Construct the nostr lmdb instance
let lmdb = cx.foreground_executor().block_on(async move { let lmdb = cx.foreground_executor().block_on(async move {
NostrLmdb::open(config_dir().join("nostr")) NostrLmdb::open(config_dir().join("nostr"))
.await .await
.expect("Failed to initialize database") .expect("Failed to initialize database")
}); });
builder = builder.database(lmdb);
} else {
builder = builder.database(MemoryDatabase::unbounded())
}
// Construct the nostr client // Build the nostr client
let client = ClientBuilder::default() let client = builder.build();
.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();
// Run at the end of current cycle // Run at the end of current cycle
cx.defer_in(window, |this, _window, cx| { cx.defer_in(window, |this, _window, cx| {
@@ -123,7 +145,6 @@ impl NostrRegistry {
signer, signer,
npubs, npubs,
app_keys, app_keys,
relay_list_state: RelayState::Idle,
tasks: vec![], tasks: vec![],
} }
} }
@@ -143,10 +164,18 @@ impl NostrRegistry {
self.npubs.clone() self.npubs.clone()
} }
/// Get the app keys
pub fn keys(&self) -> Keys {
self.app_keys.clone()
}
/// Connect to the bootstrapping relays /// Connect to the bootstrapping relays
fn connect(&mut self, cx: &mut Context<Self>) { fn connect(&mut self, cx: &mut Context<Self>) {
let client = self.client(); let client = self.client();
// Emit connecting event
cx.emit(StateEvent::Connecting);
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
cx.background_executor() cx.background_executor()
.await_on_background(async move { .await_on_background(async move {
@@ -167,6 +196,7 @@ impl NostrRegistry {
// Update the state // Update the state
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
cx.emit(StateEvent::Connected);
this.get_npubs(cx); this.get_npubs(cx);
})?; })?;
@@ -177,6 +207,7 @@ impl NostrRegistry {
/// Get all used npubs /// Get all used npubs
fn get_npubs(&mut self, cx: &mut Context<Self>) { fn get_npubs(&mut self, cx: &mut Context<Self>) {
let npubs = self.npubs.downgrade(); let npubs = self.npubs.downgrade();
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move { let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
let dir = config_dir().join("keys"); let dir = config_dir().join("keys");
// Ensure keys directory exists // Ensure keys directory exists
@@ -227,9 +258,8 @@ impl NostrRegistry {
} }
}, },
Err(e) => { Err(e) => {
log::error!("Failed to get npubs: {e}"); this.update(cx, |_this, cx| {
this.update(cx, |this, cx| { cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
this.create_identity(cx);
})?; })?;
} }
} }
@@ -401,14 +431,13 @@ impl NostrRegistry {
cx.notify(); cx.notify();
} }
}); });
// Emit signer changed event // Emit signer changed event
cx.emit(SignerEvent::Set); cx.emit(StateEvent::SignerSet);
})?; })?;
} }
Err(e) => { Err(e) => {
this.update(cx, |_this, cx| { 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) => { Err(e) => {
this.update(cx, |_this, cx| { 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) => { Err(e) => {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(SignerEvent::Error(e.to_string())); cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
})?; })?;
} }
} }
} }
Err(e) => { Err(e) => {
this.update(cx, |_this, cx| { 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<SignerEvent> for NostrRegistry {}
/// Get or create a new app keys /// Get or create a new app keys
fn get_or_init_app_keys() -> Result<Keys, Error> { fn get_or_init_app_keys() -> Result<Keys, Error> {
let dir = config_dir().join(".app_keys"); let dir = config_dir().join(".app_keys");
@@ -726,43 +753,6 @@ fn default_messaging_relays() -> Vec<RelayUrl> {
] ]
} }
/// 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)] #[derive(Debug, Clone)]
pub struct CoopAuthUrlHandler; pub struct CoopAuthUrlHandler;