2 Commits
Author SHA1 Message Date
reya b349656c56 . 2026-07-27 10:46:10 +07:00
reya 8bbb472103 optimize 2026-07-27 10:14:19 +07:00
9 changed files with 287 additions and 329 deletions
+123 -137
View File
@@ -1,10 +1,8 @@
use std::cmp::Reverse; use std::cmp::Reverse;
use std::collections::{BTreeSet, HashMap, HashSet}; use std::collections::{BTreeSet, HashMap, HashSet};
use std::hash::{DefaultHasher, Hash, Hasher}; use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
use std::sync::RwLock;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, LazyLock, RwLock};
use std::time::Duration; use std::time::Duration;
use anyhow::{Context as AnyhowContext, Error, anyhow}; use anyhow::{Context as AnyhowContext, Error, anyhow};
@@ -17,8 +15,6 @@ use gpui::{
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
#[cfg(not(target_arch = "wasm32"))]
use smol::lock::RwLock;
use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP, UniversalSigner}; use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP, UniversalSigner};
mod message; mod message;
@@ -27,6 +23,9 @@ mod room;
pub use message::*; pub use message::*;
pub use room::*; pub use room::*;
/// A static keypair used only for signing locally-cached rumor events.
static LOCAL_KEYS: LazyLock<Keys> = LazyLock::new(Keys::generate);
pub fn init(window: &mut Window, cx: &mut App) { pub fn init(window: &mut Window, cx: &mut App) {
ChatRegistry::set_global(cx.new(|cx| ChatRegistry::new(window, cx)), cx); ChatRegistry::set_global(cx.new(|cx| ChatRegistry::new(window, cx)), cx);
} }
@@ -103,6 +102,12 @@ pub struct ChatRegistry {
/// Async tasks /// Async tasks
tasks: SmallVec<[Task<Result<(), Error>>; 2]>, tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
/// Notification listener task (cancelled on signer change)
notification_listener: Option<Task<Result<(), Error>>>,
/// Signal consumer task (cancelled on signer change)
signal_consumer: Option<Task<Result<(), Error>>>,
/// Subscriptions /// Subscriptions
_subscriptions: SmallVec<[Subscription; 2]>, _subscriptions: SmallVec<[Subscription; 2]>,
} }
@@ -153,12 +158,18 @@ impl ChatRegistry {
signal_rx: rx, signal_rx: rx,
signal_tx: tx, signal_tx: tx,
tasks: smallvec![], tasks: smallvec![],
notification_listener: None,
signal_consumer: None,
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
} }
/// Handle nostr notifications /// Handle nostr notifications
fn handle_notifications(&mut self, cx: &mut Context<Self>) { fn handle_notifications(&mut self, cx: &mut Context<Self>) {
// Cancel previous notification tasks before spawning new ones
self.notification_listener = None;
self.signal_consumer = None;
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
@@ -176,7 +187,7 @@ impl ChatRegistry {
let tx = self.signal_tx.clone(); let tx = self.signal_tx.clone();
let rx = self.signal_rx.clone(); let rx = self.signal_rx.clone();
self.tasks.push(cx.background_spawn(async move { self.notification_listener = Some(cx.background_spawn(async move {
let mut notifications = client.notifications(); let mut notifications = client.notifications();
let mut processed_events = HashSet::new(); let mut processed_events = HashSet::new();
@@ -209,7 +220,7 @@ impl ChatRegistry {
// Keep track of which relays have seen this event // Keep track of which relays have seen this event
{ {
let mut seens = seens.write().await; let mut seens = seens.write().unwrap();
seens.entry(event.id).or_default().insert(relay_url); seens.entry(event.id).or_default().insert(relay_url);
} }
@@ -217,9 +228,13 @@ impl ChatRegistry {
match extract_rumor(&client, &signer, event.as_ref()).await { match extract_rumor(&client, &signer, event.as_ref()).await {
Ok(rumor) => { Ok(rumor) => {
// Map the rumor id to the gift wrap event id for later lookup // Map the rumor id to the gift wrap event id for later lookup
let Some(rumor_id) = rumor.id else {
log::error!("Rumor missing id after ensure_id");
continue;
};
{ {
let mut event_map = event_map.write().await; let mut event_map = event_map.write().unwrap();
event_map.insert(rumor.id.unwrap(), event.id); event_map.insert(rumor_id, event.id);
} }
// Check if the rumor has a recipient // Check if the rumor has a recipient
@@ -256,7 +271,7 @@ impl ChatRegistry {
Ok(()) Ok(())
})); }));
self.tasks.push(cx.spawn(async move |this, cx| { self.signal_consumer = Some(cx.spawn(async move |this, cx| {
while let Ok(message) = rx.recv_async().await { while let Ok(message) = rx.recv_async().await {
match message { match message {
Signal::Message(message) => { Signal::Message(message) => {
@@ -287,21 +302,23 @@ impl ChatRegistry {
})); }));
} }
/// Tracking the status of unwrapping gift wrap events. /// Check periodically whether old gift-wrap events have finished processing,
/// and refresh rooms once the backlog is caught up.
fn tracking(&mut self, cx: &mut Context<Self>) { fn tracking(&mut self, cx: &mut Context<Self>) {
let status = self.tracking.clone(); let status = self.tracking.clone();
let tx = self.signal_tx.clone(); let tx = self.signal_tx.clone();
let check_interval = Duration::from_secs(15);
self.tasks.push(cx.spawn(async move |_, cx| { self.tasks.push(cx.spawn(async move |_, cx| {
let loop_duration = Duration::from_secs(15);
loop { loop {
if status.load(Ordering::Acquire) { cx.background_executor().timer(check_interval).await;
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
_ = tx.send_async(Signal::Eose).await; // Only trigger a room refresh if old events were being tracked
} else { // (i.e. the notification handler set the flag while catching up).
// `swap` atomically reads and clears the flag.
if status.swap(false, Ordering::AcqRel) {
_ = tx.send_async(Signal::Eose).await; _ = tx.send_async(Signal::Eose).await;
} }
cx.background_executor().timer(loop_duration).await;
} }
})); }));
} }
@@ -315,55 +332,51 @@ impl ChatRegistry {
return; return;
}; };
self.tasks.push(cx.background_spawn(async move { let subscribe = cx.background_spawn({
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let client = client.clone();
// Construct filter for msg relays async move {
let msg_relays = Filter::new() let opts =
SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let msg_relays = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
let contact_list = Filter::new()
.kind(Kind::ContactList)
.author(public_key)
.limit(1);
client
.subscribe(vec![msg_relays, contact_list])
.close_on(opts)
.await
}
});
self.tasks.push(cx.spawn(async move |this, cx| {
// Wait for subscription to complete (or fail silently)
_ = subscribe.await;
// Give relays time to respond
cx.background_executor().timer(Duration::from_secs(5)).await;
let filter = Filter::new()
.kind(Kind::InboxRelays) .kind(Kind::InboxRelays)
.author(public_key) .author(public_key)
.limit(1); .limit(1);
// Construct filter for contact list let found = client
let contact_list = Filter::new() .database()
.kind(Kind::ContactList) .query(filter)
.author(public_key)
.limit(1);
// Subscribe
client
.subscribe(vec![msg_relays, contact_list])
.close_on(opts)
.await?;
Ok(())
}));
let client = nostr.read(cx).client();
// Spawn a task to verify user inbox relays after 5 seconds
self.tasks.push(cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(5)).await;
if !cx
.background_spawn(async move {
// Construct inbox relays filter
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
// Check the latest inbox relays event in database
client
.database()
.query(filter)
.await
.unwrap_or_default()
.first_owned()
.is_some()
})
.await .await
{ .unwrap_or_default()
.first_owned()
.is_some();
if !found {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(ChatEvent::InboxRelayNotFound); cx.emit(ChatEvent::InboxRelayNotFound);
})?; })?;
@@ -379,52 +392,46 @@ impl ChatRegistry {
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let public_key = signer.get_public_key_async().await?;
// Construct inbox relays filter
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
// Get the latest inbox relays event in database
let event = client
.database()
.query(filter)
.await?
.first_owned()
.ok_or(anyhow::anyhow!("No inbox relays found"))?;
// Extract relay list from event
let relays: Vec<RelayUrl> = nip17::extract_relay_list(&event).collect();
// Ensure relay connections
for url in relays.iter() {
client.add_relay(url).and_connect().await?;
}
// Construct gift wrap event filter
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(format!("{}-msg", public_key.to_hex()));
// Construct target for subscription
let target: HashMap<RelayUrl, Filter> = relays
.into_iter()
.map(|relay| (relay, filter.clone()))
.collect();
client.subscribe(target).with_id(id).await?;
Ok(())
});
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let public_key = signer.get_public_key_async().await?;
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
let event = client
.database()
.query(filter)
.await?
.first_owned()
.ok_or(anyhow::anyhow!("No inbox relays found"))?;
let relays: Vec<RelayUrl> = nip17::extract_relay_list(&event).collect();
for url in relays.iter() {
client.add_relay(url).and_connect().await?;
}
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(format!("{}-msg", public_key.to_hex()));
let target: HashMap<RelayUrl, Filter> = relays
.into_iter()
.map(|relay| (relay, filter.clone()))
.collect();
client.subscribe(target).with_id(id).await?;
Ok(())
});
if let Err(e) = task.await { if let Err(e) = task.await {
this.update(cx, |_this, cx| { this.update(cx, |_this, cx| {
cx.emit(ChatEvent::Error(e.to_string())); cx.emit(ChatEvent::Error(e.to_string()));
})?; })?;
} }
Ok(()) Ok(())
})); }));
} }
@@ -469,7 +476,8 @@ impl ChatRegistry {
/// Count the number of messages seen by a given relay. /// Count the number of messages seen by a given relay.
pub fn count_messages(&self, relay_url: &RelayUrl) -> usize { pub fn count_messages(&self, relay_url: &RelayUrl) -> usize {
self.seens self.seens
.read_blocking() .read()
.unwrap()
.values() .values()
.filter(|seen| seen.contains(relay_url)) .filter(|seen| seen.contains(relay_url))
.count() .count()
@@ -488,7 +496,8 @@ impl ChatRegistry {
/// Get the relays that have seen a given rumor id. /// Get the relays that have seen a given rumor id.
pub fn rumor_seen_on(&self, id: &EventId) -> Option<HashSet<RelayUrl>> { pub fn rumor_seen_on(&self, id: &EventId) -> Option<HashSet<RelayUrl>> {
self.event_map self.event_map
.read_blocking() .read()
.unwrap()
.get(id) .get(id)
.map(|id| self.seen_on(id)) .map(|id| self.seen_on(id))
} }
@@ -496,7 +505,8 @@ impl ChatRegistry {
/// Get the relays that have seen a given gift wrap id. /// Get the relays that have seen a given gift wrap id.
pub fn seen_on(&self, id: &EventId) -> HashSet<RelayUrl> { pub fn seen_on(&self, id: &EventId) -> HashSet<RelayUrl> {
self.seens self.seens
.read_blocking() .read()
.unwrap()
.get(id) .get(id)
.cloned() .cloned()
.unwrap_or_default() .unwrap_or_default()
@@ -505,25 +515,18 @@ impl ChatRegistry {
/// Add a new room to the start of list. /// Add a new room to the start of list.
pub fn add_room<I>(&mut self, room: I, cx: &mut Context<Self>) pub fn add_room<I>(&mut self, room: I, cx: &mut Context<Self>)
where where
I: Into<Room> + 'static, I: Into<Room>,
{ {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let Some(public_key) = nostr.read(cx).current_user() else { let Some(public_key) = nostr.read(cx).current_user() else {
return; return;
}; };
cx.spawn(async move |this, cx| { let room: Room = room.into().organize(&public_key);
let room: Room = room.into().organize(&public_key); self.rooms.insert(0, cx.new(|_| room));
this.update(cx, |this, cx| { cx.emit(ChatEvent::Ping);
this.rooms.insert(0, cx.new(|_| room)); cx.notify();
cx.emit(ChatEvent::Ping);
cx.notify();
})
.ok()
})
.detach();
} }
/// Emit an open room event. /// Emit an open room event.
@@ -635,7 +638,9 @@ impl ChatRegistry {
})?; })?;
} }
Err(e) => { Err(e) => {
log::error!("Failed to load rooms: {}", e); this.update(cx, |_, cx| {
cx.emit(ChatEvent::Error(e.to_string()));
})?;
} }
}; };
@@ -775,7 +780,7 @@ async fn extract_rumor(
} }
// Try to unwrap with the available signer // Try to unwrap with the available signer
let unwrapped = try_unwrap(signer, gift_wrap).await?; let unwrapped = try_unwrap_with(signer, gift_wrap).await?;
let mut rumor = unwrapped.rumor; let mut rumor = unwrapped.rumor;
// Generate event id for the rumor if it doesn't have one // Generate event id for the rumor if it doesn't have one
@@ -789,25 +794,6 @@ async fn extract_rumor(
Ok(rumor) Ok(rumor)
} }
/// Helper method to try unwrapping with different signers
async fn try_unwrap(signer: &UniversalSigner, gift_wrap: &Event) -> Result<UnwrappedGift, Error> {
/*
* // Try with the device signer first
if let Some(signer) = signer.get_encryption_signer().await {
log::info!("trying with encryption key");
if let Ok(unwrapped) = try_unwrap_with(gift_wrap, &signer).await {
return Ok(unwrapped);
}
}
// Fallback to the user's signer
let user_signer = signer.get().await;
*/
let unwrapped = try_unwrap_with(signer, gift_wrap).await?;
Ok(unwrapped)
}
/// Attempts to unwrap a gift wrap event with a given signer. /// Attempts to unwrap a gift wrap event with a given signer.
async fn try_unwrap_with( async fn try_unwrap_with(
signer: &UniversalSigner, signer: &UniversalSigner,
@@ -866,7 +852,7 @@ async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Resul
// Construct the event // Construct the event
let event = EventBuilder::new(Kind::ApplicationSpecificData, content) let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
.tags(tags) .tags(tags)
.finalize_async(&Keys::generate()) .finalize_async(&*LOCAL_KEYS)
.await?; .await?;
// Save the event to the database // Save the event to the database
+43 -63
View File
@@ -1,7 +1,7 @@
use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::sync::Arc;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use std::sync::RwLock; use std::sync::RwLock as AsyncRwLock;
use std::sync::{Arc, RwLock};
pub use actions::*; pub use actions::*;
use anyhow::{Context as AnyhowContext, Error}; use anyhow::{Context as AnyhowContext, Error};
@@ -21,7 +21,7 @@ use person::{Person, PersonRegistry};
use settings::{AppSettings, SignerKind}; use settings::{AppSettings, SignerKind};
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use smol::lock::RwLock; use smol::lock::RwLock as AsyncRwLock;
use state::{NostrRegistry, upload}; use state::{NostrRegistry, upload};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
@@ -63,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: Entity<BTreeMap<EventId, Vec<SendReport>>>, reports_by_id: Arc<RwLock<BTreeMap<EventId, Vec<SendReport>>>>,
/// Chat input state /// Chat input state
input: Entity<InputState>, input: Entity<InputState>,
@@ -75,7 +75,7 @@ pub struct ChatPanel {
subject_bar: Entity<bool>, subject_bar: Entity<bool>,
/// Sent message ids /// Sent message ids
sent_ids: Arc<RwLock<Vec<EventId>>>, sent_ids: Arc<AsyncRwLock<Vec<EventId>>>,
/// Replies to /// Replies to
replies_to: Entity<HashSet<EventId>>, replies_to: Entity<HashSet<EventId>>,
@@ -98,7 +98,7 @@ impl ChatPanel {
// Define attachments and replies_to entities // Define attachments and replies_to entities
let attachments = cx.new(|_| vec![]); let attachments = cx.new(|_| vec![]);
let replies_to = cx.new(|_| HashSet::new()); let replies_to = cx.new(|_| HashSet::new());
let reports_by_id = cx.new(|_| BTreeMap::new()); let reports_by_id = Arc::new(RwLock::new(BTreeMap::new()));
// Define list of messages // Define list of messages
let messages = BTreeSet::default(); let messages = BTreeSet::default();
@@ -172,7 +172,7 @@ impl ChatPanel {
attachments, attachments,
rendered_texts_by_id: BTreeMap::new(), rendered_texts_by_id: BTreeMap::new(),
reports_by_id, reports_by_id,
sent_ids: Arc::new(RwLock::new(Vec::new())), sent_ids: Arc::new(AsyncRwLock::new(Vec::new())),
uploading: false, uploading: false,
subscriptions, subscriptions,
tasks: vec![], tasks: vec![],
@@ -192,7 +192,7 @@ 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 reports = self.reports_by_id.clone();
let (tx, rx) = flume::bounded::<Arc<SendStatus>>(256); let (tx, rx) = flume::bounded::<Arc<SendStatus>>(256);
@@ -223,10 +223,11 @@ 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(status) = rx.recv_async().await { while let Ok(status) = rx.recv_async().await {
reports.update(cx, |this, cx| { {
for reports in this.values_mut() { let mut map = reports.write().unwrap();
for reports in map.values_mut() {
for report in reports.iter_mut() { for report in reports.iter_mut() {
let Some(output) = report.output.as_mut() else { let Some(output) = report.output.as_mut() else {
continue; continue;
@@ -243,10 +244,10 @@ impl ChatPanel {
} }
} }
} }
cx.notify();
} }
} }
})?; }
this.update(cx, |_, cx| cx.notify()).ok();
} }
Ok(()) Ok(())
})); }));
@@ -345,69 +346,46 @@ impl ChatPanel {
return; return;
} }
// 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();
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
let room = room.upgrade().context("Room is not available")?;
this.update_in(cx, |this, window, cx| {
match room.read(cx).rumor(content, replies, cx) {
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);
}
}
})?;
Ok(())
}));
}
/// 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>) {
let sent_ids = self.sent_ids.clone(); let sent_ids = self.sent_ids.clone();
// This can't fail, because we already ensured that the ID is set // Upgrade room and create the rumor synchronously
let id = rumor.id.unwrap(); let Some(room_entity) = room.upgrade() else {
return;
// Add empty reports };
self.insert_reports(id, vec![], cx); let Some(rumor) = room_entity.read(cx).rumor(content.clone(), replies, cx) else {
window.push_notification("Failed to create message", cx);
// Upgrade room reference
let Some(room) = self.room.upgrade() else {
return; return;
}; };
// Get the send message task let id = rumor.id.expect("rumor must have an id");
let Some(task) = room.read(cx).send(rumor, cx) else {
// Insert optimistic message and clear input
self.insert_message(&rumor, true, cx);
self.insert_reports(id, vec![], cx);
self.clear(window, cx);
// Get the send task
let Some(send_task) = room_entity.read(cx).send(rumor, cx) else {
window.push_notification("Failed to send message", cx); window.push_notification("Failed to send message", cx);
return; return;
}; };
// Spawn a single task to await the send and update reports
self.tasks.push(cx.spawn_in(window, async move |this, cx| { self.tasks.push(cx.spawn_in(window, async move |this, cx| {
// Send and get reports let outputs = send_task.await;
let outputs = task.await;
// Add sent IDs to the list
let mut sent_ids = sent_ids.write().await; let mut sent_ids = sent_ids.write().await;
sent_ids.extend(outputs.iter().filter_map(|output| output.gift_wrap_id)); sent_ids.extend(outputs.iter().filter_map(|output| output.gift_wrap_id));
// Update the state
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.insert_reports(id, outputs, cx); this.insert_reports(id, outputs, cx);
})?; })?;
Ok(()) Ok(())
})) }));
} }
/// Clear the input field, attachments, and replies /// Clear the input field, attachments, and replies
@@ -429,10 +407,13 @@ impl ChatPanel {
/// Insert reports /// 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.update(cx, |this, cx| { self.reports_by_id
this.entry(id).or_default().extend(reports); .write()
cx.notify(); .unwrap()
}); .entry(id)
.or_default()
.extend(reports);
cx.notify();
} }
/// Insert a message into the chat panel /// Insert a message into the chat panel
@@ -466,13 +447,12 @@ impl ChatPanel {
} }
/// Check if a message has any reports /// Check if a message has any reports
fn has_reports(&self, id: &EventId, cx: &App) -> bool { fn has_reports(&self, id: &EventId, _cx: &App) -> bool {
self.reports_by_id.read(cx).get(id).is_some() self.reports_by_id.read().unwrap().get(id).is_some()
} }
/// Get all sent reports for a message by its ID fn sent_reports(&self, id: &EventId, _cx: &App) -> Option<Vec<SendReport>> {
fn sent_reports(&self, id: &EventId, cx: &App) -> Option<Vec<SendReport>> { self.reports_by_id.read().unwrap().get(id).cloned()
self.reports_by_id.read(cx).get(id).cloned()
} }
/// Get a message by its ID /// Get a message by its ID
+36 -44
View File
@@ -258,28 +258,20 @@ impl DeviceRegistry {
})); }));
let announcement_existed = self.announcement_existed.clone(); let announcement_existed = self.announcement_existed.clone();
let executor = cx.background_executor().clone();
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
if !cx // Wait for 5 seconds
.background_spawn(async move { cx.background_executor().timer(Duration::from_secs(5)).await;
// Wait for 5 seconds
executor.timer(Duration::from_secs(5)).await;
// Then check if the msg relays have been found // Then check if the msg relays have been found
if !announcement_existed.load(Ordering::Acquire) { if announcement_existed.load(Ordering::Acquire) {
return true; return Ok(());
}
false
})
.await
{
this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::NotSet);
})?;
} }
this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::NotSet);
})?;
Ok(()) Ok(())
})); }));
} }
@@ -404,11 +396,10 @@ impl DeviceRegistry {
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let Ok(app_keys) = get_or_init_app_keys(cx) else { let app_keys_task = get_or_init_app_keys(cx);
return;
};
let task: Task<Result<Option<Event>, Error>> = cx.background_spawn(async move { let task: Task<Result<Option<Event>, Error>> = cx.background_spawn(async move {
let app_keys = app_keys_task.await?;
let app_pubkey = app_keys.public_key(); let app_pubkey = app_keys.public_key();
let public_key = signer.get_public_key_async().await?; let public_key = signer.get_public_key_async().await?;
@@ -489,11 +480,10 @@ impl DeviceRegistry {
/// Parse the approval event to get encryption key then set it /// Parse the approval event to get encryption key then set it
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) { fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
let Ok(app_keys) = get_or_init_app_keys(cx) else { let app_keys_task = get_or_init_app_keys(cx);
return;
};
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move { let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
let app_keys = app_keys_task.await?;
let master = event let master = event
.tags .tags
.iter() .iter()
@@ -573,7 +563,7 @@ impl DeviceRegistry {
Ok(()) Ok(())
}); });
cx.spawn_in(window, async move |_this, cx| { self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
match task.await { match task.await {
Ok(_) => { Ok(_) => {
cx.update(|window, cx| { cx.update(|window, cx| {
@@ -591,8 +581,9 @@ impl DeviceRegistry {
.ok(); .ok();
} }
}; };
})
.detach(); Ok(())
}));
} }
/// Handle encryption request /// Handle encryption request
@@ -715,33 +706,34 @@ impl DeviceRegistry {
struct DeviceNotification; struct DeviceNotification;
/// Get or create new app keys /// Get or create new app keys (async, returns a task)
fn get_or_init_app_keys(cx: &App) -> Result<Keys, Error> { fn get_or_init_app_keys(cx: &App) -> Task<Result<Keys, Error>> {
let read = cx.read_credentials(CLIENT_NAME); let read = cx.read_credentials(CLIENT_NAME);
let stored_keys: Option<Keys> = cx.foreground_executor().block_on(async move {
if let Ok(Some((_, secret))) = read.await {
SecretKey::from_slice(&secret).map(Keys::new).ok()
} else {
None
}
});
if let Some(keys) = stored_keys { cx.spawn(async move |cx| {
Ok(keys) if let Ok(Some((_, secret))) = read.await
} else { && let Ok(keys) = SecretKey::from_slice(&secret).map(Keys::new)
{
return Ok(keys);
}
// No stored keys found or invalid — generate new ones
let keys = Keys::generate(); let keys = Keys::generate();
let user = keys.public_key().to_hex(); let user = keys.public_key().to_hex();
let secret = keys.secret_key().to_secret_bytes(); let secret = keys.secret_key().to_secret_bytes();
let write = cx.write_credentials(CLIENT_NAME, &user, &secret);
cx.foreground_executor().block_on(async move { cx.update(|cx| {
if let Err(e) = write.await { let write = cx.write_credentials(CLIENT_NAME, &user, &secret);
log::error!("Keyring not available or panic: {e}") cx.background_spawn(async move {
} if let Err(e) = write.await {
log::error!("Keyring not available or panic: {e}")
}
})
.detach();
}); });
Ok(keys) Ok(keys)
} })
} }
/// Encrypt and store device keys in the local database. /// Encrypt and store device keys in the local database.
+57 -65
View File
@@ -1,6 +1,5 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use anyhow::{Error, anyhow}; use anyhow::{Error, anyhow};
@@ -24,9 +23,9 @@ impl Global for GlobalPersonRegistry {}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Dispatch { enum Dispatch {
Person(Box<Person>), Person(Person),
Announcement(Box<Event>), Announcement(Event),
Relays(Box<Event>), Relays(Event),
} }
/// Person Registry /// Person Registry
@@ -36,7 +35,7 @@ pub struct PersonRegistry {
persons: HashMap<PublicKey, Entity<Person>>, persons: HashMap<PublicKey, Entity<Person>>,
/// Set of public keys that have been seen /// Set of public keys that have been seen
seens: Rc<RefCell<HashSet<PublicKey>>>, seens: RefCell<HashSet<PublicKey>>,
/// Sender for requesting metadata /// Sender for requesting metadata
sender: flume::Sender<PublicKey>, sender: flume::Sender<PublicKey>,
@@ -67,49 +66,38 @@ impl PersonRegistry {
let mut tasks = smallvec![]; let mut tasks = smallvec![];
tasks.push( tasks.push(cx.background_spawn({
// Handle nostr notifications let client = client.clone();
cx.background_spawn({ async move {
let client = client.clone(); Self::handle_notifications(&client, &tx).await;
}
}));
async move { tasks.push(cx.background_spawn({
Self::handle_notifications(&client, &tx).await; let client = client.clone();
} async move {
}), Self::handle_requests(&client, &mta_rx).await;
); }
}));
tasks.push( tasks.push(cx.spawn(async move |this, cx| {
// Handle metadata requests while let Ok(event) = rx.recv_async().await {
cx.background_spawn({ this.update(cx, |this, cx| {
let client = client.clone(); match event {
Dispatch::Person(person) => {
async move { this.insert(person, cx);
Self::handle_requests(&client, &mta_rx).await; }
} Dispatch::Announcement(event) => {
}), this.set_announcement(&event, cx);
); }
Dispatch::Relays(event) => {
tasks.push( this.set_messaging_relays(&event, cx);
// Update GPUI state }
cx.spawn(async move |this, cx| { };
while let Ok(event) = rx.recv_async().await { })
this.update(cx, |this, cx| { .ok();
match event { }
Dispatch::Person(person) => { }));
this.insert(*person, cx);
}
Dispatch::Announcement(event) => {
this.set_announcement(&event, cx);
}
Dispatch::Relays(event) => {
this.set_messaging_relays(&event, cx);
}
};
})
.ok();
}
}),
);
// Load all user profiles from the database // Load all user profiles from the database
cx.defer_in(window, |this, _window, cx| { cx.defer_in(window, |this, _window, cx| {
@@ -118,7 +106,7 @@ impl PersonRegistry {
Self { Self {
persons: HashMap::new(), persons: HashMap::new(),
seens: Rc::new(RefCell::new(HashSet::new())), seens: RefCell::new(HashSet::new()),
sender: mta_tx, sender: mta_tx,
tasks, tasks,
} }
@@ -145,24 +133,25 @@ impl PersonRegistry {
Kind::Metadata => { Kind::Metadata => {
let metadata = Metadata::from_json(&event.content).unwrap_or_default(); let metadata = Metadata::from_json(&event.content).unwrap_or_default();
let person = Person::new(event.pubkey, metadata); let person = Person::new(event.pubkey, metadata);
let val = Box::new(person); if tx.send_async(Dispatch::Person(person)).await.is_err() {
// Send log::warn!("PersonRegistry channel closed, dropping metadata event");
tx.send_async(Dispatch::Person(val)).await.ok(); }
} }
Kind::ContactList => { Kind::ContactList => {
let public_keys = event.extract_public_keys(); let public_keys = event.extract_public_keys();
// Get metadata for all public keys if let Err(e) = get_metadata(client, public_keys).await {
get_metadata(client, public_keys).await.ok(); log::warn!("Failed to get metadata for contact list: {e}");
}
} }
Kind::InboxRelays => { Kind::InboxRelays => {
let val = Box::new(event.into_owned()); tx.send_async(Dispatch::Relays(event.into_owned()))
// Send .await
tx.send_async(Dispatch::Relays(val)).await.ok(); .ok();
} }
Kind::Custom(10044) => { Kind::Custom(10044) => {
let val = Box::new(event.into_owned()); tx.send_async(Dispatch::Announcement(event.into_owned()))
// Send .await
tx.send_async(Dispatch::Announcement(val)).await.ok(); .ok();
} }
_ => {} _ => {}
} }
@@ -182,13 +171,17 @@ impl PersonRegistry {
Ok(Some(public_key)) => { Ok(Some(public_key)) => {
batch.insert(public_key); batch.insert(public_key);
// Process the batch if it's full // Process the batch if it's full
if batch.len() >= 20 { if batch.len() >= 20
get_metadata(client, std::mem::take(&mut batch)).await.ok(); && let Err(e) = get_metadata(client, std::mem::take(&mut batch)).await
{
log::warn!("Failed to get metadata batch: {e}");
} }
} }
_ => { _ => {
if !batch.is_empty() { if !batch.is_empty()
get_metadata(client, std::mem::take(&mut batch)).await.ok(); && let Err(e) = get_metadata(client, std::mem::take(&mut batch)).await
{
log::warn!("Failed to get metadata batch: {e}");
} }
} }
} }
@@ -290,15 +283,14 @@ impl PersonRegistry {
} }
let public_key = *public_key; let public_key = *public_key;
let mut seen = self.seens.borrow_mut();
if seen.insert(public_key) { if self.seens.borrow_mut().insert(public_key) {
let sender = self.sender.clone(); let sender = self.sender.clone();
// Spawn background task to request metadata // Spawn background task to request metadata
cx.background_spawn(async move { cx.background_spawn(async move {
if let Err(e) = sender.send_async(public_key).await { if let Err(e) = sender.send_async(public_key).await {
log::warn!("Failed to send public key for metadata request: {}", e); log::warn!("Failed to send public key for metadata request: {e}");
} }
}) })
.detach(); .detach();
+4 -4
View File
@@ -164,7 +164,7 @@ impl NostrRegistry {
<T as AsyncSignEvent>::Error: std::error::Error + Send + Sync + 'static, <T as AsyncSignEvent>::Error: std::error::Error + Send + Sync + 'static,
<T as AsyncNip44>::Error: std::error::Error + Send + Sync + 'static, <T as AsyncNip44>::Error: std::error::Error + Send + Sync + 'static,
{ {
cx.spawn(async move |this, cx| { let task = cx.spawn(async move |this, cx| {
match new_signer.get_public_key_async().await { match new_signer.get_public_key_async().await {
Ok(public_key) => { Ok(public_key) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
@@ -181,9 +181,9 @@ impl NostrRegistry {
} }
}; };
Ok::<(), anyhow::Error>(()) Ok(())
}) });
.detach(); self.tasks.push(task);
} }
/// Connect to the bootstrapping relays /// Connect to the bootstrapping relays
+3 -4
View File
@@ -146,7 +146,7 @@ impl ImportIdentity {
let password = uri.to_string(); let password = uri.to_string();
let save = cx.write_credentials(USER_KEYRING, "bunker", password.as_bytes()); let save = cx.write_credentials(USER_KEYRING, "bunker", password.as_bytes());
cx.spawn_in(window, async move |_this, cx| { self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
let keys = master_keys.await; let keys = master_keys.await;
let timeout = Duration::from_secs(30); let timeout = Duration::from_secs(30);
@@ -162,9 +162,8 @@ impl ImportIdentity {
cx.notify(); cx.notify();
}); });
Ok::<(), anyhow::Error>(()) Ok(())
}) }));
.detach();
} }
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) { fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
+9 -5
View File
@@ -1,13 +1,14 @@
use std::sync::Arc; use std::sync::Arc;
use ::settings::AppSettings; use ::settings::AppSettings;
use anyhow::Error;
use chat::{ChatEvent, ChatRegistry}; use chat::{ChatEvent, ChatRegistry};
use common::{CoopImageCache, download_dir}; use common::{CoopImageCache, download_dir};
use device::{DeviceEvent, DeviceRegistry}; use device::{DeviceEvent, DeviceRegistry};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement, Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, div, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, div,
image_cache, px, relative, image_cache, px, relative,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
@@ -65,6 +66,9 @@ pub struct Workspace {
/// App's Image Cache /// App's Image Cache
image_cache: Entity<CoopImageCache>, image_cache: Entity<CoopImageCache>,
/// Async tasks
tasks: Vec<Task<Result<(), Error>>>,
/// Event subscriptions /// Event subscriptions
_subscriptions: SmallVec<[Subscription; 6]>, _subscriptions: SmallVec<[Subscription; 6]>,
} }
@@ -245,6 +249,7 @@ impl Workspace {
Self { Self {
dock, dock,
image_cache, image_cache,
tasks: vec![],
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
} }
@@ -390,7 +395,7 @@ impl Workspace {
let device = DeviceRegistry::global(cx).downgrade(); let device = DeviceRegistry::global(cx).downgrade();
let save_dialog = cx.prompt_for_new_path(download_dir(), Some("encryption.txt")); let save_dialog = cx.prompt_for_new_path(download_dir(), Some("encryption.txt"));
cx.spawn_in(window, async move |_this, cx| { self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
// Get the output path from the save dialog // Get the output path from the save dialog
let output_path = match save_dialog.await { let output_path = match save_dialog.await {
Ok(Ok(Some(path))) => path, Ok(Ok(Some(path))) => path,
@@ -417,9 +422,8 @@ impl Workspace {
cx.open_with_system(output_path.as_path()); cx.open_with_system(output_path.as_path());
})?; })?;
Ok::<_, anyhow::Error>(()) Ok(())
}) }));
.detach();
} }
Command::ImportEncryption => { Command::ImportEncryption => {
self.import_encryption(window, cx); self.import_encryption(window, cx);
+8 -4
View File
@@ -1,6 +1,7 @@
use anyhow::Error;
use gpui::{ use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Window, div, svg, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div, svg,
}; };
use state::NostrRegistry; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -18,6 +19,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
pub struct GreeterPanel { pub struct GreeterPanel {
name: SharedString, name: SharedString,
focus_handle: FocusHandle, focus_handle: FocusHandle,
tasks: Vec<Task<Result<(), Error>>>,
} }
impl GreeterPanel { impl GreeterPanel {
@@ -25,6 +27,7 @@ impl GreeterPanel {
Self { Self {
name: "Onboarding".into(), name: "Onboarding".into(),
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
tasks: vec![],
} }
} }
@@ -32,7 +35,7 @@ impl GreeterPanel {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
if let Some(public_key) = nostr.read(cx).current_user() { if let Some(public_key) = nostr.read(cx).current_user() {
cx.spawn_in(window, async move |_this, cx| { self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
cx.update(|window, cx| { cx.update(|window, cx| {
Workspace::add_panel( Workspace::add_panel(
profile::init(public_key, window, cx), profile::init(public_key, window, cx),
@@ -42,8 +45,9 @@ impl GreeterPanel {
); );
}) })
.ok(); .ok();
})
.detach(); Ok(())
}));
} }
} }
} }
+4 -3
View File
@@ -132,7 +132,7 @@ impl ProfilePanel {
cx.notify(); cx.notify();
if status { if status {
cx.spawn_in(window, async move |this, cx| { self.tasks.push(cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await; cx.background_executor().timer(Duration::from_secs(2)).await;
// Reset the copied state after a delay // Reset the copied state after a delay
@@ -143,8 +143,9 @@ impl ProfilePanel {
.ok(); .ok();
}) })
.ok(); .ok();
})
.detach(); Ok(())
}));
} }
} }