feat: revamp the chat panel ui #7
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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,20 +153,11 @@ 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 sent_by_coop = {
|
|
||||||
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 new_message = NewMessage::new(event.id, rumor);
|
||||||
let signal = Signal::Message(new_message);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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);
|
||||||
tx.send_async(Arc::new(request)).await.ok();
|
let signal = Signal::Auth(Arc::new(request));
|
||||||
|
|
||||||
|
tx.send_async(signal).await.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RelayMessage::Ok {
|
RelayMessage::Ok {
|
||||||
event_id, message, ..
|
event_id, message, ..
|
||||||
} => {
|
} => {
|
||||||
let msg = MachineReadablePrefix::parse(&message);
|
let msg = MachineReadablePrefix::parse(&message);
|
||||||
let mut tracker = tracker().write().await;
|
|
||||||
|
|
||||||
// Handle authentication messages
|
// Handle authentication messages
|
||||||
if let Some(MachineReadablePrefix::AuthRequired) = msg {
|
if let Some(MachineReadablePrefix::AuthRequired) = msg {
|
||||||
// Keep track of events that need to be resent after authentication
|
let signal = Signal::Pending((event_id, relay_url));
|
||||||
tracker.add_to_pending(event_id, relay_url);
|
|
||||||
} else {
|
tx.send_async(signal).await.ok();
|
||||||
// Keep track of events sent by Coop
|
|
||||||
tracker.sent(event_id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) => {
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user