feat: refactor send report #20

Merged
reya merged 2 commits from refactor-report into master 2026-03-12 10:06:14 +00:00
3 changed files with 127 additions and 106 deletions

View File

@@ -613,9 +613,19 @@ impl ChatRegistry {
/// If the room doesn't exist, it will be created. /// If the room doesn't exist, it will be created.
/// Updates room ordering based on the most recent messages. /// Updates room ordering based on the most recent messages.
pub fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) { pub fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
match self.rooms.iter().find(|e| e.read(cx).id == message.room) { match self.rooms.iter().find(|e| e.read(cx).id == message.room) {
Some(room) => { Some(room) => {
room.update(cx, |this, cx| { 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); this.push_message(message, cx);
}); });
self.sort(cx); self.sort(cx);

View File

@@ -57,16 +57,44 @@ impl SendReport {
/// Returns true if the send is pending. /// Returns true if the send is pending.
pub fn pending(&self) -> bool { 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. /// Returns true if the send was successful.
pub fn success(&self) -> bool { pub fn success(&self) -> bool {
if let Some(output) = self.output.as_ref() { self.error.is_none() && self.output.as_ref().is_some_and(|o| !o.success.is_empty())
!output.failed.is_empty() }
} else {
false /// 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 }
} }
} }
@@ -204,10 +232,8 @@ impl Room {
/// Sets this room is ongoing conversation /// Sets this room is ongoing conversation
pub fn set_ongoing(&mut self, cx: &mut Context<Self>) { pub fn set_ongoing(&mut self, cx: &mut Context<Self>) {
if self.kind != RoomKind::Ongoing { self.kind = RoomKind::Ongoing;
self.kind = RoomKind::Ongoing; cx.notify();
cx.notify();
}
} }
/// Updates the creation timestamp of the room /// Updates the creation timestamp of the room

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
pub use actions::*; pub use actions::*;
use anyhow::{Context as AnyhowContext, Error}; 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 common::RenderedTimestamp;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
@@ -24,7 +24,6 @@ use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock::{Panel, PanelEvent}; use ui::dock::{Panel, PanelEvent};
use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::menu::DropdownMenu; use ui::menu::DropdownMenu;
use ui::notification::Notification; use ui::notification::Notification;
@@ -185,22 +184,33 @@ impl ChatPanel {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let sent_ids = self.sent_ids.clone(); 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::<Arc<SendStatus>>(256);
self.tasks.push(cx.background_spawn(async move { self.tasks.push(cx.background_spawn(async move {
let mut notifications = client.notifications(); let mut notifications = client.notifications();
while let Some(notification) = notifications.next().await { while let Some(notification) = notifications.next().await {
if let ClientNotification::Message { if let ClientNotification::Message {
message: RelayMessage::Ok { event_id, .. }, message:
RelayMessage::Ok {
event_id,
status,
message,
},
relay_url, relay_url,
} = notification } = notification
{ {
let sent_ids = sent_ids.read().await; let sent_ids = sent_ids.read().await;
if sent_ids.contains(&event_id) { 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(()) Ok(())
})); }));
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |_this, cx| {
while let Ok((event_id, relay_url)) = rx.recv_async().await { while let Ok(status) = rx.recv_async().await {
this.update(cx, |this, cx| { reports.update(cx, |this, cx| {
this.reports_by_id.update(cx, |this, cx| { for reports in this.values_mut() {
for reports in this.values_mut() { for report in reports.iter_mut() {
for report in reports.iter_mut() { let Some(output) = report.output.as_mut() else {
if let Some(output) = report.output.as_mut() { continue;
if output.id() == &event_id { };
output.success.insert(relay_url.clone()); match &*status {
cx.notify(); 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(()) Ok(())
})); }));
} }
@@ -442,23 +459,12 @@ impl ChatPanel {
self.reports_by_id self.reports_by_id
.read(cx) .read(cx)
.get(id) .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 /// Check if a message has any reports
fn sent_success(&self, id: &EventId, cx: &App) -> bool { fn has_reports(&self, id: &EventId, cx: &App) -> bool {
self.reports_by_id self.reports_by_id.read(cx).get(id).is_some()
.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<bool> {
self.reports_by_id
.read(cx)
.get(id)
.map(|reports| reports.iter().all(|r| !r.success()))
} }
/// Get all sent reports for a message by its ID /// Get all sent reports for a message by its ID
@@ -825,19 +831,13 @@ impl ChatPanel {
) -> AnyElement { ) -> AnyElement {
let id = message.id; let id = message.id;
let author = self.profile(&message.author, cx); 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 replies = message.replies_to.as_slice();
let has_replies = !replies.is_empty(); let has_replies = !replies.is_empty();
// Check if message is sent failed
let sent_pending = self.sent_pending(&id, cx); let sent_pending = self.sent_pending(&id, cx);
let has_reports = self.has_reports(&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);
// Hide avatar setting // Hide avatar setting
let hide_avatar = AppSettings::get_hide_avatar(cx); let hide_avatar = AppSettings::get_hide_avatar(cx);
@@ -854,14 +854,17 @@ impl ChatPanel {
.flex() .flex()
.gap_3() .gap_3()
.when(!hide_avatar, |this| { .when(!hide_avatar, |this| {
this.child(Avatar::new(author.avatar()).dropdown_menu( this.child(
move |this, _window, _cx| { Avatar::new(author.avatar())
this.menu("Copy Public Key", Box::new(Command::Copy(public_key))) .flex_shrink_0()
.menu("View Relays", Box::new(Command::Relays(public_key))) .relative()
.separator() .dropdown_menu(move |this, _window, _cx| {
.menu("View on njump.me", Box::new(Command::Njump(public_key))) 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( .child(
v_flex() v_flex()
@@ -882,21 +885,16 @@ impl ChatPanel {
) )
.child(message.created_at.to_human_time()) .child(message.created_at.to_human_time())
.when(sent_pending, |this| { .when(sent_pending, |this| {
this.child(deferred(Indicator::new().small())) this.child(SharedString::from("• Sending..."))
}) })
.when(sent_success, |this| { .when(has_reports, |this| {
this.child(deferred(self.render_sent_indicator(&id, cx))) this.child(deferred(self.render_sent_reports(&id, cx)))
}), }),
) )
.when(has_replies, |this| { .when(has_replies, |this| {
this.children(self.render_message_replies(replies, cx)) this.children(self.render_message_replies(replies, cx))
}) })
.child(rendered_text) .child(rendered_text),
.when_some(sent_failed, |this, failed| {
this.when(failed, |this| {
this.child(deferred(self.render_message_reports(&id, cx)))
})
}),
), ),
) )
.child( .child(
@@ -909,7 +907,7 @@ impl ChatPanel {
.h_full() .h_full()
.bg(cx.theme().border_transparent), .bg(cx.theme().border_transparent),
) )
.child(self.render_actions(&id, &public_key, cx)) .child(self.render_actions(&id, &pk, cx))
.on_mouse_down( .on_mouse_down(
MouseButton::Middle, MouseButton::Middle,
cx.listener(move |this, _, _window, cx| { cx.listener(move |this, _, _window, cx| {
@@ -969,50 +967,37 @@ impl ChatPanel {
items items
} }
fn render_sent_indicator(&self, id: &EventId, cx: &Context<Self>) -> 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() div()
.id(SharedString::from(id.to_hex())) .id(SharedString::from(id.to_hex()))
.child(SharedString::from("• Sent")) .child(label)
.when_some(self.sent_reports(id, cx), |this, reports| { .when(failed, |this| this.text_color(cx.theme().text_danger))
.when_some(reports, |this, reports| {
this.on_click(move |_e, window, cx| { this.on_click(move |_e, window, cx| {
let reports = reports.clone(); let reports = reports.clone();
window.open_modal(cx, move |this, _window, cx| { window.open_modal(cx, move |this, _window, cx| {
this.show_close(true) this.title(SharedString::from("Sent Reports"))
.title(SharedString::from("Sent Reports")) .show_close(true)
.child(v_flex().pb_2().gap_4().children({ .child(v_flex().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<Self>) -> 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({
let mut items = Vec::with_capacity(reports.len()); let mut items = Vec::with_capacity(reports.len());
for report in reports.iter() { for report in reports.iter() {