From c9d1f1c8c35ad959ce89fd2355376def156f09d3 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 12 Mar 2026 15:50:33 +0700 Subject: [PATCH 1/2] . --- crates/chat/src/room.rs | 34 ++++++-- crates/chat_ui/src/lib.rs | 177 +++++++++++++++++--------------------- 2 files changed, 110 insertions(+), 101 deletions(-) diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index 713a32a..448eda8 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -62,11 +62,35 @@ impl SendReport { /// Returns true if the send was successful. pub fn success(&self) -> bool { - if let Some(output) = self.output.as_ref() { - !output.failed.is_empty() - } else { - false - } + self.error.is_none() && self.output.as_ref().is_some_and(|o| !o.success.is_empty()) + } + + /// Returns true if the send failed. + pub fn failed(&self) -> bool { + self.error.is_some() && self.output.as_ref().is_some_and(|o| !o.failed.is_empty()) + } +} + +#[derive(Debug, Clone)] +pub enum SendStatus { + Ok { + id: EventId, + relay: RelayUrl, + }, + Failed { + id: EventId, + relay: RelayUrl, + message: String, + }, +} + +impl SendStatus { + pub fn ok(id: EventId, relay: RelayUrl) -> Self { + Self::Ok { id, relay } + } + + pub fn failed(id: EventId, relay: RelayUrl, message: String) -> Self { + Self::Failed { id, relay, message } } } diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index aab9c72..6c1719c 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -3,7 +3,7 @@ use std::sync::Arc; pub use actions::*; use anyhow::{Context as AnyhowContext, Error}; -use chat::{Message, RenderedMessage, Room, RoomEvent, SendReport}; +use chat::{Message, RenderedMessage, Room, RoomEvent, SendReport, SendStatus}; use common::RenderedTimestamp; use gpui::prelude::FluentBuilder; use gpui::{ @@ -24,7 +24,6 @@ use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::dock::{Panel, PanelEvent}; -use ui::indicator::Indicator; use ui::input::{InputEvent, InputState, TextInput}; use ui::menu::DropdownMenu; use ui::notification::Notification; @@ -185,22 +184,33 @@ impl ChatPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let sent_ids = self.sent_ids.clone(); + let reports = self.reports_by_id.downgrade(); - let (tx, rx) = flume::bounded::<(EventId, RelayUrl)>(256); + let (tx, rx) = flume::bounded::>(256); self.tasks.push(cx.background_spawn(async move { let mut notifications = client.notifications(); while let Some(notification) = notifications.next().await { if let ClientNotification::Message { - message: RelayMessage::Ok { event_id, .. }, + message: + RelayMessage::Ok { + event_id, + status, + message, + }, relay_url, } = notification { let sent_ids = sent_ids.read().await; if sent_ids.contains(&event_id) { - tx.send_async((event_id, relay_url)).await.ok(); + let status = if status { + SendStatus::ok(event_id, relay_url) + } else { + SendStatus::failed(event_id, relay_url, message.into()) + }; + tx.send_async(Arc::new(status)).await.ok(); } } } @@ -208,24 +218,31 @@ impl ChatPanel { Ok(()) })); - self.tasks.push(cx.spawn(async move |this, cx| { - while let Ok((event_id, relay_url)) = rx.recv_async().await { - this.update(cx, |this, cx| { - this.reports_by_id.update(cx, |this, cx| { - for reports in this.values_mut() { - for report in reports.iter_mut() { - if let Some(output) = report.output.as_mut() { - if output.id() == &event_id { - output.success.insert(relay_url.clone()); - cx.notify(); + self.tasks.push(cx.spawn(async move |_this, cx| { + while let Ok(status) = rx.recv_async().await { + reports.update(cx, |this, cx| { + for reports in this.values_mut() { + for report in reports.iter_mut() { + let Some(output) = report.output.as_mut() else { + continue; + }; + match &*status { + SendStatus::Ok { id, relay } => { + if output.id() == id { + output.success.insert(relay.clone()); + } + } + SendStatus::Failed { id, relay, message } => { + if output.id() == id { + output.failed.insert(relay.clone(), message.clone()); } } } + cx.notify(); } - }); + } })?; } - Ok(()) })); } @@ -442,23 +459,12 @@ impl ChatPanel { self.reports_by_id .read(cx) .get(id) - .is_some_and(|reports| reports.iter().any(|r| r.pending())) + .is_some_and(|reports| reports.iter().all(|r| r.pending())) } - /// Check if a message was sent successfully by its ID - fn sent_success(&self, id: &EventId, cx: &App) -> bool { - self.reports_by_id - .read(cx) - .get(id) - .is_some_and(|reports| reports.iter().any(|r| r.success())) - } - - /// Check if a message failed to send by its ID - fn sent_failed(&self, id: &EventId, cx: &App) -> Option { - self.reports_by_id - .read(cx) - .get(id) - .map(|reports| reports.iter().all(|r| !r.success())) + /// Check if a message has any reports + fn has_reports(&self, id: &EventId, cx: &App) -> bool { + self.reports_by_id.read(cx).get(id).is_some() } /// Get all sent reports for a message by its ID @@ -825,19 +831,13 @@ impl ChatPanel { ) -> AnyElement { let id = message.id; let author = self.profile(&message.author, cx); - let public_key = author.public_key(); + let pk = author.public_key(); let replies = message.replies_to.as_slice(); let has_replies = !replies.is_empty(); - // Check if message is sent failed let sent_pending = self.sent_pending(&id, cx); - - // Check if message is sent successfully - let sent_success = self.sent_success(&id, cx); - - // Check if message is sent failed - let sent_failed = self.sent_failed(&id, cx); + let has_reports = self.has_reports(&id, cx); // Hide avatar setting let hide_avatar = AppSettings::get_hide_avatar(cx); @@ -854,14 +854,17 @@ impl ChatPanel { .flex() .gap_3() .when(!hide_avatar, |this| { - this.child(Avatar::new(author.avatar()).dropdown_menu( - move |this, _window, _cx| { - this.menu("Copy Public Key", Box::new(Command::Copy(public_key))) - .menu("View Relays", Box::new(Command::Relays(public_key))) - .separator() - .menu("View on njump.me", Box::new(Command::Njump(public_key))) - }, - )) + this.child( + Avatar::new(author.avatar()) + .flex_shrink_0() + .relative() + .dropdown_menu(move |this, _window, _cx| { + this.menu("Public Key", Box::new(Command::Copy(pk))) + .menu("View Relays", Box::new(Command::Relays(pk))) + .separator() + .menu("View on njump.me", Box::new(Command::Njump(pk))) + }), + ) }) .child( v_flex() @@ -882,21 +885,16 @@ impl ChatPanel { ) .child(message.created_at.to_human_time()) .when(sent_pending, |this| { - this.child(deferred(Indicator::new().small())) + this.child(SharedString::from("• Sending...")) }) - .when(sent_success, |this| { - this.child(deferred(self.render_sent_indicator(&id, cx))) + .when(has_reports, |this| { + this.child(deferred(self.render_sent_reports(&id, cx))) }), ) .when(has_replies, |this| { this.children(self.render_message_replies(replies, cx)) }) - .child(rendered_text) - .when_some(sent_failed, |this, failed| { - this.when(failed, |this| { - this.child(deferred(self.render_message_reports(&id, cx))) - }) - }), + .child(rendered_text), ), ) .child( @@ -909,7 +907,7 @@ impl ChatPanel { .h_full() .bg(cx.theme().border_transparent), ) - .child(self.render_actions(&id, &public_key, cx)) + .child(self.render_actions(&id, &pk, cx)) .on_mouse_down( MouseButton::Middle, cx.listener(move |this, _, _window, cx| { @@ -969,50 +967,37 @@ impl ChatPanel { items } - fn render_sent_indicator(&self, id: &EventId, cx: &Context) -> impl IntoElement { + fn render_sent_reports(&self, id: &EventId, cx: &App) -> impl IntoElement { + let reports = self.sent_reports(id, cx); + + let success = reports + .as_ref() + .is_some_and(|reports| reports.iter().any(|r| r.success())); + + let failed = reports + .as_ref() + .is_some_and(|reports| reports.iter().all(|r| r.failed())); + + let label = if success { + SharedString::from("• Sent") + } else if failed { + SharedString::from("• Failed") + } else { + SharedString::from("• Sending...") + }; + div() .id(SharedString::from(id.to_hex())) - .child(SharedString::from("• Sent")) - .when_some(self.sent_reports(id, cx), |this, reports| { + .child(label) + .when(failed, |this| this.text_color(cx.theme().text_danger)) + .when_some(reports, |this, reports| { this.on_click(move |_e, window, cx| { let reports = reports.clone(); window.open_modal(cx, move |this, _window, cx| { - this.show_close(true) - .title(SharedString::from("Sent Reports")) - .child(v_flex().pb_2().gap_4().children({ - let mut items = Vec::with_capacity(reports.len()); - - for report in reports.iter() { - items.push(Self::render_report(report, cx)) - } - - items - })) - }); - }) - }) - } - - fn render_message_reports(&self, id: &EventId, cx: &Context) -> impl IntoElement { - h_flex() - .id(SharedString::from(id.to_hex())) - .gap_1() - .text_color(cx.theme().text_danger) - .text_xs() - .italic() - .child(Icon::new(IconName::Info).small()) - .child(SharedString::from( - "Failed to send message. Click to see details.", - )) - .when_some(self.sent_reports(id, cx), |this, reports| { - this.on_click(move |_e, window, cx| { - let reports = reports.clone(); - - window.open_modal(cx, move |this, _window, cx| { - this.show_close(true) - .title(SharedString::from("Sent Reports")) - .child(v_flex().gap_4().w_full().children({ + this.title(SharedString::from("Sent Reports")) + .show_close(true) + .child(v_flex().gap_4().children({ let mut items = Vec::with_capacity(reports.len()); for report in reports.iter() { -- 2.49.1 From d43383bed1c939dcbc8c22e7d1c1b97b20826a1b Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 12 Mar 2026 16:49:57 +0700 Subject: [PATCH 2/2] update room kind on new message --- crates/chat/src/lib.rs | 10 ++++++++++ crates/chat/src/room.rs | 12 +++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 10528a7..8eeea4c 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -613,9 +613,19 @@ impl ChatRegistry { /// If the room doesn't exist, it will be created. /// Updates room ordering based on the most recent messages. pub fn new_message(&mut self, message: NewMessage, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let signer = nostr.read(cx).signer(); + match self.rooms.iter().find(|e| e.read(cx).id == message.room) { Some(room) => { room.update(cx, |this, cx| { + if this.kind == RoomKind::Request { + if let Some(public_key) = signer.public_key() { + if message.rumor.pubkey == public_key { + this.set_ongoing(cx); + } + } + } this.push_message(message, cx); }); self.sort(cx); diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index 448eda8..efbf095 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -57,7 +57,11 @@ impl SendReport { /// Returns true if the send is pending. pub fn pending(&self) -> bool { - self.output.is_none() && self.error.is_none() + self.error.is_none() + && self + .output + .as_ref() + .is_some_and(|o| o.success.is_empty() && o.failed.is_empty()) } /// Returns true if the send was successful. @@ -228,10 +232,8 @@ impl Room { /// Sets this room is ongoing conversation pub fn set_ongoing(&mut self, cx: &mut Context) { - if self.kind != RoomKind::Ongoing { - self.kind = RoomKind::Ongoing; - cx.notify(); - } + self.kind = RoomKind::Ongoing; + cx.notify(); } /// Updates the creation timestamp of the room -- 2.49.1