feat: revamp the chat panel ui #7

Merged
reya merged 9 commits from revamp-chat-ui into master 2026-02-19 07:25:08 +00:00
8 changed files with 239 additions and 248 deletions
Showing only changes of commit 1d8e3724a8 - Show all commits

2
Cargo.lock generated
View File

@@ -1009,9 +1009,9 @@ dependencies = [
"chat", "chat",
"common", "common",
"dock", "dock",
"flume",
"gpui", "gpui",
"gpui_tokio", "gpui_tokio",
"indexset",
"itertools 0.13.0", "itertools 0.13.0",
"log", "log",
"nostr-sdk", "nostr-sdk",

View File

@@ -14,7 +14,7 @@ use gpui::{
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use state::{tracker, NostrRegistry, RelayState, DEVICE_GIFTWRAP, USER_GIFTWRAP}; use state::{NostrRegistry, RelayState, DEVICE_GIFTWRAP, USER_GIFTWRAP};
mod message; mod message;
mod room; mod room;
@@ -153,19 +153,10 @@ impl ChatRegistry {
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 {
true => { true => {
// Check if the event is sent by coop let new_message = NewMessage::new(event.id, rumor);
let sent_by_coop = { let signal = Signal::Message(new_message);
let tracker = tracker().read().await;
tracker.is_sent_by_coop(&event.id)
};
// No need to emit if sent by coop
// the event is already emitted
if !sent_by_coop {
let new_message = NewMessage::new(event.id, rumor);
let signal = Signal::Message(new_message);
tx.send_async(signal).await.ok(); tx.send_async(signal).await.ok();
}
} }
false => { false => {
status.store(true, Ordering::Release); status.store(true, Ordering::Release);

View File

@@ -30,11 +30,13 @@ impl SendReport {
} }
} }
/// Set the output.
pub fn output(mut self, output: Output<EventId>) -> Self { pub fn output(mut self, output: Output<EventId>) -> Self {
self.output = Some(output); self.output = Some(output);
self self
} }
/// Set the error message.
pub fn error<T>(mut self, error: T) -> Self pub fn error<T>(mut self, error: T) -> Self
where where
T: Into<SharedString>, T: Into<SharedString>,
@@ -43,11 +45,13 @@ impl SendReport {
self self
} }
pub fn is_relay_error(&self) -> bool { /// Returns true if the send is pending.
self.error.is_some() pub fn pending(&self) -> bool {
self.output.is_none() && self.error.is_none()
} }
pub fn is_sent_success(&self) -> bool { /// Returns true if the send was successful.
pub fn success(&self) -> bool {
if let Some(output) = self.output.as_ref() { if let Some(output) = self.output.as_ref() {
!output.success.is_empty() !output.success.is_empty()
} else { } else {

View File

@@ -22,10 +22,10 @@ anyhow.workspace = true
itertools.workspace = true itertools.workspace = true
smallvec.workspace = true smallvec.workspace = true
smol.workspace = true smol.workspace = true
flume.workspace = true
log.workspace = true log.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
indexset = "0.12.3"
once_cell = "1.19.0" once_cell = "1.19.0"
regex = "1" 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::*; pub use actions::*;
use anyhow::{Context as AnyhowContext, Error}; use anyhow::{Context as AnyhowContext, Error};
@@ -7,25 +8,26 @@ use common::{nip96_upload, RenderedTimestamp};
use dock::panel::{Panel, PanelEvent}; use dock::panel::{Panel, PanelEvent};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ 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, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit, ParentElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit, ParentElement,
PathPromptOptions, Render, SharedString, StatefulInteractiveElement, Styled, StyledImage, PathPromptOptions, Render, SharedString, StatefulInteractiveElement, Styled, StyledImage,
Subscription, Task, WeakEntity, Window, Subscription, Task, WeakEntity, Window,
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use indexset::{BTreeMap, BTreeSet};
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::{Person, PersonRegistry}; use person::{Person, PersonRegistry};
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::fs; use smol::fs;
use smol::lock::RwLock;
use state::NostrRegistry; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::context_menu::ContextMenuExt; use ui::context_menu::ContextMenuExt;
use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::notification::Notification; use ui::notification::Notification;
use ui::popup_menu::PopupMenuExt; use ui::popup_menu::PopupMenuExt;
@@ -61,7 +63,7 @@ pub struct ChatPanel {
rendered_texts_by_id: BTreeMap<EventId, RenderedText>, rendered_texts_by_id: BTreeMap<EventId, RenderedText>,
/// Mapping message (rumor event) ids to their reports /// 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 state
input: Entity<InputState>, input: Entity<InputState>,
@@ -76,7 +78,7 @@ pub struct ChatPanel {
uploading: bool, uploading: bool,
/// Async operations /// Async operations
tasks: SmallVec<[Task<Result<(), Error>>; 2]>, tasks: Vec<Task<Result<(), Error>>>,
/// Event subscriptions /// Event subscriptions
subscriptions: SmallVec<[Subscription; 2]>, subscriptions: SmallVec<[Subscription; 2]>,
@@ -125,6 +127,7 @@ impl ChatPanel {
cx.defer_in(window, |this, window, cx| { cx.defer_in(window, |this, window, cx| {
this.subscribe_room_events(window, cx); this.subscribe_room_events(window, cx);
this.connect(window, cx); this.connect(window, cx);
this.handle_notifications(cx);
this.get_messages(window, cx); this.get_messages(window, cx);
}); });
@@ -138,13 +141,46 @@ impl ChatPanel {
replies_to, replies_to,
attachments, attachments,
rendered_texts_by_id: BTreeMap::new(), rendered_texts_by_id: BTreeMap::new(),
reports_by_id: BTreeMap::new(), reports_by_id: Arc::new(RwLock::new(BTreeMap::new())),
uploading: false, uploading: false,
subscriptions, 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>) { fn subscribe_room_events(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(room) = self.room.upgrade() else { let Some(room) = self.room.upgrade() else {
return; return;
@@ -240,6 +276,7 @@ impl ChatPanel {
// Get room entity // Get room entity
let room = self.room.clone(); let room = self.room.clone();
// Get content and replies
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect(); let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
let content = value.to_string(); let content = value.to_string();
@@ -251,6 +288,7 @@ impl ChatPanel {
Some(rumor) => { Some(rumor) => {
this.insert_message(&rumor, true, cx); this.insert_message(&rumor, true, cx);
this.send_and_wait(rumor, window, cx); this.send_and_wait(rumor, window, cx);
this.clear(window, cx);
} }
None => { None => {
window.push_notification("Failed to create message", cx); 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>) { 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 { let Some(room) = self.room.upgrade() else {
return; return;
}; };
@@ -274,14 +316,36 @@ impl ChatPanel {
self.tasks.push(cx.spawn_in(window, async move |this, cx| { self.tasks.push(cx.spawn_in(window, async move |this, cx| {
let outputs = task.await; 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(()) 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>) { 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(); cx.notify();
} }
@@ -315,23 +379,25 @@ impl ChatPanel {
} }
} }
/// Check if a message failed to send by its ID /// Check if a message is pending
fn is_sent_failed(&self, id: &EventId) -> bool { fn sent_pending(&self, id: &EventId) -> bool {
self.reports_by_id self.reports_by_id
.read_blocking()
.get(id) .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 /// 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 self.reports_by_id
.read_blocking()
.get(id) .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 /// Get all sent reports for a message by its ID
fn sent_reports(&self, id: &EventId) -> Option<&Vec<SendReport>> { fn sent_reports(&self, id: &EventId) -> Option<Vec<SendReport>> {
self.reports_by_id.get(id) self.reports_by_id.read_blocking().get(id).cloned()
} }
/// Get a message by its ID /// 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>) { fn upload(&mut self, window: &mut Window, 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();
@@ -401,9 +460,9 @@ impl ChatPanel {
prompt: None, prompt: None,
}); });
cx.spawn_in(window, async move |this, cx| { self.tasks.push(cx.spawn_in(window, async move |this, cx| {
let mut paths = path.await.ok()?.ok()??; let mut paths = path.await??.context("Not found")?;
let path = paths.pop()?; let path = paths.pop().context("No path")?;
let upload = Tokio::spawn(cx, async move { let upload = Tokio::spawn(cx, async move {
let file = fs::read(path).await.ok()?; let file = fs::read(path).await.ok()?;
@@ -432,9 +491,8 @@ impl ChatPanel {
.ok(); .ok();
} }
Some(()) Ok(())
}) }));
.detach();
} }
fn set_uploading(&mut self, uploading: bool, cx: &mut Context<Self>) { 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 { fn profile(&self, public_key: &PublicKey, cx: &Context<Self>) -> Person {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
persons.read(cx).get(public_key, cx) persons.read(cx).get(public_key, cx)
@@ -530,7 +581,7 @@ impl ChatPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> AnyElement { ) -> AnyElement {
if let Some(message) = self.messages.get_index(ix) { if let Some(message) = self.messages.iter().nth(ix) {
match message { match message {
Message::User(rendered) => { Message::User(rendered) => {
let text = self let text = self
@@ -555,7 +606,7 @@ impl ChatPanel {
&self, &self,
ix: usize, ix: usize,
message: &RenderedMessage, message: &RenderedMessage,
text: AnyElement, rendered_text: AnyElement,
cx: &Context<Self>, cx: &Context<Self>,
) -> AnyElement { ) -> AnyElement {
let id = message.id; let id = message.id;
@@ -566,10 +617,10 @@ impl ChatPanel {
let has_replies = !replies.is_empty(); let has_replies = !replies.is_empty();
// Check if message is sent failed // 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 // 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 // Hide avatar setting
let hide_avatar = AppSettings::get_hide_avatar(cx); let hide_avatar = AppSettings::get_hide_avatar(cx);
@@ -617,18 +668,19 @@ impl ChatPanel {
.child(author.name()), .child(author.name()),
) )
.child(message.created_at.to_human_time()) .child(message.created_at.to_human_time())
.when_some(is_sent_success, |this, status| { .when(sent_pending, |this| {
this.when(status, |this| { this.child(deferred(Indicator::new().small()))
this.child(self.render_message_sent(&id, cx)) })
}) .when(sent_success, |this| {
this.child(deferred(self.render_sent_indicator(&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(text) .child(rendered_text)
.when(is_sent_failed, |this| { .when(!sent_success, |this| {
this.child(self.render_message_reports(&id, cx)) this.child(deferred(self.render_message_reports(&id, cx)))
}), }),
), ),
) )
@@ -693,11 +745,11 @@ impl ChatPanel {
items 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() div()
.id(SharedString::from(id.to_hex())) .id(SharedString::from(id.to_hex()))
.child(SharedString::from("• Sent")) .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| { this.on_click(move |_e, window, cx| {
let reports = reports.clone(); let reports = reports.clone();
@@ -708,8 +760,7 @@ impl ChatPanel {
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() {
//items.push(Self::render_report(report, cx)) items.push(Self::render_report(report, cx))
items.push(div())
} }
items items
@@ -730,7 +781,7 @@ impl ChatPanel {
.child(SharedString::from( .child(SharedString::from(
"Failed to send message. Click to see details.", "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| { this.on_click(move |_e, window, cx| {
let reports = reports.clone(); let reports = reports.clone();
@@ -741,8 +792,7 @@ impl ChatPanel {
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() {
//items.push(Self::render_report(report, cx)) items.push(Self::render_report(report, cx))
items.push(div())
} }
items items
@@ -752,7 +802,6 @@ impl ChatPanel {
}) })
} }
/*
fn render_report(report: &SendReport, cx: &App) -> impl IntoElement { fn render_report(report: &SendReport, cx: &App) -> impl IntoElement {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&report.receiver, cx); let profile = persons.read(cx).get(&report.receiver, cx);
@@ -775,48 +824,6 @@ impl ChatPanel {
.child(name.clone()), .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| { .when_some(report.error.clone(), |this, error| {
this.child( this.child(
h_flex() h_flex()
@@ -832,7 +839,7 @@ impl ChatPanel {
.child(div().flex_1().w_full().text_center().child(error)), .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( this.child(
v_flex() v_flex()
.gap_2() .gap_2()
@@ -902,7 +909,6 @@ impl ChatPanel {
) )
}) })
} }
*/
fn render_border(&self, cx: &Context<Self>) -> impl IntoElement { fn render_border(&self, cx: &Context<Self>) -> impl IntoElement {
div() div()

View File

@@ -13,7 +13,7 @@ use gpui::{
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use settings::{AppSettings, AuthMode}; use settings::{AppSettings, AuthMode};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use state::{tracker, NostrRegistry}; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::notification::Notification; use ui::notification::Notification;
@@ -28,7 +28,7 @@ pub fn init(window: &mut Window, cx: &mut App) {
/// Authentication request /// Authentication request
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct AuthRequest { struct AuthRequest {
url: RelayUrl, url: RelayUrl,
challenge: String, challenge: String,
} }
@@ -56,6 +56,12 @@ impl AuthRequest {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Signal {
Auth(Arc<AuthRequest>),
Pending((EventId, RelayUrl)),
}
struct GlobalRelayAuth(Entity<RelayAuth>); struct GlobalRelayAuth(Entity<RelayAuth>);
impl Global for GlobalRelayAuth {} impl Global for GlobalRelayAuth {}
@@ -63,6 +69,9 @@ impl Global for GlobalRelayAuth {}
// Relay authentication // Relay authentication
#[derive(Debug)] #[derive(Debug)]
pub struct RelayAuth { pub struct RelayAuth {
/// Pending events waiting for resend after authentication
pending_events: HashSet<(EventId, RelayUrl)>,
/// Tasks for asynchronous operations /// Tasks for asynchronous operations
tasks: SmallVec<[Task<()>; 2]>, tasks: SmallVec<[Task<()>; 2]>,
@@ -84,89 +93,113 @@ impl RelayAuth {
/// Create a new relay auth instance /// Create a new relay auth instance
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
// Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Arc<AuthRequest>>(100);
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
subscriptions.push( subscriptions.push(
// Observe the current state // Observe the nostr state
cx.observe(&nostr, move |this, state, cx| { cx.observe_in(&nostr, window, move |this, state, window, cx| {
if state.read(cx).connected() { if state.read(cx).connected() {
this.handle_notifications(tx.clone(), cx) this.handle_notifications(window, cx)
}
}),
);
tasks.push(
// Update GPUI states
cx.spawn_in(window, async move |this, cx| {
while let Ok(req) = rx.recv_async().await {
this.update_in(cx, |this, window, cx| {
this.handle_auth(&req, window, cx);
})
.ok();
} }
}), }),
); );
Self { Self {
tasks, pending_events: HashSet::default(),
tasks: smallvec![],
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
} }
// Handle nostr notifications /// Handle nostr notifications
fn handle_notifications( fn handle_notifications(&mut self, window: &mut Window, cx: &mut Context<Self>) {
&mut self,
tx: flume::Sender<Arc<AuthRequest>>,
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 task = cx.background_spawn(async move { // Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Signal>(256);
cx.background_spawn(async move {
let mut notifications = client.notifications(); let mut notifications = client.notifications();
let mut challenges: HashSet<Cow<'_, str>> = HashSet::default(); let mut challenges: HashSet<Cow<'_, str>> = HashSet::default();
while let Some(notification) = notifications.next().await { while let Some(notification) = notifications.next().await {
match notification { if let ClientNotification::Message { relay_url, message } = notification {
ClientNotification::Message { relay_url, message } => { match message {
match message { RelayMessage::Auth { challenge } => {
RelayMessage::Auth { challenge } => { if challenges.insert(challenge.clone()) {
if challenges.insert(challenge.clone()) { let request = AuthRequest::new(challenge, relay_url);
let request = AuthRequest::new(challenge, relay_url); let signal = Signal::Auth(Arc::new(request));
tx.send_async(Arc::new(request)).await.ok();
}
}
RelayMessage::Ok {
event_id, message, ..
} => {
let msg = MachineReadablePrefix::parse(&message);
let mut tracker = tracker().write().await;
// Handle authentication messages tx.send_async(signal).await.ok();
if let Some(MachineReadablePrefix::AuthRequired) = msg {
// Keep track of events that need to be resent after authentication
tracker.add_to_pending(event_id, relay_url);
} else {
// Keep track of events sent by Coop
tracker.sent(event_id)
}
} }
_ => {}
} }
RelayMessage::Ok {
event_id, message, ..
} => {
let msg = MachineReadablePrefix::parse(&message);
// Handle authentication messages
if let Some(MachineReadablePrefix::AuthRequired) = msg {
let signal = Signal::Pending((event_id, relay_url));
tx.send_async(signal).await.ok();
}
}
_ => {}
} }
ClientNotification::Shutdown => break,
_ => {}
} }
} }
}); })
.detach();
self.tasks.push(task); self.tasks.push(cx.spawn_in(window, async move |this, cx| {
while let Ok(signal) = rx.recv_async().await {
match signal {
Signal::Auth(req) => {
this.update_in(cx, |this, window, cx| {
this.handle_auth(&req, window, cx);
})
.ok();
}
Signal::Pending((event_id, relay_url)) => {
this.update_in(cx, |this, _window, cx| {
this.insert_pending_event(event_id, relay_url, cx);
})
.ok();
}
}
}
}));
} }
/// Insert a pending event waiting for resend after authentication
fn insert_pending_event(&mut self, id: EventId, relay: RelayUrl, cx: &mut Context<Self>) {
self.pending_events.insert((id, relay));
cx.notify();
}
/// Get all pending events for a specific relay,
fn get_pending_events(&self, relay: &RelayUrl, _cx: &App) -> Vec<EventId> {
let pending_events: Vec<EventId> = self
.pending_events
.iter()
.filter(|(_, pending_relay)| pending_relay == relay)
.map(|(id, _relay)| id)
.cloned()
.collect();
pending_events
}
/// Clear all pending events for a specific relay,
fn clear_pending_events(&mut self, relay: &RelayUrl, cx: &mut Context<Self>) {
self.pending_events
.retain(|(_, pending_relay)| pending_relay != relay);
cx.notify();
}
/// Handle authentication request
fn handle_auth(&mut self, req: &Arc<AuthRequest>, window: &mut Window, cx: &mut Context<Self>) { fn handle_auth(&mut self, req: &Arc<AuthRequest>, window: &mut Window, cx: &mut Context<Self>) {
let settings = AppSettings::global(cx); let settings = AppSettings::global(cx);
let trusted_relay = settings.read(cx).trusted_relay(req.url(), cx); let trusted_relay = settings.read(cx).trusted_relay(req.url(), cx);
@@ -181,29 +214,25 @@ impl RelayAuth {
} }
} }
/// Respond to an authentication request. /// Send auth response and wait for confirmation
fn response(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) { fn auth(&self, req: &Arc<AuthRequest>, cx: &App) -> Task<Result<(), Error>> {
let settings = AppSettings::global(cx);
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let req = req.clone(); let req = req.clone();
let challenge = req.challenge().to_string();
let async_req = req.clone();
let task: Task<Result<(), Error>> = cx.background_spawn(async move { // Get all pending events for the relay
let pending_events = self.get_pending_events(req.url(), cx);
cx.background_spawn(async move {
// Construct event // Construct event
let builder = EventBuilder::auth(async_req.challenge(), async_req.url().clone()); let builder = EventBuilder::auth(req.challenge(), req.url().clone());
let event = client.sign_event_builder(builder).await?; let event = client.sign_event_builder(builder).await?;
// Get the event ID // Get the event ID
let id = event.id; let id = event.id;
// Get the relay // Get the relay
let relay = client let relay = client.relay(req.url()).await?.context("Relay not found")?;
.relay(async_req.url())
.await?
.context("Relay not found")?;
// Subscribe to notifications // Subscribe to notifications
let mut notifications = relay.notifications(); let mut notifications = relay.notifications();
@@ -219,17 +248,18 @@ impl RelayAuth {
message: RelayMessage::Ok { event_id, .. }, message: RelayMessage::Ok { event_id, .. },
} => { } => {
if id == event_id { if id == event_id {
// Re-subscribe to previous subscription // Get all subscriptions
// relay.resubscribe().await?; let subscriptions = relay.subscriptions().await;
// Get all pending events that need to be resent // Re-subscribe to previous subscriptions
let mut tracker = tracker().write().await; for (id, filters) in subscriptions.into_iter() {
let ids: Vec<EventId> = tracker.pending_resend(relay.url()); relay.subscribe(filters).with_id(id).await?;
}
for id in ids.into_iter() { // Re-send pending events
for id in pending_events {
if let Some(event) = client.database().event_by_id(&id).await? { if let Some(event) = client.database().event_by_id(&id).await? {
let event_id = relay.send_event(&event).await?; relay.send_event(&event).await?;
tracker.sent(event_id);
} }
} }
@@ -242,22 +272,33 @@ impl RelayAuth {
} }
Err(anyhow!("Authentication failed")) Err(anyhow!("Authentication failed"))
}); })
}
/// Respond to an authentication request.
fn response(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) {
let settings = AppSettings::global(cx);
let req = req.clone();
let challenge = req.challenge().to_string();
// Create a task for authentication
let task = self.auth(&req, cx);
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let result = task.await; let result = task.await;
let url = req.url(); let url = req.url();
this.update_in(cx, |_this, window, cx| { this.update_in(cx, |this, window, cx| {
window.clear_notification(challenge, cx); window.clear_notification(challenge, cx);
match result { match result {
Ok(_) => { Ok(_) => {
// Clear pending events for the authenticated relay
this.clear_pending_events(url, cx);
// Save the authenticated relay to automatically authenticate future requests // Save the authenticated relay to automatically authenticate future requests
settings.update(cx, |this, cx| { settings.update(cx, |this, cx| {
this.add_trusted_relay(url, cx); this.add_trusted_relay(url, cx);
}); });
window.push_notification(format!("{} has been authenticated", url), cx); window.push_notification(format!("{} has been authenticated", url), cx);
} }
Err(e) => { Err(e) => {

View File

@@ -1,46 +0,0 @@
use std::collections::HashSet;
use std::sync::{Arc, OnceLock};
use nostr_sdk::prelude::*;
use smol::lock::RwLock;
static TRACKER: OnceLock<Arc<RwLock<EventTracker>>> = OnceLock::new();
pub fn tracker() -> &'static Arc<RwLock<EventTracker>> {
TRACKER.get_or_init(|| Arc::new(RwLock::new(EventTracker::default())))
}
/// Event tracker
#[derive(Debug, Clone, Default)]
pub struct EventTracker {
/// Tracking events sent by Coop in the current session
sent_ids: HashSet<EventId>,
/// Events that need to be resent later
pending_resend: HashSet<(EventId, RelayUrl)>,
}
impl EventTracker {
/// Check if an event was sent by Coop in the current session.
pub fn is_sent_by_coop(&self, id: &EventId) -> bool {
self.sent_ids.contains(id)
}
/// Mark an event as sent by Coop.
pub fn sent(&mut self, id: EventId) {
self.sent_ids.insert(id);
}
/// Get all events that need to be resent later for a specific relay.
pub fn pending_resend(&mut self, relay: &RelayUrl) -> Vec<EventId> {
self.pending_resend
.extract_if(|(_id, url)| url == relay)
.map(|(id, _url)| id)
.collect()
}
/// Add an event (id and relay url) to the pending resend set.
pub fn add_to_pending(&mut self, id: EventId, url: RelayUrl) {
self.pending_resend.insert((id, url));
}
}

View File

@@ -12,12 +12,10 @@ use nostr_lmdb::NostrLmdb;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
mod constants; mod constants;
mod event;
mod nip05; mod nip05;
mod signer; mod signer;
pub use constants::*; pub use constants::*;
pub use event::*;
pub use nip05::*; pub use nip05::*;
pub use signer::*; pub use signer::*;
@@ -32,9 +30,6 @@ pub fn init(cx: &mut App) {
// Initialize the tokio runtime // Initialize the tokio runtime
gpui_tokio::init(cx); gpui_tokio::init(cx);
// Initialize the event tracker
let _tracker = tracker();
NostrRegistry::set_global(cx.new(NostrRegistry::new), cx); NostrRegistry::set_global(cx.new(NostrRegistry::new), cx);
} }