wip
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m43s

This commit is contained in:
2026-02-17 07:54:46 +07:00
parent d25080f5e7
commit 1d8e3724a8
8 changed files with 239 additions and 248 deletions

View File

@@ -22,10 +22,10 @@ anyhow.workspace = true
itertools.workspace = true
smallvec.workspace = true
smol.workspace = true
flume.workspace = true
log.workspace = true
serde.workspace = true
serde_json.workspace = true
indexset = "0.12.3"
once_cell = "1.19.0"
regex = "1"

View File

@@ -1,4 +1,5 @@
use std::collections::HashSet;
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::sync::Arc;
pub use actions::*;
use anyhow::{Context as AnyhowContext, Error};
@@ -7,25 +8,26 @@ use common::{nip96_upload, RenderedTimestamp};
use dock::panel::{Panel, PanelEvent};
use gpui::prelude::FluentBuilder;
use gpui::{
div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext,
deferred, div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext,
ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit, ParentElement,
PathPromptOptions, Render, SharedString, StatefulInteractiveElement, Styled, StyledImage,
Subscription, Task, WeakEntity, Window,
};
use gpui_tokio::Tokio;
use indexset::{BTreeMap, BTreeSet};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use person::{Person, PersonRegistry};
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use smol::fs;
use smol::lock::RwLock;
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::context_menu::ContextMenuExt;
use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput};
use ui::notification::Notification;
use ui::popup_menu::PopupMenuExt;
@@ -61,7 +63,7 @@ pub struct ChatPanel {
rendered_texts_by_id: BTreeMap<EventId, RenderedText>,
/// Mapping message (rumor event) ids to their reports
reports_by_id: BTreeMap<EventId, Vec<SendReport>>,
reports_by_id: Arc<RwLock<BTreeMap<EventId, Vec<SendReport>>>>,
/// Input state
input: Entity<InputState>,
@@ -76,7 +78,7 @@ pub struct ChatPanel {
uploading: bool,
/// Async operations
tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
tasks: Vec<Task<Result<(), Error>>>,
/// Event subscriptions
subscriptions: SmallVec<[Subscription; 2]>,
@@ -125,6 +127,7 @@ impl ChatPanel {
cx.defer_in(window, |this, window, cx| {
this.subscribe_room_events(window, cx);
this.connect(window, cx);
this.handle_notifications(cx);
this.get_messages(window, cx);
});
@@ -138,13 +141,46 @@ impl ChatPanel {
replies_to,
attachments,
rendered_texts_by_id: BTreeMap::new(),
reports_by_id: BTreeMap::new(),
reports_by_id: Arc::new(RwLock::new(BTreeMap::new())),
uploading: false,
subscriptions,
tasks: smallvec![],
tasks: vec![],
}
}
/// Handle nostr notifications
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let reports = self.reports_by_id.clone();
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, .. },
relay_url,
} = notification
{
let mut writer = reports.write().await;
for reports in writer.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());
}
}
}
}
}
}
Ok(())
}));
}
fn subscribe_room_events(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(room) = self.room.upgrade() else {
return;
@@ -240,6 +276,7 @@ impl ChatPanel {
// Get room entity
let room = self.room.clone();
// Get content and replies
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
let content = value.to_string();
@@ -251,6 +288,7 @@ impl ChatPanel {
Some(rumor) => {
this.insert_message(&rumor, true, cx);
this.send_and_wait(rumor, window, cx);
this.clear(window, cx);
}
None => {
window.push_notification("Failed to create message", cx);
@@ -262,7 +300,11 @@ impl ChatPanel {
}));
}
/// Send message in the background and wait for the response
fn send_and_wait(&mut self, rumor: UnsignedEvent, window: &mut Window, cx: &mut Context<Self>) {
// This can't fail, because we already ensured that the ID is set
let id = rumor.id.unwrap();
let Some(room) = self.room.upgrade() else {
return;
};
@@ -274,14 +316,36 @@ impl ChatPanel {
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
let outputs = task.await;
log::info!("Message sent successfully: {outputs:?}");
// Update the state
this.update(cx, |this, cx| {
this.insert_reports(id, outputs, cx);
})?;
Ok(())
}))
}
/// Clear the input field, attachments, and replies
///
/// Only run after sending a message
fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.input.update(cx, |this, cx| {
this.set_value("", window, cx);
});
self.attachments.update(cx, |this, cx| {
this.clear();
cx.notify();
});
self.replies_to.update(cx, |this, cx| {
this.clear();
cx.notify();
})
}
/// Synchronously insert reports
fn insert_reports(&mut self, id: EventId, reports: Vec<SendReport>, cx: &mut Context<Self>) {
self.reports_by_id.insert(id, reports);
self.reports_by_id.write_blocking().insert(id, reports);
cx.notify();
}
@@ -315,23 +379,25 @@ impl ChatPanel {
}
}
/// Check if a message failed to send by its ID
fn is_sent_failed(&self, id: &EventId) -> bool {
/// Check if a message is pending
fn sent_pending(&self, id: &EventId) -> bool {
self.reports_by_id
.read_blocking()
.get(id)
.is_some_and(|reports| reports.iter().all(|r| !r.is_sent_success()))
.is_some_and(|reports| reports.iter().any(|r| r.pending()))
}
/// Check if a message was sent successfully by its ID
fn is_sent_success(&self, id: &EventId) -> Option<bool> {
fn sent_success(&self, id: &EventId) -> bool {
self.reports_by_id
.read_blocking()
.get(id)
.map(|reports| reports.iter().all(|r| r.is_sent_success()))
.is_some_and(|reports| reports.iter().all(|r| r.success()))
}
/// Get the sent reports for a message by its ID
fn sent_reports(&self, id: &EventId) -> Option<&Vec<SendReport>> {
self.reports_by_id.get(id)
/// Get all sent reports for a message by its ID
fn sent_reports(&self, id: &EventId) -> Option<Vec<SendReport>> {
self.reports_by_id.read_blocking().get(id).cloned()
}
/// Get a message by its ID
@@ -380,13 +446,6 @@ impl ChatPanel {
});
}
fn remove_all_replies(&mut self, cx: &mut Context<Self>) {
self.replies_to.update(cx, |this, cx| {
this.clear();
cx.notify();
});
}
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
@@ -401,9 +460,9 @@ impl ChatPanel {
prompt: None,
});
cx.spawn_in(window, async move |this, cx| {
let mut paths = path.await.ok()?.ok()??;
let path = paths.pop()?;
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
let mut paths = path.await??.context("Not found")?;
let path = paths.pop().context("No path")?;
let upload = Tokio::spawn(cx, async move {
let file = fs::read(path).await.ok()?;
@@ -432,9 +491,8 @@ impl ChatPanel {
.ok();
}
Some(())
})
.detach();
Ok(())
}));
}
fn set_uploading(&mut self, uploading: bool, cx: &mut Context<Self>) {
@@ -458,13 +516,6 @@ impl ChatPanel {
});
}
fn remove_all_attachments(&mut self, cx: &mut Context<Self>) {
self.attachments.update(cx, |this, cx| {
this.clear();
cx.notify();
});
}
fn profile(&self, public_key: &PublicKey, cx: &Context<Self>) -> Person {
let persons = PersonRegistry::global(cx);
persons.read(cx).get(public_key, cx)
@@ -530,7 +581,7 @@ impl ChatPanel {
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
if let Some(message) = self.messages.get_index(ix) {
if let Some(message) = self.messages.iter().nth(ix) {
match message {
Message::User(rendered) => {
let text = self
@@ -555,7 +606,7 @@ impl ChatPanel {
&self,
ix: usize,
message: &RenderedMessage,
text: AnyElement,
rendered_text: AnyElement,
cx: &Context<Self>,
) -> AnyElement {
let id = message.id;
@@ -566,10 +617,10 @@ impl ChatPanel {
let has_replies = !replies.is_empty();
// Check if message is sent failed
let is_sent_failed = self.is_sent_failed(&id);
let sent_pending = self.sent_pending(&id);
// Check if message is sent successfully
let is_sent_success = self.is_sent_success(&id);
let sent_success = self.sent_success(&id);
// Hide avatar setting
let hide_avatar = AppSettings::get_hide_avatar(cx);
@@ -617,18 +668,19 @@ impl ChatPanel {
.child(author.name()),
)
.child(message.created_at.to_human_time())
.when_some(is_sent_success, |this, status| {
this.when(status, |this| {
this.child(self.render_message_sent(&id, cx))
})
.when(sent_pending, |this| {
this.child(deferred(Indicator::new().small()))
})
.when(sent_success, |this| {
this.child(deferred(self.render_sent_indicator(&id, cx)))
}),
)
.when(has_replies, |this| {
this.children(self.render_message_replies(replies, cx))
})
.child(text)
.when(is_sent_failed, |this| {
this.child(self.render_message_reports(&id, cx))
.child(rendered_text)
.when(!sent_success, |this| {
this.child(deferred(self.render_message_reports(&id, cx)))
}),
),
)
@@ -693,11 +745,11 @@ impl ChatPanel {
items
}
fn render_message_sent(&self, id: &EventId, _cx: &Context<Self>) -> impl IntoElement {
fn render_sent_indicator(&self, id: &EventId, _cx: &Context<Self>) -> impl IntoElement {
div()
.id(SharedString::from(id.to_hex()))
.child(SharedString::from("• Sent"))
.when_some(self.sent_reports(id).cloned(), |this, reports| {
.when_some(self.sent_reports(id), |this, reports| {
this.on_click(move |_e, window, cx| {
let reports = reports.clone();
@@ -708,8 +760,7 @@ impl ChatPanel {
let mut items = Vec::with_capacity(reports.len());
for report in reports.iter() {
//items.push(Self::render_report(report, cx))
items.push(div())
items.push(Self::render_report(report, cx))
}
items
@@ -730,7 +781,7 @@ impl ChatPanel {
.child(SharedString::from(
"Failed to send message. Click to see details.",
))
.when_some(self.sent_reports(id).cloned(), |this, reports| {
.when_some(self.sent_reports(id), |this, reports| {
this.on_click(move |_e, window, cx| {
let reports = reports.clone();
@@ -741,8 +792,7 @@ impl ChatPanel {
let mut items = Vec::with_capacity(reports.len());
for report in reports.iter() {
//items.push(Self::render_report(report, cx))
items.push(div())
items.push(Self::render_report(report, cx))
}
items
@@ -752,7 +802,6 @@ impl ChatPanel {
})
}
/*
fn render_report(report: &SendReport, cx: &App) -> impl IntoElement {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&report.receiver, cx);
@@ -775,48 +824,6 @@ impl ChatPanel {
.child(name.clone()),
),
)
.when(report.relays_not_found, |this| {
this.child(
h_flex()
.flex_wrap()
.justify_center()
.p_2()
.h_20()
.w_full()
.text_sm()
.rounded(cx.theme().radius)
.bg(cx.theme().danger_background)
.text_color(cx.theme().danger_foreground)
.child(
div()
.flex_1()
.w_full()
.text_center()
.child(SharedString::from("Messaging Relays not found")),
),
)
})
.when(report.device_not_found, |this| {
this.child(
h_flex()
.flex_wrap()
.justify_center()
.p_2()
.h_20()
.w_full()
.text_sm()
.rounded(cx.theme().radius)
.bg(cx.theme().danger_background)
.text_color(cx.theme().danger_foreground)
.child(
div()
.flex_1()
.w_full()
.text_center()
.child(SharedString::from("Encryption Key not found")),
),
)
})
.when_some(report.error.clone(), |this, error| {
this.child(
h_flex()
@@ -832,7 +839,7 @@ impl ChatPanel {
.child(div().flex_1().w_full().text_center().child(error)),
)
})
.when_some(report.status.clone(), |this, output| {
.when_some(report.output.clone(), |this, output| {
this.child(
v_flex()
.gap_2()
@@ -902,7 +909,6 @@ impl ChatPanel {
)
})
}
*/
fn render_border(&self, cx: &Context<Self>) -> impl IntoElement {
div()