This commit is contained in:
2026-07-23 16:12:36 +07:00
parent bf6ae65b05
commit 40166fd071
5 changed files with 129 additions and 85 deletions

View File

@@ -47,16 +47,14 @@ pub enum ChatEvent {
/// No Inbox Relays found, the app is not ready to subscribe messages /// No Inbox Relays found, the app is not ready to subscribe messages
InboxRelayNotFound, InboxRelayNotFound,
/// An error occurred /// An error occurred
Error(SharedString), Error(String),
} }
/// Channel signal. /// Channel signal.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Signal { enum Signal {
/// Inbox Relays found, the app is ready to subscribe messages /// Inbox Relays found, the app is ready to subscribe messages
InboxReady(Box<Event>), InboxReady,
/// No Inbox Relays found, the app is not ready to subscribe messages
InboxRelayNotFound,
/// Message received from relay pool /// Message received from relay pool
Message(NewMessage), Message(NewMessage),
/// Eose received from relay pool /// Eose received from relay pool
@@ -70,18 +68,6 @@ impl Signal {
Self::Message(NewMessage::new(gift_wrap, rumor)) Self::Message(NewMessage::new(gift_wrap, rumor))
} }
pub fn inbox_ready(event: &Event) -> Self {
Self::InboxReady(Box::new(event.to_owned()))
}
pub fn inbox_relay_not_found() -> Self {
Self::InboxRelayNotFound
}
pub fn eose() -> Self {
Self::Eose
}
pub fn error<T>(event: &Event, reason: T) -> Self pub fn error<T>(event: &Event, reason: T) -> Self
where where
T: Into<SharedString>, T: Into<SharedString>,
@@ -108,9 +94,6 @@ pub struct ChatRegistry {
/// Tracking the status of unwrapping gift wrap events. /// Tracking the status of unwrapping gift wrap events.
tracking: Arc<AtomicBool>, tracking: Arc<AtomicBool>,
/// Whether the messaging relays have been found.
msg_relays_existed: Arc<AtomicBool>,
/// Channel for sending signals to the UI. /// Channel for sending signals to the UI.
signal_tx: flume::Sender<Signal>, signal_tx: flume::Sender<Signal>,
@@ -167,7 +150,6 @@ impl ChatRegistry {
seens: Arc::new(RwLock::new(HashMap::default())), seens: Arc::new(RwLock::new(HashMap::default())),
event_map: Arc::new(RwLock::new(HashMap::default())), event_map: Arc::new(RwLock::new(HashMap::default())),
tracking: Arc::new(AtomicBool::new(false)), tracking: Arc::new(AtomicBool::new(false)),
msg_relays_existed: Arc::new(AtomicBool::new(false)),
signal_rx: rx, signal_rx: rx,
signal_tx: tx, signal_tx: tx,
tasks: smallvec![], tasks: smallvec![],
@@ -182,8 +164,6 @@ impl ChatRegistry {
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let tracking = self.tracking.clone(); let tracking = self.tracking.clone();
let msg_relays_existed = self.msg_relays_existed.clone();
let seens = self.seens.clone(); let seens = self.seens.clone();
let event_map = self.event_map.clone(); let event_map = self.event_map.clone();
let trashes = self.trashes.downgrade(); let trashes = self.trashes.downgrade();
@@ -208,12 +188,6 @@ impl ChatRegistry {
match *message { match *message {
RelayMessage::Event { event, .. } => { RelayMessage::Event { event, .. } => {
// Keep track of which relays have seen this event
{
let mut seens = seens.write().await;
seens.entry(event.id).or_default().insert(relay_url);
}
// De-duplicate events by their ID // De-duplicate events by their ID
if !processed_events.insert(event.id) { if !processed_events.insert(event.id) {
continue; continue;
@@ -222,12 +196,9 @@ impl ChatRegistry {
// Handle msg relays event to determine when the app is ready to subscribe // Handle msg relays event to determine when the app is ready to subscribe
if event.kind == Kind::InboxRelays { if event.kind == Kind::InboxRelays {
let current_user = signer.get_public_key_async().await?; let current_user = signer.get_public_key_async().await?;
if event.pubkey == current_user {
// Mark that the msg relays have been found
msg_relays_existed.store(true, Ordering::Release);
// Emit the inbox ready signal // Emit the inbox ready signal
tx.send_async(Signal::inbox_ready(&event)).await?; if event.pubkey == current_user {
tx.send_async(Signal::InboxReady).await?;
} }
} }
@@ -236,6 +207,12 @@ impl ChatRegistry {
continue; continue;
} }
// Keep track of which relays have seen this event
{
let mut seens = seens.write().await;
seens.entry(event.id).or_default().insert(relay_url);
}
// Extract the rumor from the gift wrap event // Extract the rumor from the gift wrap event
match extract_rumor(&client, &signer, event.as_ref()).await { match extract_rumor(&client, &signer, event.as_ref()).await {
Ok(rumor) => { Ok(rumor) => {
@@ -270,7 +247,7 @@ impl ChatRegistry {
RelayMessage::EndOfStoredEvents(id) RelayMessage::EndOfStoredEvents(id)
if (id.as_ref() == &sub_id1 || id.as_ref() == &sub_id2) => if (id.as_ref() == &sub_id1 || id.as_ref() == &sub_id2) =>
{ {
tx.send_async(Signal::eose()).await?; tx.send_async(Signal::Eose).await?;
} }
_ => {} _ => {}
} }
@@ -287,14 +264,9 @@ impl ChatRegistry {
this.new_message(message, cx); this.new_message(message, cx);
})?; })?;
} }
Signal::InboxReady(event) => { Signal::InboxReady => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.get_messages(&event, cx); this.get_messages(cx);
})?;
}
Signal::InboxRelayNotFound => {
this.update(cx, |_this, cx| {
cx.emit(ChatEvent::InboxRelayNotFound);
})?; })?;
} }
Signal::Eose => { Signal::Eose => {
@@ -367,20 +339,34 @@ impl ChatRegistry {
Ok(()) Ok(())
})); }));
let tx = self.signal_tx.clone(); let client = nostr.read(cx).client();
let msg_relays_existed = self.msg_relays_existed.clone();
// Reset the status flag // Spawn a task to verify user inbox relays after 5 seconds
msg_relays_existed.store(false, Ordering::Release); self.tasks.push(cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(5)).await;
// Wait for the msg relays to be found or timeout if !cx
self.tasks.push(cx.background_spawn(async move { .background_spawn(async move {
// Wait for 5 seconds // Construct inbox relays filter
smol::Timer::after(Duration::from_secs(5)).await; let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
// Then check if the msg relays have been found // Check the latest inbox relays event in database
if !msg_relays_existed.load(Ordering::Acquire) { client
tx.send_async(Signal::inbox_relay_not_found()).await?; .database()
.query(filter)
.await
.unwrap_or_default()
.first_owned()
.is_some()
})
.await
{
this.update(cx, |_this, cx| {
cx.emit(ChatEvent::InboxRelayNotFound);
})?;
} }
Ok(()) Ok(())
@@ -388,35 +374,47 @@ impl ChatRegistry {
} }
/// Get all messages for the provided signer /// Get all messages for the provided signer
fn get_messages(&mut self, msg_relays: &Event, cx: &mut Context<Self>) { fn get_messages(&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 signer = nostr.read(cx).signer();
let urls: Vec<RelayUrl> = nip17::extract_relay_list(msg_relays).collect();
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let public_key = signer.get_public_key_async().await?; let public_key = signer.get_public_key_async().await?;
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(format!("{}-msg", public_key.to_hex())); // Construct inbox relays filter
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
// Get the latest inbox relays event in database
let event = client
.database()
.query(filter)
.await?
.first_owned()
.ok_or(anyhow::anyhow!("No inbox relays found"))?;
// Extract relay list from event
let relays: Vec<RelayUrl> = nip17::extract_relay_list(&event).collect();
// Ensure relay connections // Ensure relay connections
for url in urls.iter() { for url in relays.iter() {
client.add_relay(url).and_connect().await?; client.add_relay(url).and_connect().await?;
} }
// Construct gift wrap event filter
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(format!("{}-msg", public_key.to_hex()));
// Construct target for subscription // Construct target for subscription
let target: HashMap<RelayUrl, Filter> = urls let target: HashMap<RelayUrl, Filter> = relays
.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?; client.subscribe(target).with_id(id).await?;
log::info!(
"Successfully subscribed to gift-wrap messages on: {:?}",
output.success
);
Ok(()) Ok(())
}); });
@@ -424,7 +422,7 @@ impl ChatRegistry {
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
if let Err(e) = task.await { if let Err(e) = task.await {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(ChatEvent::Error(SharedString::from(e.to_string()))); cx.emit(ChatEvent::Error(e.to_string()));
})?; })?;
} }
Ok(()) Ok(())

View File

@@ -126,13 +126,13 @@ impl Person {
if let Some(display_name) = self.metadata().display_name.as_ref() if let Some(display_name) = self.metadata().display_name.as_ref()
&& !display_name.is_empty() && !display_name.is_empty()
{ {
return SharedString::from(display_name); return SharedString::from(display_name.trim());
} }
if let Some(name) = self.metadata().name.as_ref() if let Some(name) = self.metadata().name.as_ref()
&& !name.is_empty() && !name.is_empty()
{ {
return SharedString::from(name); return SharedString::from(name.trim());
} }
SharedString::from(shorten_pubkey(self.public_key(), 4)) SharedString::from(shorten_pubkey(self.public_key(), 4))

View File

@@ -3,7 +3,7 @@ use std::time::Duration;
use anyhow::{Error, anyhow}; use anyhow::{Error, anyhow};
use common::config_dir; use common::config_dir;
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window}; use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, Window};
use nostr_gossip_memory::prelude::*; use nostr_gossip_memory::prelude::*;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use nostr_lmdb::prelude::*; use nostr_lmdb::prelude::*;
@@ -53,17 +53,25 @@ pub enum StateEvent {
/// Connected to the bootstrapping relay /// Connected to the bootstrapping relay
Connected, Connected,
/// An error occurred /// An error occurred
Error(SharedString), Error(String),
} }
impl StateEvent { impl StateEvent {
pub fn error<T>(error: T) -> Self pub fn error<T>(error: T) -> Self
where where
T: Into<SharedString>, T: Into<String>,
{ {
Self::Error(error.into()) Self::Error(error.into())
} }
pub fn is_connecting(&self) -> bool {
matches!(self, StateEvent::Connecting)
}
pub fn is_connected(&self) -> bool {
matches!(self, StateEvent::Connected)
}
pub fn signer_changed(&self) -> bool { pub fn signer_changed(&self) -> bool {
matches!(self, StateEvent::SignerChanged) matches!(self, StateEvent::SignerChanged)
} }
@@ -126,9 +134,10 @@ impl NostrRegistry {
}) })
.build(); .build();
// Run at the end of current cycle // Connect to bootstrap relays after the window is ready
cx.defer_in(window, |this, _window, cx| { cx.defer_in(window, |this, _window, cx| {
this.connect(cx); this.connect_bootstrap_relays(cx);
this.check_credential(cx);
}); });
Self { Self {
@@ -163,20 +172,34 @@ impl NostrRegistry {
<T as AsyncNip44>::Error: std::error::Error + Send + Sync + 'static, <T as AsyncNip44>::Error: std::error::Error + Send + Sync + 'static,
{ {
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let current_user = new_signer.get_public_key_async().await.ok(); match new_signer.get_public_key_async().await {
Ok(public_key) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.current_user = current_user; this.signer.swap_inner(new_signer);
this.current_user = Some(public_key);
cx.emit(StateEvent::SignerChanged); cx.emit(StateEvent::SignerChanged);
cx.notify();
}) })
.ok(); .ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(StateEvent::error(e.to_string()));
})
.ok();
}
};
}) })
.detach(); .detach();
} }
/// Connect to the bootstrapping relays /// Connect to the bootstrapping relays
fn connect(&mut self, cx: &mut Context<Self>) { fn connect_bootstrap_relays(&mut self, cx: &mut Context<Self>) {
let client = self.client(); let client = self.client();
// Emit connecting event
cx.emit(StateEvent::Connecting);
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
// Add indexer relay to the relay pool // Add indexer relay to the relay pool
for url in INDEXER_RELAYS.into_iter() { for url in INDEXER_RELAYS.into_iter() {
@@ -197,9 +220,6 @@ impl NostrRegistry {
Ok(()) Ok(())
}); });
// 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| {
if let Err(e) = task.await { if let Err(e) = task.await {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
@@ -215,6 +235,17 @@ impl NostrRegistry {
})); }));
} }
fn check_credential(&mut self, cx: &mut Context<Self>) {
self.tasks.push(cx.spawn(async move |this, cx| {
this.update(cx, |this, cx| {
// TODO: check credential
cx.notify();
})?;
Ok(())
}));
}
/// Get the public key of a NIP-05 address /// Get the public key of a NIP-05 address
pub fn query_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> { pub fn query_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
let client = self.client(); let client = self.client();

View File

@@ -81,6 +81,17 @@ impl Workspace {
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
subscriptions.push(
// Observe sign in state changes
cx.observe_in(&nostr, window, move |this, nostr, window, cx| {
if nostr.read(cx).current_user().is_some() {
this.set_center_layout(window, cx);
} else {
this.import_identity(window, cx);
}
}),
);
subscriptions.push( subscriptions.push(
// Observe system appearance and update theme // Observe system appearance and update theme
cx.observe_window_appearance(window, |_this, window, cx| { cx.observe_window_appearance(window, |_this, window, cx| {
@@ -90,7 +101,7 @@ impl Workspace {
subscriptions.push( subscriptions.push(
// Subscribe to the nostr events // Subscribe to the nostr events
cx.subscribe_in(&nostr, window, move |_this, _state, event, window, cx| { cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
match event { match event {
StateEvent::Connecting => { StateEvent::Connecting => {
let note = Notification::new() let note = Notification::new()
@@ -108,6 +119,10 @@ impl Workspace {
window.push_notification(note, cx); window.push_notification(note, cx);
} }
StateEvent::SignerChanged => {
this.set_center_layout(window, cx);
window.close_all_modals(cx);
}
_ => {} _ => {}
}; };
}), }),

View File

@@ -462,7 +462,7 @@ impl Sidebar {
}); });
RoomEntry::new(range.start + ix) RoomEntry::new(range.start + ix)
.name(profile.name()) .name(profile.name().trim())
.avatar(profile.avatar()) .avatar(profile.avatar())
.on_click(handler) .on_click(handler)
.selected(selected) .selected(selected)