clean up
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m24s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m26s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled

This commit is contained in:
2026-02-22 16:54:51 +07:00
parent 67ccfcb132
commit 31df6d7937
13 changed files with 148 additions and 176 deletions

2
Cargo.lock generated
View File

@@ -1658,6 +1658,7 @@ dependencies = [
"itertools 0.13.0", "itertools 0.13.0",
"log", "log",
"nostr-sdk", "nostr-sdk",
"person",
"serde", "serde",
"serde_json", "serde_json",
"smallvec", "smallvec",
@@ -4658,7 +4659,6 @@ version = "0.3.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common", "common",
"device",
"flume", "flume",
"gpui", "gpui",
"log", "log",

View File

@@ -50,11 +50,28 @@ enum Signal {
Eose, Eose,
} }
/// 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)
}
}
/// Chat Registry /// Chat Registry
#[derive(Debug)] #[derive(Debug)]
pub struct ChatRegistry { pub struct ChatRegistry {
/// Relay state for messaging relay list /// Relay state for messaging relay list
messaging_relay_list: Entity<RelayState>, state: Entity<InboxState>,
/// Collection of all chat rooms /// Collection of all chat rooms
rooms: Vec<Entity<Room>>, rooms: Vec<Entity<Room>>,
@@ -84,7 +101,7 @@ 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 messaging_relay_list = cx.new(|_| RelayState::default()); let state = cx.new(|_| InboxState::default());
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
@@ -106,26 +123,28 @@ impl ChatRegistry {
subscriptions.push( subscriptions.push(
// Observe the nip17 state and load chat rooms on every state change // Observe the nip17 state and load chat rooms on every state change
cx.observe(&messaging_relay_list, |this, state, cx| { cx.observe(&state, |this, state, cx| {
match state.read(cx) { if let InboxState::RelayConfigured(event) = state.read(cx) {
RelayState::Configured => { let relay_urls: Vec<_> = nip17::extract_relay_list(event).cloned().collect();
this.get_messages(cx); this.get_messages(relay_urls, cx);
}
_ => {
this.get_rooms(cx);
}
} }
}), }),
); );
// Run at the end of current cycle // Run at the end of the current cycle
cx.defer_in(window, |this, _window, cx| { cx.defer_in(window, |this, _window, cx| {
// Handle nostr notifications
this.handle_notifications(cx); this.handle_notifications(cx);
// Track unwrap gift wrap progress
this.tracking(cx); this.tracking(cx);
// Load chat rooms
this.get_rooms(cx);
}); });
Self { Self {
messaging_relay_list, state,
rooms: vec![], rooms: vec![],
tracking_flag: Arc::new(AtomicBool::new(false)), tracking_flag: Arc::new(AtomicBool::new(false)),
tasks: smallvec![], tasks: smallvec![],
@@ -170,8 +189,6 @@ impl ChatRegistry {
continue; continue;
} }
log::info!("Received gift wrap event: {:?}", event);
// Extract the rumor from the gift wrap event // Extract the rumor from the gift wrap event
match Self::extract_rumor(&client, &device_signer, event.as_ref()).await { match Self::extract_rumor(&client, &device_signer, event.as_ref()).await {
Ok(rumor) => match rumor.created_at >= initialized_at { Ok(rumor) => match rumor.created_at >= initialized_at {
@@ -238,17 +255,19 @@ impl ChatRegistry {
})); }));
} }
/// Ensure messaging relays are set up for the current user.
fn ensure_messaging_relays(&mut self, cx: &mut Context<Self>) { fn ensure_messaging_relays(&mut self, cx: &mut Context<Self>) {
let state = self.messaging_relay_list.downgrade();
let task = self.verify_relays(cx); let task = self.verify_relays(cx);
self.tasks.push(cx.spawn(async move |_this, cx| { // Set state to checking
self.set_state(InboxState::Checking, cx);
self.tasks.push(cx.spawn(async move |this, cx| {
let result = task.await?; let result = task.await?;
// Update state // Update state
state.update(cx, |this, cx| { this.update(cx, |this, cx| {
*this = result; this.set_state(result, cx);
cx.notify();
})?; })?;
Ok(()) Ok(())
@@ -256,13 +275,12 @@ impl ChatRegistry {
} }
// Verify messaging relay list for current user // Verify messaging relay list for current user
fn verify_relays(&mut self, cx: &mut Context<Self>) -> Task<Result<RelayState, Error>> { fn verify_relays(&mut self, cx: &mut Context<Self>) -> Task<Result<InboxState, 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 public_key = signer.public_key().unwrap(); let public_key = signer.public_key().unwrap();
let write_relays = nostr.read(cx).write_relays(&public_key, cx); let write_relays = nostr.read(cx).write_relays(&public_key, cx);
cx.background_spawn(async move { cx.background_spawn(async move {
@@ -287,8 +305,7 @@ impl ChatRegistry {
while let Some((_url, res)) = stream.next().await { while let Some((_url, res)) = stream.next().await {
match res { match res {
Ok(event) => { Ok(event) => {
log::info!("Received relay list event: {event:?}"); return Ok(InboxState::RelayConfigured(Box::new(event)));
return Ok(RelayState::Configured);
} }
Err(e) => { Err(e) => {
log::error!("Failed to receive relay list event: {e}"); log::error!("Failed to receive relay list event: {e}");
@@ -296,41 +313,54 @@ impl ChatRegistry {
} }
} }
Ok(RelayState::NotConfigured) Ok(InboxState::RelayNotAvailable)
}) })
} }
/// Get all messages for current user /// Get all messages for current user
fn get_messages(&mut self, cx: &mut Context<Self>) { fn get_messages<I>(&mut self, relay_urls: I, cx: &mut Context<Self>)
let task = self.subscribe_to_giftwrap_events(cx); where
I: IntoIterator<Item = RelayUrl>,
{
let task = self.subscribe(relay_urls, cx);
self.tasks.push(cx.spawn(async move |_this, _cx| { self.tasks.push(cx.spawn(async move |this, cx| {
task.await?; task.await?;
// Update state // Update state
this.update(cx, |this, cx| {
this.set_state(InboxState::Subscribing, cx);
})?;
Ok(()) 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_to_giftwrap_events(&mut self, cx: &mut Context<Self>) -> Task<Result<(), Error>> { fn subscribe<I>(&mut self, urls: I, cx: &mut Context<Self>) -> 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 public_key = signer.public_key().unwrap(); let urls = urls.into_iter().collect::<Vec<_>>();
let messaging_relays = nostr.read(cx).messaging_relays(&public_key, cx);
cx.background_spawn(async move { cx.background_spawn(async move {
let urls = messaging_relays.await; let public_key = signer.get_public_key().await?;
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(USER_GIFTWRAP); let id = SubscriptionId::new(USER_GIFTWRAP);
// Ensure relay connections
for url in urls.iter() {
client.add_relay(url).and_connect().await?;
}
// Construct target for subscription // Construct target for subscription
let target: HashMap<&RelayUrl, Filter> = let target: HashMap<RelayUrl, Filter> = urls
urls.iter().map(|relay| (relay, filter.clone())).collect(); .into_iter()
.map(|relay| (relay, filter.clone()))
.collect();
let output = client.subscribe(target).with_id(id).await?; let output = client.subscribe(target).with_id(id).await?;
@@ -343,9 +373,17 @@ impl ChatRegistry {
}) })
} }
/// Set the state of the inbox
fn set_state(&mut self, state: InboxState, cx: &mut Context<Self>) {
self.state.update(cx, |this, cx| {
*this = state;
cx.notify();
});
}
/// Get the relay state /// Get the relay state
pub fn relay_state(&self, cx: &App) -> RelayState { pub fn state(&self, cx: &App) -> InboxState {
self.messaging_relay_list.read(cx).clone() self.state.read(cx).clone()
} }
/// Get the loading status of the chat registry /// Get the loading status of the chat registry
@@ -491,16 +529,21 @@ impl ChatRegistry {
pub fn get_rooms(&mut self, cx: &mut Context<Self>) { pub fn get_rooms(&mut self, cx: &mut Context<Self>) {
let task = self.get_rooms_from_database(cx); let task = self.get_rooms_from_database(cx);
cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
let rooms = task.await.ok()?; match task.await {
Ok(rooms) => {
this.update(cx, move |this, cx| { this.update(cx, |this, cx| {
this.extend_rooms(rooms, cx); this.extend_rooms(rooms, cx);
this.sort(cx); this.sort(cx);
}) })?;
.ok() }
}) Err(e) => {
.detach(); log::error!("Failed to load rooms: {}", e);
}
};
Ok(())
}));
} }
/// Create a task to load rooms from the database /// Create a task to load rooms from the database

View File

@@ -1216,8 +1216,7 @@ impl ChatPanel {
let encryption = matches!(signer_kind, SignerKind::Encryption); let encryption = matches!(signer_kind, SignerKind::Encryption);
let user = matches!(signer_kind, SignerKind::User); let user = matches!(signer_kind, SignerKind::User);
this.check_side(ui::Side::Right) this.menu_with_check_and_disabled(
.menu_with_check_and_disabled(
"Auto", "Auto",
auto, auto,
Box::new(Command::ChangeSigner(SignerKind::Auto)), Box::new(Command::ChangeSigner(SignerKind::Auto)),
@@ -1339,8 +1338,8 @@ impl Render for ChatPanel {
h_flex() h_flex()
.pl_1() .pl_1()
.gap_1() .gap_1()
.child(self.render_encryption_menu(window, cx))
.child(self.render_emoji_menu(window, cx)) .child(self.render_emoji_menu(window, cx))
.child(self.render_encryption_menu(window, cx))
.child( .child(
Button::new("send") Button::new("send")
.icon(IconName::PaperPlaneFill) .icon(IconName::PaperPlaneFill)

View File

@@ -1,4 +1,4 @@
use chat::ChatRegistry; use chat::{ChatRegistry, InboxState};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, div, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
@@ -83,15 +83,16 @@ impl Render for GreeterPanel {
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr."; const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let nip17_state = chat.read(cx).relay_state(cx); let nip17 = chat.read(cx).state(cx);
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let nip65_state = nostr.read(cx).relay_list_state(); let nip65 = nostr.read(cx).relay_list_state();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let owned = signer.owned(); let owned = signer.owned();
let required_actions = let required_actions =
nip65_state == RelayState::NotConfigured || nip17_state == RelayState::NotConfigured; nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable;
h_flex() h_flex()
.size_full() .size_full()
@@ -152,7 +153,7 @@ impl Render for GreeterPanel {
v_flex() v_flex()
.gap_2() .gap_2()
.w_full() .w_full()
.when(nip65_state.not_configured(), |this| { .when(nip65.not_configured(), |this| {
this.child( this.child(
Button::new("relaylist") Button::new("relaylist")
.icon(Icon::new(IconName::Relay)) .icon(Icon::new(IconName::Relay))
@@ -170,7 +171,7 @@ impl Render for GreeterPanel {
}), }),
) )
}) })
.when(nip17_state.not_configured(), |this| { .when(nip17.not_configured(), |this| {
this.child( this.child(
Button::new("import") Button::new("import")
.icon(Icon::new(IconName::Relay)) .icon(Icon::new(IconName::Relay))

View File

@@ -1,6 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use chat::{ChatEvent, ChatRegistry}; use chat::{ChatEvent, ChatRegistry, InboxState};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement,
@@ -233,13 +233,13 @@ impl Workspace {
), ),
_ => this, _ => this,
}) })
.map(|this| match chat.read(cx).relay_state(cx) { .map(|this| match chat.read(cx).state(cx) {
RelayState::Checking => { InboxState::Checking => {
this.child(div().text_xs().text_color(cx.theme().text_muted).child( this.child(div().text_xs().text_color(cx.theme().text_muted).child(
SharedString::from("Fetching user's messaging relay list..."), SharedString::from("Fetching user's messaging relay list..."),
)) ))
} }
RelayState::NotConfigured => this.child( InboxState::RelayNotAvailable => this.child(
h_flex() h_flex()
.h_6() .h_6()
.w_full() .w_full()

View File

@@ -7,6 +7,7 @@ publish.workspace = true
[dependencies] [dependencies]
common = { path = "../common" } common = { path = "../common" }
state = { path = "../state" } state = { path = "../state" }
person = { path = "../person" }
gpui.workspace = true gpui.workspace = true
nostr-sdk.workspace = true nostr-sdk.workspace = true

View File

@@ -4,12 +4,11 @@ use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error}; use anyhow::{anyhow, Context as AnyhowContext, Error};
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use state::{app_name, NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT}; use state::{
app_name, Announcement, DeviceState, NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT,
mod device; };
pub use device::*;
const IDENTIFIER: &str = "coop:device"; const IDENTIFIER: &str = "coop:device";
@@ -218,16 +217,17 @@ impl DeviceRegistry {
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let public_key = signer.public_key().unwrap(); let public_key = signer.public_key().unwrap();
let messaging_relays = nostr.read(cx).messaging_relays(&public_key, cx); let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
let relay_urls = profile.messaging_relays().clone();
cx.background_spawn(async move { cx.background_spawn(async move {
let relay_urls = messaging_relays.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> = relay_urls
.iter() .into_iter()
.map(|relay| (relay, filter.clone())) .map(|relay| (relay, filter.clone()))
.collect(); .collect();

View File

@@ -7,7 +7,6 @@ publish.workspace = true
[dependencies] [dependencies]
common = { path = "../common" } common = { path = "../common" }
state = { path = "../state" } state = { path = "../state" }
device = { path = "../device" }
gpui.workspace = true gpui.workspace = true
nostr-sdk.workspace = true nostr-sdk.workspace = true

View File

@@ -5,11 +5,10 @@ use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use common::EventUtils; use common::EventUtils;
use device::Announcement;
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::{NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT}; use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT};
mod person; mod person;

View File

@@ -1,9 +1,9 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use device::Announcement;
use gpui::SharedString; use gpui::SharedString;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use state::Announcement;
const IMAGE_RESIZER: &str = "https://wsrv.nl"; const IMAGE_RESIZER: &str = "https://wsrv.nl";

View File

@@ -5,10 +5,7 @@ use nostr_sdk::prelude::*;
/// Gossip /// Gossip
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Gossip { pub struct Gossip {
/// Gossip relays for each public key
relays: HashMap<PublicKey, HashSet<(RelayUrl, Option<RelayMetadata>)>>, relays: HashMap<PublicKey, HashSet<(RelayUrl, Option<RelayMetadata>)>>,
/// Messaging relays for each public key
messaging_relays: HashMap<PublicKey, HashSet<RelayUrl>>,
} }
impl Gossip { impl Gossip {
@@ -69,39 +66,5 @@ impl Gossip {
}) })
.take(3), .take(3),
); );
log::info!("Updating gossip relays for: {}", event.pubkey);
}
/// Get messaging relays for a given public key
pub fn messaging_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
self.messaging_relays
.get(public_key)
.cloned()
.unwrap_or_default()
.into_iter()
.collect()
}
/// Insert messaging relays for a public key
pub fn insert_messaging_relays(&mut self, event: &Event) {
self.messaging_relays
.entry(event.pubkey)
.or_default()
.extend(
event
.tags
.iter()
.filter_map(|tag| {
if let Some(TagStandard::Relay(url)) = tag.as_standardized() {
Some(url.to_owned())
} else {
None
}
})
.take(3),
);
log::info!("Updating messaging relays for: {}", event.pubkey);
} }
} }

View File

@@ -11,12 +11,14 @@ use nostr_lmdb::prelude::*;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
mod constants; mod constants;
mod device;
mod gossip; mod gossip;
mod nip05; mod nip05;
mod nip96; mod nip96;
mod signer; mod signer;
pub use constants::*; pub use constants::*;
pub use device::*;
pub use gossip::*; pub use gossip::*;
pub use nip05::*; pub use nip05::*;
pub use nip96::*; pub use nip96::*;
@@ -181,10 +183,7 @@ impl NostrRegistry {
} = notification } = notification
{ {
// Skip if the event has already been processed // Skip if the event has already been processed
if !processed_events.insert(event.id) { if processed_events.insert(event.id) {
continue;
}
match event.kind { match event.kind {
Kind::RelayList => { Kind::RelayList => {
tx.send_async(event.into_owned()).await?; tx.send_async(event.into_owned()).await?;
@@ -196,6 +195,7 @@ impl NostrRegistry {
} }
} }
} }
}
Ok(()) Ok(())
}); });
@@ -205,21 +205,12 @@ impl NostrRegistry {
self.tasks.push(cx.spawn(async move |_this, cx| { self.tasks.push(cx.spawn(async move |_this, cx| {
while let Ok(event) = rx.recv_async().await { while let Ok(event) = rx.recv_async().await {
match event.kind { if let Kind::RelayList = event.kind {
Kind::RelayList => {
gossip.update(cx, |this, cx| { gossip.update(cx, |this, cx| {
this.insert_relays(&event); this.insert_relays(&event);
cx.notify(); cx.notify();
})?; })?;
} }
Kind::InboxRelays => {
gossip.update(cx, |this, cx| {
this.insert_messaging_relays(&event);
cx.notify();
})?;
}
_ => {}
}
} }
Ok(()) Ok(())
@@ -256,15 +247,6 @@ impl NostrRegistry {
self.relay_list_state.clone() self.relay_list_state.clone()
} }
/// Get a relay hint (messaging relay) for a given public key
pub fn relay_hint(&self, public_key: &PublicKey, cx: &App) -> Option<RelayUrl> {
self.gossip
.read(cx)
.messaging_relays(public_key)
.first()
.cloned()
}
/// Get a list of write relays for a given public key /// Get a list of write relays for a given public key
pub fn write_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> { pub fn write_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client(); let client = self.client();
@@ -295,21 +277,6 @@ impl NostrRegistry {
}) })
} }
/// Get a list of messaging relays for a given public key
pub fn messaging_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
let relays = self.gossip.read(cx).messaging_relays(public_key);
cx.background_spawn(async move {
// Ensure relay connections
for url in relays.iter() {
client.add_relay(url).and_connect().await.ok();
}
relays
})
}
/// Set the connected status of the client /// Set the connected status of the client
fn set_connected(&mut self, cx: &mut Context<Self>) { fn set_connected(&mut self, cx: &mut Context<Self>) {
self.connected = true; self.connected = true;