diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 5e01263..e6ce61e 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; use std::collections::{HashMap, HashSet}; +use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::Duration; @@ -10,9 +11,9 @@ use common::display::ReadableProfile; use common::event::EventUtils; use global::constants::{ 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::{ div, px, rems, App, AppContext, AsyncWindowContext, Axis, Context, Entity, InteractiveElement, @@ -28,7 +29,6 @@ use settings::AppSettings; use signer_proxy::{BrowserSignerProxy, BrowserSignerProxyOptions}; use smallvec::{smallvec, SmallVec}; use smol::channel::{Receiver, Sender}; -use smol::lock::Mutex; use theme::{ActiveTheme, Theme, ThemeMode}; use title_bar::TitleBar; 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); } -#[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 { title_bar: Entity, dock: Entity, @@ -95,13 +81,7 @@ impl ChatSpace { let title_bar = cx.new(|_| TitleBar::new()); 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::(1024); - let (event_tx, event_rx) = smol::channel::bounded::(2048); - - let pubkey_tx_clone = pubkey_tx.clone(); let mut subscriptions = smallvec![]; let mut tasks = smallvec![]; @@ -132,7 +112,7 @@ impl ChatSpace { .await .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 .expect("Failed to handle nostr events. Please restart the application."); }), @@ -140,9 +120,16 @@ impl ChatSpace { tasks.push( // 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 { - 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( // Continuously handle signals from the Nostr channel cx.spawn_in(window, async move |this, cx| { @@ -198,47 +178,61 @@ impl ChatSpace { Ok(()) } - async fn wait_for_signer_set(relay_tracking: &Arc>) { + async fn observe_signer() { let client = nostr_client(); let ingester = ingester(); - - let mut signer_set = false; - let mut retry = 0; - let mut nip65_retry = 0; + let loop_duration = Duration::from_millis(500); + let mut is_sent_signal = false; + let mut identity: Option = None; loop { - if signer_set { - let state = relay_tracking.lock().await; + if let Some(public_key) = identity { + let nip65 = Filter::new().kind(Kind::RelayList).author(public_key); - if state.nip65 == RelayTrackStatus::Found { - if state.nip17 == RelayTrackStatus::Found { - break; - } else if state.nip17 == RelayTrackStatus::NotFound { - ingester.send(IngesterSignal::DmRelayNotFound).await; - break; - } else { - retry += 1; - if retry == RELAY_RETRY { - ingester.send(IngesterSignal::DmRelayNotFound).await; - break; + if client.database().count(nip65).await.unwrap_or(0) > 0 { + let nip17 = Filter::new().kind(Kind::InboxRelays).author(public_key); + + match client.database().query(nip17).await { + Ok(events) => { + if let Some(event) = events.first_owned() { + let relay_urls = Self::extract_relay_list(&event); + + if relay_urls.is_empty() { + if !is_sent_signal { + ingester.send(Signal::DmRelayNotFound).await; + is_sent_signal = true; + } + } else { + break; + } + } else if !is_sent_signal { + ingester.send(Signal::DmRelayNotFound).await; + is_sent_signal = true; + } else { + break; + } + } + Err(_) => { + if !is_sent_signal { + ingester.send(Signal::DmRelayNotFound).await; + is_sent_signal = true; + } } } + } else if !is_sent_signal { + ingester.send(Signal::DmRelayNotFound).await; + is_sent_signal = true; } else { - nip65_retry += 1; - if nip65_retry == RELAY_RETRY { - ingester.send(IngesterSignal::DmRelayNotFound).await; - break; - } + break; } - } - - if !signer_set { + } else { + // Wait for signer set if let Ok(signer) = client.signer().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. - ingester.send(IngesterSignal::SignerSet(public_key)).await; + ingester.send(Signal::SignerSet(public_key)).await; // Subscribe to the NIP-65 relays for the public key. 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 { - match smol::future::or( - async { + let futs = smol::future::or( + async move { if let Ok(public_key) = rx.recv().await { BatchEvent::PublicKey(public_key) } else { BatchEvent::Closed } }, - async { + async move { smol::Timer::after(timeout).await; BatchEvent::Timeout }, - ) - .await - { + ); + + match futs.await { BatchEvent::PublicKey(public_key) => { // Prevent duplicate keys from being processed if processed_pubkeys.insert(public_key) { batch.insert(public_key); } + // Process the batch if it's full if batch.len() >= METADATA_BATCH_LIMIT { Self::fetch_metadata_for_pubkeys(std::mem::take(&mut batch)).await; @@ -295,73 +322,21 @@ impl ChatSpace { } BatchEvent::Closed => { Self::fetch_metadata_for_pubkeys(std::mem::take(&mut batch)).await; - // Exit the current loop break; } } } } - async fn process_gift_wrap(pubkey_tx: &Sender, event_rx: &Receiver) { - 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>, - event_tx: &Sender, - pubkey_tx: &Sender, - ) -> Result<(), Error> { + async fn process_nostr_events(pubkey_tx: &Sender) -> Result<(), Error> { let client = nostr_client(); let ingester = ingester(); let css = css(); + let auto_close = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + let mut event_counter = 0; let mut processed_events: HashSet = HashSet::new(); let mut challenges: HashSet> = HashSet::new(); let mut notifications = client.notifications(); @@ -382,9 +357,6 @@ impl ChatSpace { Kind::RelayList => { // Get metadata for event's pubkey that matches the current user's pubkey 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 Self::fetch_single_event(Kind::Metadata, event.pubkey).await; @@ -397,30 +369,17 @@ impl ChatSpace { } Kind::InboxRelays => { if let Ok(true) = Self::is_self_event(&event).await { - let relays: Vec = event - .tags - .filter_standardized(TagKind::Relay) - .filter_map(|t| { - if let TagStandard::Relay(url) = t { - Some(url.to_owned()) - } else { - None - } - }) - .collect(); + let relays: Vec = Self::extract_relay_list(&event); if !relays.is_empty() { - let mut relay_tracking = relay_tracking.lock().await; - relay_tracking.nip17 = RelayTrackStatus::Found; - for relay in relays.iter() { if client.add_relay(relay).await.is_err() { 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() { 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 => { - ingester - .send(IngesterSignal::Metadata(event.into_owned())) - .await; + ingester.send(Signal::Metadata(event.into_owned())).await; } Kind::GiftWrap => { - if event_tx.send(event.clone().into_owned()).await.is_err() { - Self::unwrap_gift_wrap_event(&event, pubkey_tx).await; + // 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; + + // 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 } => { if challenges.insert(challenge.clone()) { let req = AuthRequest::new(challenge, relay_url); // 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 { @@ -498,7 +469,7 @@ impl ChatSpace { let settings = AppSettings::global(cx); match signal { - IngesterSignal::SignerSet(public_key) => { + Signal::SignerSet(public_key) => { window.close_modal(cx); // Setup the default layout for current workspace @@ -515,10 +486,10 @@ impl ChatSpace { // Load all chat rooms registry.update(cx, |this, 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 view.update(cx, |this, cx| { this.set_onboarding_layout(window, cx); @@ -530,7 +501,7 @@ impl ChatSpace { this.reset(cx); }); } - IngesterSignal::Auth(req) => { + Signal::Auth(req) => { let relay_url = &req.url; let challenge = &req.challenge; let auto_auth = AppSettings::get_auto_auth(cx); @@ -550,30 +521,27 @@ impl ChatSpace { }) .ok(); } - IngesterSignal::ProxyDown => { + Signal::ProxyDown => { if !is_open_proxy_modal { + is_open_proxy_modal = true; + view.update(cx, |this, cx| { this.render_proxy_modal(window, cx); }) .ok(); - is_open_proxy_modal = true; } } - // Load chat rooms and stop the loading status - IngesterSignal::Finish => { + // Notify the user that the gift wrap still processing + Signal::EventProcessing => { registry.update(cx, |this, cx| { - this.load_rooms(window, 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); - } + this.set_loading(true, cx); }); } - // Load chat rooms without setting as finished - IngesterSignal::PartialFinish => { + Signal::EventProcessed(finish) => { 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 if let Some(ids) = ChatSpace::all_panels(window, cx) { this.refresh_rooms(ids, cx); @@ -581,24 +549,25 @@ impl ChatSpace { }); } // Add the new metadata to the registry or update the existing one - IngesterSignal::Metadata(event) => { + Signal::Metadata(event) => { registry.update(cx, |this, cx| { this.insert_or_update_person(event, cx); }); } // 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| { 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| { this.set_no_nip17_relays(cx); }) .ok(); } - IngesterSignal::Notice(msg) => { + Signal::Notice(msg) => { window.push_notification(msg.as_str(), cx); } }; @@ -607,6 +576,20 @@ impl ChatSpace { } } + fn extract_relay_list(event: &Event) -> Vec { + 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 async fn is_self_event(event: &Event) -> Result { let client = nostr_client(); @@ -617,7 +600,7 @@ impl ChatSpace { } /// 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 auto_close = 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 subscription_id = SubscriptionId::new("inbox"); + let sub_id = css().gift_wrap_sub_id.clone(); let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); 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 .is_ok() { @@ -643,7 +626,7 @@ impl ChatSpace { } /// 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 auto_close = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); @@ -761,9 +744,7 @@ impl ChatSpace { // Send a notify to GPUI if this is a new message if event.created_at >= css.init_at { - ingester - .send(IngesterSignal::GiftWrap((gift.id, event))) - .await; + ingester.send(Signal::Message((gift.id, event))).await; } is_cached @@ -1119,7 +1100,7 @@ impl ChatSpace { client.reset().await; // Notify the channel about the signer being unset - ingester.send(IngesterSignal::SignerUnset).await; + ingester.send(Signal::SignerUnset).await; }) .detach(); } @@ -1221,7 +1202,7 @@ impl ChatSpace { cx: &Context, ) -> impl IntoElement { 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() .gap_2() @@ -1376,7 +1357,7 @@ impl ChatSpace { break; } else { - ingester.send(IngesterSignal::ProxyDown).await; + ingester.send(Signal::ProxyDown).await; } smol::Timer::after(Duration::from_secs(1)).await; } diff --git a/crates/coop/src/views/account.rs b/crates/coop/src/views/account.rs index c77e5b0..7153099 100644 --- a/crates/coop/src/views/account.rs +++ b/crates/coop/src/views/account.rs @@ -5,7 +5,7 @@ use client_keys::ClientKeys; use common::display::ReadableProfile; use common::handle_auth::CoopAuthUrlHandler; 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::{ div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, @@ -255,7 +255,7 @@ impl Account { client.unset_signer().await; // Notify the channel about the signer being unset - ingester.send(IngesterSignal::SignerUnset).await; + ingester.send(Signal::SignerUnset).await; }) .detach(); } diff --git a/crates/coop/src/views/new_account.rs b/crates/coop/src/views/new_account.rs index 6a8c5ca..4ebd37c 100644 --- a/crates/coop/src/views/new_account.rs +++ b/crates/coop/src/views/new_account.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; use common::nip96::nip96_upload; -use global::constants::ACCOUNT_IDENTIFIER; +use global::constants::{ACCOUNT_IDENTIFIER, NIP17_RELAYS, NIP65_RELAYS}; use global::nostr_client; use gpui::{ div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, @@ -70,6 +70,7 @@ impl NewAccount { window.open_modal(cx, move |modal, _window, _cx| { let weak_view = weak_view.clone(); let current_view = current_view.clone(); + modal .alert() .title(shared_t!("new_account.backup_label")) @@ -124,8 +125,44 @@ impl NewAccount { // Set the client's signer with the current keys cx.background_spawn(async move { let client = nostr_client(); + + // Set the client's signer with the current keys 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(); } diff --git a/crates/coop/src/views/onboarding.rs b/crates/coop/src/views/onboarding.rs index c81f1dd..4952a03 100644 --- a/crates/coop/src/views/onboarding.rs +++ b/crates/coop/src/views/onboarding.rs @@ -17,6 +17,7 @@ use smallvec::{smallvec, SmallVec}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; +use ui::notification::Notification; use ui::popup_menu::PopupMenu; use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt}; @@ -148,7 +149,10 @@ impl Onboarding { } Err(e) => { cx.update(|window, cx| { - window.push_notification(e.to_string(), cx); + window.push_notification( + Notification::error(e.to_string()).title("Nostr Connect"), + cx, + ); }) .ok(); } diff --git a/crates/coop/src/views/setup_relay.rs b/crates/coop/src/views/setup_relay.rs index 0a8d434..22361a9 100644 --- a/crates/coop/src/views/setup_relay.rs +++ b/crates/coop/src/views/setup_relay.rs @@ -10,7 +10,6 @@ use gpui::{ TextAlign, UniformList, Window, }; use i18n::{shared_t, t}; -use itertools::Itertools; use nostr_sdk::prelude::*; use registry::Registry; use smallvec::{smallvec, SmallVec}; @@ -20,6 +19,8 @@ use ui::input::{InputEvent, InputState, TextInput}; use ui::modal::ModalButtonProps; 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 { cx.new(|cx| SetupRelay::new(kind, window, cx)) } @@ -82,7 +83,7 @@ impl SetupRelay { let filter = Filter::new().kind(kind).author(identity).limit(1); if let Some(event) = client.database().query(filter).await?.first() { - let relays = event + let relays: Vec = event .tags .iter() .filter_map(|tag| tag.as_standardized()) @@ -95,7 +96,7 @@ impl SetupRelay { None } }) - .collect_vec(); + .collect(); Ok(relays) } else { @@ -195,22 +196,32 @@ impl SetupRelay { let task: Task> = cx.background_spawn(async move { let client = nostr_client(); + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + let tags: Vec = relays .iter() .map(|relay| Tag::relay(relay.clone())) .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 - client.send_event_builder(builder).await?; + client.send_event(&event).await?; // Connect to messaging relays - for relay in relays.into_iter() { - _ = client.add_relay(&relay).await; - _ = client.connect_relay(&relay).await; + for relay in relays.iter() { + _ = client.add_relay(relay).await; + _ = client.connect_relay(relay).await; } + // Fetch gift wrap events + ChatSpace::fetch_gift_wrap(&relays, public_key).await; + Ok(()) }); @@ -223,11 +234,8 @@ impl SetupRelay { .ok(); } Err(e) => { - cx.update(|window, cx| { - this.update(cx, |this, cx| { - this.set_error(e.to_string(), window, cx); - }) - .ok(); + this.update_in(cx, |this, window, cx| { + this.set_error(e.to_string(), window, cx); }) .ok(); } diff --git a/crates/coop/src/views/sidebar/list_item.rs b/crates/coop/src/views/sidebar/list_item.rs index 8bbb0cb..d42c541 100644 --- a/crates/coop/src/views/sidebar/list_item.rs +++ b/crates/coop/src/views/sidebar/list_item.rs @@ -15,6 +15,7 @@ use ui::actions::OpenProfile; use ui::avatar::Avatar; use ui::context_menu::ContextMenuExt; use ui::modal::ModalButtonProps; +use ui::skeleton::Skeleton; use ui::{h_flex, ContextModal, StyledExt}; use crate::views::screening; @@ -108,7 +109,21 @@ impl RenderOnce for RoomListItem { self.handler, ) 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() diff --git a/crates/coop/src/views/sidebar/mod.rs b/crates/coop/src/views/sidebar/mod.rs index 7e95d00..2650456 100644 --- a/crates/coop/src/views/sidebar/mod.rs +++ b/crates/coop/src/views/sidebar/mod.rs @@ -33,6 +33,7 @@ mod list_item; const FIND_DELAY: u64 = 600; const FIND_LIMIT: usize = 10; +const TOTAL_SKELETONS: usize = 3; pub fn init(window: &mut Window, cx: &mut App) -> Entity { Sidebar::new(window, cx) @@ -586,6 +587,7 @@ impl Focusable for Sidebar { impl Render for Sidebar { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let registry = Registry::read_global(cx); + let loading = registry.loading; // Get rooms from either search results or the chat registry 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() .image_cache(self.image_cache.clone()) .size_full() @@ -690,7 +701,7 @@ impl Render for Sidebar { .child( uniform_list( "rooms", - rooms.len(), + total_rooms, cx.processor(move |this, range, _window, cx| { this.list_items(&rooms, range, cx) }), diff --git a/crates/global/src/lib.rs b/crates/global/src/lib.rs index d215557..bd8ad7e 100644 --- a/crates/global/src/lib.rs +++ b/crates/global/src/lib.rs @@ -1,4 +1,5 @@ use std::collections::{HashMap, HashSet}; +use std::sync::atomic::AtomicBool; use std::sync::OnceLock; use std::time::Duration; @@ -47,7 +48,7 @@ impl Notice { /// Signals sent through the global event channel to notify UI #[derive(Debug)] -pub enum IngesterSignal { +pub enum Signal { /// A signal to notify UI that the client's signer has been set SignerSet(PublicKey), @@ -64,13 +65,13 @@ pub enum IngesterSignal { Metadata(Event), /// 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 - Finish, + /// A signal to notify UI that gift wrap events still processing + EventProcessing, - /// A signal to notify UI that partial processing of gift wrap events has been completed - PartialFinish, + /// A signal to notify UI that gift wrap events have been processed + EventProcessed(bool), /// A signal to notify UI that no DM relay for current user was found DmRelayNotFound, @@ -81,8 +82,8 @@ pub enum IngesterSignal { #[derive(Debug)] pub struct Ingester { - rx: Receiver, - tx: Sender, + rx: Receiver, + tx: Sender, } impl Default for Ingester { @@ -93,15 +94,15 @@ impl Default for Ingester { impl Ingester { pub fn new() -> Self { - let (tx, rx) = smol::channel::bounded::(2048); + let (tx, rx) = smol::channel::bounded::(2048); Self { rx, tx } } - pub fn signals(&self) -> &Receiver { + pub fn signals(&self) -> &Receiver { &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 { 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. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct CoopSimpleStorage { pub init_at: Timestamp, + pub gift_wrap_sub_id: SubscriptionId, + pub gift_wrap_processing: AtomicBool, pub sent_ids: RwLock>, pub resent_ids: RwLock>>, pub resend_queue: RwLock>, } +impl Default for CoopSimpleStorage { + fn default() -> Self { + Self::new() + } +} + impl CoopSimpleStorage { pub fn new() -> Self { Self { init_at: Timestamp::now(), + gift_wrap_sub_id: SubscriptionId::new("inbox"), + gift_wrap_processing: AtomicBool::new(true), sent_ids: RwLock::new(HashSet::new()), resent_ids: RwLock::new(Vec::new()), resend_queue: RwLock::new(HashMap::new()), diff --git a/crates/registry/src/lib.rs b/crates/registry/src/lib.rs index d188e06..9f2e0da 100644 --- a/crates/registry/src/lib.rs +++ b/crates/registry/src/lib.rs @@ -264,7 +264,7 @@ impl Registry { } /// Load all rooms from the database. - pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context) { + pub fn load_rooms(&mut self, finish: bool, window: &mut Window, cx: &mut Context) { log::info!("Starting to load chat rooms..."); // Get the contact bypass setting @@ -340,8 +340,11 @@ impl Registry { cx.spawn_in(window, async move |this, cx| { match task.await { Ok(rooms) => { - this.update_in(cx, |_, window, cx| { - cx.defer_in(window, |this, _window, cx| { + this.update_in(cx, move |_, window, cx| { + cx.defer_in(window, move |this, _window, cx| { + if finish { + this.set_loading(false, cx); + } this.extend_rooms(rooms, cx); this.sort(cx); }); diff --git a/crates/ui/src/skeleton.rs b/crates/ui/src/skeleton.rs index b7d5eb6..ef09230 100644 --- a/crates/ui/src/skeleton.rs +++ b/crates/ui/src/skeleton.rs @@ -56,7 +56,7 @@ impl RenderOnce for Skeleton { .bg(color) .with_animation( "skeleton", - Animation::new(Duration::from_secs(2)) + Animation::new(Duration::from_secs(3)) .repeat() .with_easing(bounce(ease_in_out)), move |this, delta| { diff --git a/crates/ui/src/tooltip.rs b/crates/ui/src/tooltip.rs index 31d2595..e71b395 100644 --- a/crates/ui/src/tooltip.rs +++ b/crates/ui/src/tooltip.rs @@ -1,5 +1,5 @@ use gpui::{ - deferred, div, relative, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, + div, relative, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, Window, }; use theme::ActiveTheme; @@ -16,7 +16,7 @@ impl Tooltip { impl Render for Tooltip { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div().child(deferred( + div().child( div() .font_family(".SystemUIFont") .m_3() @@ -30,6 +30,6 @@ impl Render for Tooltip { .text_color(cx.theme().text_muted) .line_height(relative(1.25)) .child(self.text.clone()), - )) + ) } }