chore: improve the event loop (#141)

* improve wait for signer

* refactor gift wrap processor

* .

* .

* .

* .

* .
This commit is contained in:
reya
2025-09-05 19:01:26 +07:00
committed by GitHub
parent 70e235dcc2
commit ede41c41c3
11 changed files with 289 additions and 219 deletions

View File

@@ -1,5 +1,6 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -10,9 +11,9 @@ use common::display::ReadableProfile;
use common::event::EventUtils; use common::event::EventUtils;
use global::constants::{ use global::constants::{
ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH, METADATA_BATCH_LIMIT, ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH, METADATA_BATCH_LIMIT,
METADATA_BATCH_TIMEOUT, RELAY_RETRY, SEARCH_RELAYS, WAIT_FOR_FINISH, METADATA_BATCH_TIMEOUT, SEARCH_RELAYS,
}; };
use global::{css, ingester, nostr_client, AuthRequest, IngesterSignal, Notice}; use global::{css, ingester, nostr_client, AuthRequest, Notice, Signal};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, px, rems, App, AppContext, AsyncWindowContext, Axis, Context, Entity, InteractiveElement, div, px, rems, App, AppContext, AsyncWindowContext, Axis, Context, Entity, InteractiveElement,
@@ -28,7 +29,6 @@ use settings::AppSettings;
use signer_proxy::{BrowserSignerProxy, BrowserSignerProxyOptions}; use signer_proxy::{BrowserSignerProxy, BrowserSignerProxyOptions};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::channel::{Receiver, Sender}; use smol::channel::{Receiver, Sender};
use smol::lock::Mutex;
use theme::{ActiveTheme, Theme, ThemeMode}; use theme::{ActiveTheme, Theme, ThemeMode};
use title_bar::TitleBar; use title_bar::TitleBar;
use ui::actions::OpenProfile; use ui::actions::OpenProfile;
@@ -64,20 +64,6 @@ pub fn new_account(window: &mut Window, cx: &mut App) {
ChatSpace::set_center_panel(panel, window, cx); ChatSpace::set_center_panel(panel, window, cx);
} }
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
enum RelayTrackStatus {
#[default]
Waiting,
NotFound,
Found,
}
#[derive(Debug, Clone, Default)]
struct RelayTracking {
nip17: RelayTrackStatus,
nip65: RelayTrackStatus,
}
pub struct ChatSpace { pub struct ChatSpace {
title_bar: Entity<TitleBar>, title_bar: Entity<TitleBar>,
dock: Entity<DockArea>, dock: Entity<DockArea>,
@@ -95,13 +81,7 @@ impl ChatSpace {
let title_bar = cx.new(|_| TitleBar::new()); let title_bar = cx.new(|_| TitleBar::new());
let dock = cx.new(|cx| DockArea::new(window, cx)); let dock = cx.new(|cx| DockArea::new(window, cx));
let relay_tracking = Arc::new(Mutex::new(RelayTracking::default()));
let relay_tracking_clone = relay_tracking.clone();
let (pubkey_tx, pubkey_rx) = smol::channel::bounded::<PublicKey>(1024); let (pubkey_tx, pubkey_rx) = smol::channel::bounded::<PublicKey>(1024);
let (event_tx, event_rx) = smol::channel::bounded::<Event>(2048);
let pubkey_tx_clone = pubkey_tx.clone();
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
let mut tasks = smallvec![]; let mut tasks = smallvec![];
@@ -132,7 +112,7 @@ impl ChatSpace {
.await .await
.expect("Failed connect the bootstrap relays. Please restart the application."); .expect("Failed connect the bootstrap relays. Please restart the application.");
Self::process_nostr_events(&relay_tracking_clone, &event_tx, &pubkey_tx_clone) Self::process_nostr_events(&pubkey_tx)
.await .await
.expect("Failed to handle nostr events. Please restart the application."); .expect("Failed to handle nostr events. Please restart the application.");
}), }),
@@ -140,9 +120,16 @@ impl ChatSpace {
tasks.push( tasks.push(
// Wait for the signer to be set // Wait for the signer to be set
// Also verify NIP65 and NIP17 relays after the signer is set // Also verify NIP-65 and NIP-17 relays after the signer is set
cx.background_spawn(async move { cx.background_spawn(async move {
Self::wait_for_signer_set(&relay_tracking).await; Self::observe_signer().await;
}),
);
tasks.push(
// Observe gift wrap process in the background
cx.background_spawn(async move {
Self::observe_giftwrap().await;
}), }),
); );
@@ -153,13 +140,6 @@ impl ChatSpace {
}), }),
); );
tasks.push(
// Process gift wrap event in the background
cx.background_spawn(async move {
Self::process_gift_wrap(&pubkey_tx, &event_rx).await;
}),
);
tasks.push( tasks.push(
// Continuously handle signals from the Nostr channel // Continuously handle signals from the Nostr channel
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
@@ -198,47 +178,61 @@ impl ChatSpace {
Ok(()) Ok(())
} }
async fn wait_for_signer_set(relay_tracking: &Arc<Mutex<RelayTracking>>) { async fn observe_signer() {
let client = nostr_client(); let client = nostr_client();
let ingester = ingester(); let ingester = ingester();
let loop_duration = Duration::from_millis(500);
let mut signer_set = false; let mut is_sent_signal = false;
let mut retry = 0; let mut identity: Option<PublicKey> = None;
let mut nip65_retry = 0;
loop { loop {
if signer_set { if let Some(public_key) = identity {
let state = relay_tracking.lock().await; let nip65 = Filter::new().kind(Kind::RelayList).author(public_key);
if state.nip65 == RelayTrackStatus::Found { if client.database().count(nip65).await.unwrap_or(0) > 0 {
if state.nip17 == RelayTrackStatus::Found { let nip17 = Filter::new().kind(Kind::InboxRelays).author(public_key);
break;
} else if state.nip17 == RelayTrackStatus::NotFound { match client.database().query(nip17).await {
ingester.send(IngesterSignal::DmRelayNotFound).await; Ok(events) => {
break; if let Some(event) = events.first_owned() {
} else { let relay_urls = Self::extract_relay_list(&event);
retry += 1;
if retry == RELAY_RETRY { if relay_urls.is_empty() {
ingester.send(IngesterSignal::DmRelayNotFound).await; if !is_sent_signal {
break; ingester.send(Signal::DmRelayNotFound).await;
} is_sent_signal = true;
} }
} else { } else {
nip65_retry += 1; break;
if nip65_retry == RELAY_RETRY { }
ingester.send(IngesterSignal::DmRelayNotFound).await; } else if !is_sent_signal {
ingester.send(Signal::DmRelayNotFound).await;
is_sent_signal = true;
} else {
break; break;
} }
} }
Err(_) => {
if !is_sent_signal {
ingester.send(Signal::DmRelayNotFound).await;
is_sent_signal = true;
} }
}
if !signer_set { }
} else if !is_sent_signal {
ingester.send(Signal::DmRelayNotFound).await;
is_sent_signal = true;
} else {
break;
}
} else {
// Wait for signer set
if let Ok(signer) = client.signer().await { if let Ok(signer) = client.signer().await {
if let Ok(public_key) = signer.get_public_key().await { if let Ok(public_key) = signer.get_public_key().await {
signer_set = true; identity = Some(public_key);
// Notify the app that the signer has been set. // Notify the app that the signer has been set.
ingester.send(IngesterSignal::SignerSet(public_key)).await; ingester.send(Signal::SignerSet(public_key)).await;
// Subscribe to the NIP-65 relays for the public key. // Subscribe to the NIP-65 relays for the public key.
if let Err(e) = Self::fetch_nip65_relays(public_key).await { if let Err(e) = Self::fetch_nip65_relays(public_key).await {
@@ -248,7 +242,39 @@ impl ChatSpace {
} }
} }
smol::Timer::after(Duration::from_millis(300)).await; smol::Timer::after(loop_duration).await;
}
}
async fn observe_giftwrap() {
let client = nostr_client();
let css = css();
let ingester = ingester();
let loop_duration = Duration::from_secs(10);
let mut total_notify = 0;
loop {
if client.has_signer().await {
if css.gift_wrap_processing.load(Ordering::Acquire) {
ingester.send(Signal::EventProcessing).await;
// Reset gift wrap processing flag
let _ = css.gift_wrap_processing.compare_exchange(
true,
false,
Ordering::Release,
Ordering::Relaxed,
);
} else {
// Only send signal to ingester a maximum of three times
if total_notify <= 3 {
ingester.send(Signal::EventProcessed(true)).await;
total_notify += 1;
}
}
}
smol::Timer::after(loop_duration).await;
} }
} }
@@ -265,26 +291,27 @@ impl ChatSpace {
} }
loop { loop {
match smol::future::or( let futs = smol::future::or(
async { async move {
if let Ok(public_key) = rx.recv().await { if let Ok(public_key) = rx.recv().await {
BatchEvent::PublicKey(public_key) BatchEvent::PublicKey(public_key)
} else { } else {
BatchEvent::Closed BatchEvent::Closed
} }
}, },
async { async move {
smol::Timer::after(timeout).await; smol::Timer::after(timeout).await;
BatchEvent::Timeout BatchEvent::Timeout
}, },
) );
.await
{ match futs.await {
BatchEvent::PublicKey(public_key) => { BatchEvent::PublicKey(public_key) => {
// Prevent duplicate keys from being processed // Prevent duplicate keys from being processed
if processed_pubkeys.insert(public_key) { if processed_pubkeys.insert(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() >= METADATA_BATCH_LIMIT { if batch.len() >= METADATA_BATCH_LIMIT {
Self::fetch_metadata_for_pubkeys(std::mem::take(&mut batch)).await; Self::fetch_metadata_for_pubkeys(std::mem::take(&mut batch)).await;
@@ -295,73 +322,21 @@ impl ChatSpace {
} }
BatchEvent::Closed => { BatchEvent::Closed => {
Self::fetch_metadata_for_pubkeys(std::mem::take(&mut batch)).await; Self::fetch_metadata_for_pubkeys(std::mem::take(&mut batch)).await;
// Exit the current loop
break; break;
} }
} }
} }
} }
async fn process_gift_wrap(pubkey_tx: &Sender<PublicKey>, event_rx: &Receiver<Event>) { async fn process_nostr_events(pubkey_tx: &Sender<PublicKey>) -> Result<(), Error> {
let client = nostr_client();
let ingester = ingester();
let timeout = Duration::from_secs(WAIT_FOR_FINISH);
let mut counter = 0;
loop {
// Signer is unset, probably user is not ready to retrieve gift wrap events
if client.signer().await.is_err() {
smol::Timer::after(Duration::from_secs(1)).await;
continue;
}
let recv = || async {
// no inline
(event_rx.recv().await).ok()
};
let timeout = || async {
smol::Timer::after(timeout).await;
None
};
match smol::future::or(recv(), timeout()).await {
Some(event) => {
let cached = Self::unwrap_gift_wrap_event(&event, pubkey_tx).await;
// Increment the total messages counter if message is not from cache
if !cached {
counter += 1;
}
// Send partial finish signal to GPUI
if counter >= 20 {
ingester.send(IngesterSignal::PartialFinish).await;
// Reset counter
counter = 0;
}
}
None => {
// Notify the UI that the processing is finished
ingester.send(IngesterSignal::Finish).await;
break;
}
}
}
}
async fn process_nostr_events(
relay_tracking: &Arc<Mutex<RelayTracking>>,
event_tx: &Sender<Event>,
pubkey_tx: &Sender<PublicKey>,
) -> Result<(), Error> {
let client = nostr_client(); let client = nostr_client();
let ingester = ingester(); let ingester = ingester();
let css = css(); let css = css();
let auto_close = let auto_close =
SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let mut event_counter = 0;
let mut processed_events: HashSet<EventId> = HashSet::new(); let mut processed_events: HashSet<EventId> = HashSet::new();
let mut challenges: HashSet<Cow<'_, str>> = HashSet::new(); let mut challenges: HashSet<Cow<'_, str>> = HashSet::new();
let mut notifications = client.notifications(); let mut notifications = client.notifications();
@@ -382,9 +357,6 @@ impl ChatSpace {
Kind::RelayList => { Kind::RelayList => {
// Get metadata for event's pubkey that matches the current user's pubkey // Get metadata for event's pubkey that matches the current user's pubkey
if let Ok(true) = Self::is_self_event(&event).await { if let Ok(true) = Self::is_self_event(&event).await {
let mut relay_tracking = relay_tracking.lock().await;
relay_tracking.nip65 = RelayTrackStatus::Found;
// Fetch user's metadata event // Fetch user's metadata event
Self::fetch_single_event(Kind::Metadata, event.pubkey).await; Self::fetch_single_event(Kind::Metadata, event.pubkey).await;
@@ -397,30 +369,17 @@ impl ChatSpace {
} }
Kind::InboxRelays => { Kind::InboxRelays => {
if let Ok(true) = Self::is_self_event(&event).await { if let Ok(true) = Self::is_self_event(&event).await {
let relays: Vec<RelayUrl> = event let relays: Vec<RelayUrl> = Self::extract_relay_list(&event);
.tags
.filter_standardized(TagKind::Relay)
.filter_map(|t| {
if let TagStandard::Relay(url) = t {
Some(url.to_owned())
} else {
None
}
})
.collect();
if !relays.is_empty() { if !relays.is_empty() {
let mut relay_tracking = relay_tracking.lock().await;
relay_tracking.nip17 = RelayTrackStatus::Found;
for relay in relays.iter() { for relay in relays.iter() {
if client.add_relay(relay).await.is_err() { if client.add_relay(relay).await.is_err() {
let notice = Notice::RelayFailed(relay.clone()); let notice = Notice::RelayFailed(relay.clone());
ingester.send(IngesterSignal::Notice(notice)).await; ingester.send(Signal::Notice(notice)).await;
} }
if client.connect_relay(relay).await.is_err() { if client.connect_relay(relay).await.is_err() {
let notice = Notice::RelayFailed(relay.clone()); let notice = Notice::RelayFailed(relay.clone());
ingester.send(IngesterSignal::Notice(notice)).await; ingester.send(Signal::Notice(notice)).await;
} }
} }
@@ -444,23 +403,35 @@ impl ChatSpace {
} }
} }
Kind::Metadata => { Kind::Metadata => {
ingester ingester.send(Signal::Metadata(event.into_owned())).await;
.send(IngesterSignal::Metadata(event.into_owned()))
.await;
} }
Kind::GiftWrap => { Kind::GiftWrap => {
if event_tx.send(event.clone().into_owned()).await.is_err() { // Mark gift wrap event as currently being processed
css.gift_wrap_processing.store(true, Ordering::Release);
// Process the gift wrap event
Self::unwrap_gift_wrap_event(&event, pubkey_tx).await; Self::unwrap_gift_wrap_event(&event, pubkey_tx).await;
// Trigger a partial UI reload if at least 50 events have been processed
if event_counter >= 20 {
ingester.send(Signal::EventProcessed(false)).await;
event_counter = 0;
} }
event_counter += 1;
} }
_ => {} _ => {}
} }
} }
RelayMessage::EndOfStoredEvents(subscription_id) => {
if *subscription_id == css.gift_wrap_sub_id {
ingester.send(Signal::EventProcessed(false)).await;
}
}
RelayMessage::Auth { challenge } => { RelayMessage::Auth { challenge } => {
if challenges.insert(challenge.clone()) { if challenges.insert(challenge.clone()) {
let req = AuthRequest::new(challenge, relay_url); let req = AuthRequest::new(challenge, relay_url);
// Send a signal to the ingester to handle the auth request // Send a signal to the ingester to handle the auth request
ingester.send(IngesterSignal::Auth(req)).await; ingester.send(Signal::Auth(req)).await;
} }
} }
RelayMessage::Ok { RelayMessage::Ok {
@@ -498,7 +469,7 @@ impl ChatSpace {
let settings = AppSettings::global(cx); let settings = AppSettings::global(cx);
match signal { match signal {
IngesterSignal::SignerSet(public_key) => { Signal::SignerSet(public_key) => {
window.close_modal(cx); window.close_modal(cx);
// Setup the default layout for current workspace // Setup the default layout for current workspace
@@ -515,10 +486,10 @@ impl ChatSpace {
// Load all chat rooms // Load all chat rooms
registry.update(cx, |this, cx| { registry.update(cx, |this, cx| {
this.set_identity(public_key, cx); this.set_identity(public_key, cx);
this.load_rooms(window, cx); this.load_rooms(false, window, cx);
}); });
} }
IngesterSignal::SignerUnset => { Signal::SignerUnset => {
// Setup the onboarding layout for current workspace // Setup the onboarding layout for current workspace
view.update(cx, |this, cx| { view.update(cx, |this, cx| {
this.set_onboarding_layout(window, cx); this.set_onboarding_layout(window, cx);
@@ -530,7 +501,7 @@ impl ChatSpace {
this.reset(cx); this.reset(cx);
}); });
} }
IngesterSignal::Auth(req) => { Signal::Auth(req) => {
let relay_url = &req.url; let relay_url = &req.url;
let challenge = &req.challenge; let challenge = &req.challenge;
let auto_auth = AppSettings::get_auto_auth(cx); let auto_auth = AppSettings::get_auto_auth(cx);
@@ -550,30 +521,27 @@ impl ChatSpace {
}) })
.ok(); .ok();
} }
IngesterSignal::ProxyDown => { Signal::ProxyDown => {
if !is_open_proxy_modal { if !is_open_proxy_modal {
is_open_proxy_modal = true;
view.update(cx, |this, cx| { view.update(cx, |this, cx| {
this.render_proxy_modal(window, cx); this.render_proxy_modal(window, cx);
}) })
.ok(); .ok();
is_open_proxy_modal = true;
} }
} }
// Load chat rooms and stop the loading status // Notify the user that the gift wrap still processing
IngesterSignal::Finish => { Signal::EventProcessing => {
registry.update(cx, |this, cx| { registry.update(cx, |this, cx| {
this.load_rooms(window, cx); this.set_loading(true, cx);
this.set_loading(false, cx);
// Send a signal to refresh all opened rooms' messages
if let Some(ids) = ChatSpace::all_panels(window, cx) {
this.refresh_rooms(ids, cx);
}
}); });
} }
// Load chat rooms without setting as finished Signal::EventProcessed(finish) => {
IngesterSignal::PartialFinish => {
registry.update(cx, |this, cx| { registry.update(cx, |this, cx| {
this.load_rooms(window, cx); // Load all chat rooms in the database
this.load_rooms(finish, window, cx);
// Send a signal to refresh all opened rooms' messages // Send a signal to refresh all opened rooms' messages
if let Some(ids) = ChatSpace::all_panels(window, cx) { if let Some(ids) = ChatSpace::all_panels(window, cx) {
this.refresh_rooms(ids, cx); this.refresh_rooms(ids, cx);
@@ -581,24 +549,25 @@ impl ChatSpace {
}); });
} }
// Add the new metadata to the registry or update the existing one // Add the new metadata to the registry or update the existing one
IngesterSignal::Metadata(event) => { Signal::Metadata(event) => {
registry.update(cx, |this, cx| { registry.update(cx, |this, cx| {
this.insert_or_update_person(event, cx); this.insert_or_update_person(event, cx);
}); });
} }
// Convert the gift wrapped message to a message // Convert the gift wrapped message to a message
IngesterSignal::GiftWrap((gift_wrap_id, event)) => { Signal::Message((gift_wrap_id, event)) => {
registry.update(cx, |this, cx| { registry.update(cx, |this, cx| {
this.event_to_message(gift_wrap_id, event, window, cx); this.event_to_message(gift_wrap_id, event, window, cx);
}); });
} }
IngesterSignal::DmRelayNotFound => { // Notify the user that the DM relay is not set
Signal::DmRelayNotFound => {
view.update(cx, |this, cx| { view.update(cx, |this, cx| {
this.set_no_nip17_relays(cx); this.set_no_nip17_relays(cx);
}) })
.ok(); .ok();
} }
IngesterSignal::Notice(msg) => { Signal::Notice(msg) => {
window.push_notification(msg.as_str(), cx); window.push_notification(msg.as_str(), cx);
} }
}; };
@@ -607,6 +576,20 @@ impl ChatSpace {
} }
} }
fn extract_relay_list(event: &Event) -> Vec<RelayUrl> {
event
.tags
.filter_standardized(TagKind::Relay)
.filter_map(|t| {
if let TagStandard::Relay(url) = t {
Some(url.to_owned())
} else {
None
}
})
.collect()
}
/// Checks if an event is belong to the current user /// Checks if an event is belong to the current user
async fn is_self_event(event: &Event) -> Result<bool, Error> { async fn is_self_event(event: &Event) -> Result<bool, Error> {
let client = nostr_client(); let client = nostr_client();
@@ -617,7 +600,7 @@ impl ChatSpace {
} }
/// Fetches a single event by kind and public key /// Fetches a single event by kind and public key
async fn fetch_single_event(kind: Kind, public_key: PublicKey) { pub async fn fetch_single_event(kind: Kind, public_key: PublicKey) {
let client = nostr_client(); let client = nostr_client();
let auto_close = let auto_close =
SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
@@ -628,13 +611,13 @@ impl ChatSpace {
} }
} }
async fn fetch_gift_wrap(relays: &[RelayUrl], public_key: PublicKey) { pub async fn fetch_gift_wrap(relays: &[RelayUrl], public_key: PublicKey) {
let client = nostr_client(); let client = nostr_client();
let subscription_id = SubscriptionId::new("inbox"); let sub_id = css().gift_wrap_sub_id.clone();
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
if client if client
.subscribe_with_id_to(relays.to_owned(), subscription_id, filter, None) .subscribe_with_id_to(relays.to_owned(), sub_id, filter, None)
.await .await
.is_ok() .is_ok()
{ {
@@ -643,7 +626,7 @@ impl ChatSpace {
} }
/// Fetches NIP-65 relay list for a given public key /// Fetches NIP-65 relay list for a given public key
async fn fetch_nip65_relays(public_key: PublicKey) -> Result<(), Error> { pub async fn fetch_nip65_relays(public_key: PublicKey) -> Result<(), Error> {
let client = nostr_client(); let client = nostr_client();
let auto_close = let auto_close =
SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
@@ -761,9 +744,7 @@ impl ChatSpace {
// Send a notify to GPUI if this is a new message // Send a notify to GPUI if this is a new message
if event.created_at >= css.init_at { if event.created_at >= css.init_at {
ingester ingester.send(Signal::Message((gift.id, event))).await;
.send(IngesterSignal::GiftWrap((gift.id, event)))
.await;
} }
is_cached is_cached
@@ -1119,7 +1100,7 @@ impl ChatSpace {
client.reset().await; client.reset().await;
// Notify the channel about the signer being unset // Notify the channel about the signer being unset
ingester.send(IngesterSignal::SignerUnset).await; ingester.send(Signal::SignerUnset).await;
}) })
.detach(); .detach();
} }
@@ -1221,7 +1202,7 @@ impl ChatSpace {
cx: &Context<Self>, cx: &Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let registry = Registry::read_global(cx); let registry = Registry::read_global(cx);
let loading = self.has_nip17_relays && self.auth_requests.is_empty() && registry.loading; let loading = registry.loading;
h_flex() h_flex()
.gap_2() .gap_2()
@@ -1376,7 +1357,7 @@ impl ChatSpace {
break; break;
} else { } else {
ingester.send(IngesterSignal::ProxyDown).await; ingester.send(Signal::ProxyDown).await;
} }
smol::Timer::after(Duration::from_secs(1)).await; smol::Timer::after(Duration::from_secs(1)).await;
} }

View File

@@ -5,7 +5,7 @@ use client_keys::ClientKeys;
use common::display::ReadableProfile; use common::display::ReadableProfile;
use common::handle_auth::CoopAuthUrlHandler; use common::handle_auth::CoopAuthUrlHandler;
use global::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT}; use global::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
use global::{ingester, nostr_client, IngesterSignal}; use global::{ingester, nostr_client, Signal};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
@@ -255,7 +255,7 @@ impl Account {
client.unset_signer().await; client.unset_signer().await;
// Notify the channel about the signer being unset // Notify the channel about the signer being unset
ingester.send(IngesterSignal::SignerUnset).await; ingester.send(Signal::SignerUnset).await;
}) })
.detach(); .detach();
} }

View File

@@ -1,6 +1,6 @@
use anyhow::anyhow; use anyhow::anyhow;
use common::nip96::nip96_upload; use common::nip96::nip96_upload;
use global::constants::ACCOUNT_IDENTIFIER; use global::constants::{ACCOUNT_IDENTIFIER, NIP17_RELAYS, NIP65_RELAYS};
use global::nostr_client; use global::nostr_client;
use gpui::{ use gpui::{
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
@@ -70,6 +70,7 @@ impl NewAccount {
window.open_modal(cx, move |modal, _window, _cx| { window.open_modal(cx, move |modal, _window, _cx| {
let weak_view = weak_view.clone(); let weak_view = weak_view.clone();
let current_view = current_view.clone(); let current_view = current_view.clone();
modal modal
.alert() .alert()
.title(shared_t!("new_account.backup_label")) .title(shared_t!("new_account.backup_label"))
@@ -124,8 +125,44 @@ impl NewAccount {
// Set the client's signer with the current keys // Set the client's signer with the current keys
cx.background_spawn(async move { cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
// Set the client's signer with the current keys
client.set_signer(keys).await; client.set_signer(keys).await;
client.set_metadata(&metadata).await.ok();
// Set metadata
if let Err(e) = client.set_metadata(&metadata).await {
log::error!("Failed to set metadata: {e}");
}
// Set NIP-65 relays
let builder = EventBuilder::new(Kind::RelayList, "").tags(
NIP65_RELAYS.into_iter().filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay_metadata(url, None))
} else {
None
}
}),
);
if let Err(e) = client.send_event_builder(builder).await {
log::error!("Failed to send NIP-65 relay list event: {e}");
}
// Set NIP-17 relays
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(
NIP17_RELAYS.into_iter().filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay(url))
} else {
None
}
}),
);
if let Err(e) = client.send_event_builder(builder).await {
log::error!("Failed to send messaging relay list event: {e}");
};
}) })
.detach(); .detach();
} }

View File

@@ -17,6 +17,7 @@ use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
use ui::notification::Notification;
use ui::popup_menu::PopupMenu; use ui::popup_menu::PopupMenu;
use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt}; use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt};
@@ -148,7 +149,10 @@ impl Onboarding {
} }
Err(e) => { Err(e) => {
cx.update(|window, cx| { cx.update(|window, cx| {
window.push_notification(e.to_string(), cx); window.push_notification(
Notification::error(e.to_string()).title("Nostr Connect"),
cx,
);
}) })
.ok(); .ok();
} }

View File

@@ -10,7 +10,6 @@ use gpui::{
TextAlign, UniformList, Window, TextAlign, UniformList, Window,
}; };
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use registry::Registry; use registry::Registry;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
@@ -20,6 +19,8 @@ use ui::input::{InputEvent, InputState, TextInput};
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::{h_flex, v_flex, ContextModal, IconName, Sizable, StyledExt}; use ui::{h_flex, v_flex, ContextModal, IconName, Sizable, StyledExt};
use crate::chatspace::ChatSpace;
pub fn init(kind: Kind, window: &mut Window, cx: &mut App) -> Entity<SetupRelay> { pub fn init(kind: Kind, window: &mut Window, cx: &mut App) -> Entity<SetupRelay> {
cx.new(|cx| SetupRelay::new(kind, window, cx)) cx.new(|cx| SetupRelay::new(kind, window, cx))
} }
@@ -82,7 +83,7 @@ impl SetupRelay {
let filter = Filter::new().kind(kind).author(identity).limit(1); let filter = Filter::new().kind(kind).author(identity).limit(1);
if let Some(event) = client.database().query(filter).await?.first() { if let Some(event) = client.database().query(filter).await?.first() {
let relays = event let relays: Vec<RelayUrl> = event
.tags .tags
.iter() .iter()
.filter_map(|tag| tag.as_standardized()) .filter_map(|tag| tag.as_standardized())
@@ -95,7 +96,7 @@ impl SetupRelay {
None None
} }
}) })
.collect_vec(); .collect();
Ok(relays) Ok(relays)
} else { } else {
@@ -195,22 +196,32 @@ impl SetupRelay {
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let tags: Vec<Tag> = relays let tags: Vec<Tag> = relays
.iter() .iter()
.map(|relay| Tag::relay(relay.clone())) .map(|relay| Tag::relay(relay.clone()))
.collect(); .collect();
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags); let event = EventBuilder::new(Kind::InboxRelays, "")
.tags(tags)
.build(public_key)
.sign(&signer)
.await?;
// Set messaging relays // Set messaging relays
client.send_event_builder(builder).await?; client.send_event(&event).await?;
// Connect to messaging relays // Connect to messaging relays
for relay in relays.into_iter() { for relay in relays.iter() {
_ = client.add_relay(&relay).await; _ = client.add_relay(relay).await;
_ = client.connect_relay(&relay).await; _ = client.connect_relay(relay).await;
} }
// Fetch gift wrap events
ChatSpace::fetch_gift_wrap(&relays, public_key).await;
Ok(()) Ok(())
}); });
@@ -223,13 +234,10 @@ impl SetupRelay {
.ok(); .ok();
} }
Err(e) => { Err(e) => {
cx.update(|window, cx| { this.update_in(cx, |this, window, cx| {
this.update(cx, |this, cx| {
this.set_error(e.to_string(), window, cx); this.set_error(e.to_string(), window, cx);
}) })
.ok(); .ok();
})
.ok();
} }
}; };
}) })

View File

@@ -15,6 +15,7 @@ use ui::actions::OpenProfile;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::context_menu::ContextMenuExt; use ui::context_menu::ContextMenuExt;
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::skeleton::Skeleton;
use ui::{h_flex, ContextModal, StyledExt}; use ui::{h_flex, ContextModal, StyledExt};
use crate::views::screening; use crate::views::screening;
@@ -108,7 +109,21 @@ impl RenderOnce for RoomListItem {
self.handler, self.handler,
) )
else { else {
return div().id(self.ix); return h_flex()
.id(self.ix)
.h_9()
.w_full()
.px_1p5()
.gap_2()
.child(Skeleton::new().flex_shrink_0().size_6().rounded_full())
.child(
div()
.flex_1()
.flex()
.justify_between()
.child(Skeleton::new().w_32().h_2p5().rounded_sm())
.child(Skeleton::new().w_6().h_2p5().rounded_sm()),
);
}; };
h_flex() h_flex()

View File

@@ -33,6 +33,7 @@ mod list_item;
const FIND_DELAY: u64 = 600; const FIND_DELAY: u64 = 600;
const FIND_LIMIT: usize = 10; const FIND_LIMIT: usize = 10;
const TOTAL_SKELETONS: usize = 3;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
Sidebar::new(window, cx) Sidebar::new(window, cx)
@@ -586,6 +587,7 @@ impl Focusable for Sidebar {
impl Render for Sidebar { impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let registry = Registry::read_global(cx); let registry = Registry::read_global(cx);
let loading = registry.loading;
// Get rooms from either search results or the chat registry // Get rooms from either search results or the chat registry
let rooms = if let Some(results) = self.local_result.read(cx).as_ref() { let rooms = if let Some(results) = self.local_result.read(cx).as_ref() {
@@ -601,6 +603,15 @@ impl Render for Sidebar {
} }
}; };
// Get total rooms count
let mut total_rooms = rooms.len();
// If loading in progress
// Add 3 skeletons to the room list
if loading {
total_rooms += TOTAL_SKELETONS;
}
v_flex() v_flex()
.image_cache(self.image_cache.clone()) .image_cache(self.image_cache.clone())
.size_full() .size_full()
@@ -690,7 +701,7 @@ impl Render for Sidebar {
.child( .child(
uniform_list( uniform_list(
"rooms", "rooms",
rooms.len(), total_rooms,
cx.processor(move |this, range, _window, cx| { cx.processor(move |this, range, _window, cx| {
this.list_items(&rooms, range, cx) this.list_items(&rooms, range, cx)
}), }),

View File

@@ -1,4 +1,5 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::atomic::AtomicBool;
use std::sync::OnceLock; use std::sync::OnceLock;
use std::time::Duration; use std::time::Duration;
@@ -47,7 +48,7 @@ impl Notice {
/// Signals sent through the global event channel to notify UI /// Signals sent through the global event channel to notify UI
#[derive(Debug)] #[derive(Debug)]
pub enum IngesterSignal { pub enum Signal {
/// A signal to notify UI that the client's signer has been set /// A signal to notify UI that the client's signer has been set
SignerSet(PublicKey), SignerSet(PublicKey),
@@ -64,13 +65,13 @@ pub enum IngesterSignal {
Metadata(Event), Metadata(Event),
/// A signal to notify UI that a new gift wrap event has been received /// A signal to notify UI that a new gift wrap event has been received
GiftWrap((EventId, Event)), Message((EventId, Event)),
/// A signal to notify UI that all gift wrap events have been processed /// A signal to notify UI that gift wrap events still processing
Finish, EventProcessing,
/// A signal to notify UI that partial processing of gift wrap events has been completed /// A signal to notify UI that gift wrap events have been processed
PartialFinish, EventProcessed(bool),
/// A signal to notify UI that no DM relay for current user was found /// A signal to notify UI that no DM relay for current user was found
DmRelayNotFound, DmRelayNotFound,
@@ -81,8 +82,8 @@ pub enum IngesterSignal {
#[derive(Debug)] #[derive(Debug)]
pub struct Ingester { pub struct Ingester {
rx: Receiver<IngesterSignal>, rx: Receiver<Signal>,
tx: Sender<IngesterSignal>, tx: Sender<Signal>,
} }
impl Default for Ingester { impl Default for Ingester {
@@ -93,15 +94,15 @@ impl Default for Ingester {
impl Ingester { impl Ingester {
pub fn new() -> Self { pub fn new() -> Self {
let (tx, rx) = smol::channel::bounded::<IngesterSignal>(2048); let (tx, rx) = smol::channel::bounded::<Signal>(2048);
Self { rx, tx } Self { rx, tx }
} }
pub fn signals(&self) -> &Receiver<IngesterSignal> { pub fn signals(&self) -> &Receiver<Signal> {
&self.rx &self.rx
} }
pub async fn send(&self, signal: IngesterSignal) { pub async fn send(&self, signal: Signal) {
if let Err(e) = self.tx.send(signal).await { if let Err(e) = self.tx.send(signal).await {
log::error!("Failed to send signal: {e}"); log::error!("Failed to send signal: {e}");
} }
@@ -109,18 +110,28 @@ impl Ingester {
} }
/// A simple storage to store all runtime states that using across the application. /// A simple storage to store all runtime states that using across the application.
#[derive(Debug, Default)] #[derive(Debug)]
pub struct CoopSimpleStorage { pub struct CoopSimpleStorage {
pub init_at: Timestamp, pub init_at: Timestamp,
pub gift_wrap_sub_id: SubscriptionId,
pub gift_wrap_processing: AtomicBool,
pub sent_ids: RwLock<HashSet<EventId>>, pub sent_ids: RwLock<HashSet<EventId>>,
pub resent_ids: RwLock<Vec<Output<EventId>>>, pub resent_ids: RwLock<Vec<Output<EventId>>>,
pub resend_queue: RwLock<HashMap<EventId, RelayUrl>>, pub resend_queue: RwLock<HashMap<EventId, RelayUrl>>,
} }
impl Default for CoopSimpleStorage {
fn default() -> Self {
Self::new()
}
}
impl CoopSimpleStorage { impl CoopSimpleStorage {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
init_at: Timestamp::now(), init_at: Timestamp::now(),
gift_wrap_sub_id: SubscriptionId::new("inbox"),
gift_wrap_processing: AtomicBool::new(true),
sent_ids: RwLock::new(HashSet::new()), sent_ids: RwLock::new(HashSet::new()),
resent_ids: RwLock::new(Vec::new()), resent_ids: RwLock::new(Vec::new()),
resend_queue: RwLock::new(HashMap::new()), resend_queue: RwLock::new(HashMap::new()),

View File

@@ -264,7 +264,7 @@ impl Registry {
} }
/// Load all rooms from the database. /// Load all rooms from the database.
pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub fn load_rooms(&mut self, finish: bool, window: &mut Window, cx: &mut Context<Self>) {
log::info!("Starting to load chat rooms..."); log::info!("Starting to load chat rooms...");
// Get the contact bypass setting // Get the contact bypass setting
@@ -340,8 +340,11 @@ impl Registry {
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
match task.await { match task.await {
Ok(rooms) => { Ok(rooms) => {
this.update_in(cx, |_, window, cx| { this.update_in(cx, move |_, window, cx| {
cx.defer_in(window, |this, _window, cx| { cx.defer_in(window, move |this, _window, cx| {
if finish {
this.set_loading(false, cx);
}
this.extend_rooms(rooms, cx); this.extend_rooms(rooms, cx);
this.sort(cx); this.sort(cx);
}); });

View File

@@ -56,7 +56,7 @@ impl RenderOnce for Skeleton {
.bg(color) .bg(color)
.with_animation( .with_animation(
"skeleton", "skeleton",
Animation::new(Duration::from_secs(2)) Animation::new(Duration::from_secs(3))
.repeat() .repeat()
.with_easing(bounce(ease_in_out)), .with_easing(bounce(ease_in_out)),
move |this, delta| { move |this, delta| {

View File

@@ -1,5 +1,5 @@
use gpui::{ use gpui::{
deferred, div, relative, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, div, relative, App, AppContext, Context, Entity, IntoElement, ParentElement, Render,
SharedString, Styled, Window, SharedString, Styled, Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -16,7 +16,7 @@ impl Tooltip {
impl Render for Tooltip { impl Render for Tooltip {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().child(deferred( div().child(
div() div()
.font_family(".SystemUIFont") .font_family(".SystemUIFont")
.m_3() .m_3()
@@ -30,6 +30,6 @@ impl Render for Tooltip {
.text_color(cx.theme().text_muted) .text_color(cx.theme().text_muted)
.line_height(relative(1.25)) .line_height(relative(1.25))
.child(self.text.clone()), .child(self.text.clone()),
)) )
} }
} }