From e177facef40e89fc6f218f10a978362084ae710a Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Sun, 7 Sep 2025 08:33:21 +0700 Subject: [PATCH] chore: better handle async tasks (#142) * improve some codes * . --- crates/coop/src/chatspace.rs | 124 ++++++++++++--------- crates/coop/src/views/account.rs | 66 ++++++----- crates/coop/src/views/chat/mod.rs | 38 ++++--- crates/coop/src/views/onboarding.rs | 66 +++++------ crates/coop/src/views/sidebar/list_item.rs | 6 - crates/settings/src/lib.rs | 4 +- 6 files changed, 166 insertions(+), 138 deletions(-) diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 602744a..1038382 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -40,7 +40,7 @@ use ui::dock_area::{ClosePanel, DockArea, DockItem}; use ui::modal::ModalButtonProps; use ui::notification::Notification; use ui::popup_menu::PopupMenuExt; -use ui::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt}; +use ui::{h_flex, v_flex, ContextModal, Disableable, IconName, Root, Sizable, StyledExt}; use crate::actions::{DarkMode, Logout, Settings}; use crate::views::compose::compose_button; @@ -64,10 +64,17 @@ pub fn new_account(window: &mut Window, cx: &mut App) { } pub struct ChatSpace { + // Workspace title_bar: Entity, dock: Entity, - auth_requests: Vec<(String, RelayUrl)>, + + // Temporarily store all authentication requests + auth_requests: HashMap, + + // Local state to determine if the user has set up NIP-17 relays has_nip17_relays: bool, + + // System _subscriptions: SmallVec<[Subscription; 3]>, _tasks: SmallVec<[Task<()>; 5]>, } @@ -174,7 +181,7 @@ impl ChatSpace { Self { dock, title_bar, - auth_requests: vec![], + auth_requests: HashMap::new(), has_nip17_relays: true, _subscriptions: subscriptions, _tasks: tasks, @@ -279,9 +286,10 @@ impl ChatSpace { loop { if client.has_signer().await { + total_loops += 1; + if css.gift_wrap_processing.load(Ordering::Acquire) { is_start_processing = true; - total_loops += 1; // Reset gift wrap processing flag let _ = css.gift_wrap_processing.compare_exchange( @@ -295,8 +303,8 @@ impl ChatSpace { ingester.send(signal).await; } else { // Only run further if we are already processing - // Wait until after 3 loops to prevent exiting early while events are still being processed - if is_start_processing && total_loops >= 3 { + // Wait until after 2 loops to prevent exiting early while events are still being processed + if is_start_processing && total_loops >= 2 { let signal = Signal::GiftWrapProcess(UnwrappingStatus::Complete); ingester.send(signal).await; @@ -522,21 +530,19 @@ impl ChatSpace { }); } Signal::Auth(req) => { - let relay_url = &req.url; - let challenge = &req.challenge; + let url = &req.url; let auto_auth = AppSettings::get_auto_auth(cx); - let is_authenticated_relays = - AppSettings::read_global(cx).is_authenticated_relays(relay_url); + let is_authenticated = AppSettings::read_global(cx).is_authenticated(url); view.update(cx, |this, cx| { - this.push_auth_request(challenge, relay_url, cx); + this.push_auth_request(&req, cx); - if auto_auth && is_authenticated_relays { + if auto_auth && is_authenticated { // Automatically authenticate if the relay is authenticated before - this.auth(challenge, relay_url, window, cx); + this.auth(req, window, cx); } else { // Otherwise open the auth request popup - this.open_auth_request(challenge, relay_url, window, cx); + this.open_auth_request(req, window, cx); } }) .ok(); @@ -780,19 +786,18 @@ impl ChatSpace { }; } - fn auth( - &mut self, - challenge: &str, - url: &RelayUrl, - window: &mut Window, - cx: &mut Context, - ) { + fn auth(&mut self, req: AuthRequest, window: &mut Window, cx: &mut Context) { let settings = AppSettings::global(cx); - let challenge = challenge.to_string(); - let url = url.to_owned(); + + let challenge = req.challenge.to_owned(); + let url = req.url.to_owned(); + let challenge_clone = challenge.clone(); let url_clone = url.clone(); + // Set Coop is sending auth for this request + self.sending_auth_request(&challenge, cx); + let task: Task> = cx.background_spawn(async move { let client = nostr_client(); let css = css(); @@ -885,7 +890,7 @@ impl ChatSpace { .ok(); } Err(e) => { - cx.update(|window, cx| { + this.update_in(cx, |_, window, cx| { window.push_notification(Notification::error(e.to_string()), cx); }) .ok(); @@ -895,16 +900,10 @@ impl ChatSpace { .detach(); } - fn open_auth_request( - &mut self, - challenge: &str, - relay_url: &RelayUrl, - window: &mut Window, - cx: &mut Context, - ) { + fn open_auth_request(&mut self, req: AuthRequest, window: &mut Window, cx: &mut Context) { let weak_view = cx.entity().downgrade(); - let challenge = challenge.to_string(); - let relay_url = relay_url.to_owned(); + let challenge = req.challenge.to_owned(); + let relay_url = req.url.to_owned(); let url_as_string = SharedString::from(relay_url.to_string()); let note = Notification::new() @@ -929,19 +928,25 @@ impl ChatSpace { ) .into_any_element() }) - .action(move |_window, _cx| { + .action(move |_window, cx| { let weak_view = weak_view.clone(); - let challenge = challenge.clone(); - let relay_url = relay_url.clone(); + let req = req.clone(); + let loading = weak_view + .read_with(cx, |this, cx| { + this.is_sending_auth_request(&req.challenge, cx) + }) + .unwrap_or_default(); Button::new("approve") .label(t!("common.approve")) .small() .primary() + .loading(loading) + .disabled(loading) .on_click(move |_e, window, cx| { weak_view .update(cx, |this, cx| { - this.auth(&challenge, &relay_url, window, cx); + this.auth(req.clone(), window, cx); }) .ok(); }) @@ -951,23 +956,42 @@ impl ChatSpace { } fn reopen_auth_request(&mut self, window: &mut Window, cx: &mut Context) { - for (challenge, relay_url) in self.auth_requests.clone().iter() { - self.open_auth_request(challenge, relay_url, window, cx); + for req in self.auth_requests.clone().into_iter() { + self.open_auth_request(req.0, window, cx); } } - fn push_auth_request(&mut self, challenge: &str, url: &RelayUrl, cx: &mut Context) { - self.auth_requests.push((challenge.into(), url.to_owned())); + fn push_auth_request(&mut self, req: &AuthRequest, cx: &mut Context) { + self.auth_requests.insert(req.to_owned(), false); cx.notify(); } - fn remove_auth_request(&mut self, challenge: &str, cx: &mut Context) { - if let Some(ix) = self.auth_requests.iter().position(|(c, _)| c == challenge) { - self.auth_requests.remove(ix); - cx.notify(); + fn sending_auth_request(&mut self, challenge: &str, cx: &mut Context) { + for (req, status) in self.auth_requests.iter_mut() { + if req.challenge == challenge { + *status = true; + cx.notify(); + } } } + fn is_sending_auth_request(&self, challenge: &str, _cx: &App) -> bool { + if let Some(req) = self + .auth_requests + .iter() + .find(|(req, _)| req.challenge == challenge) + { + req.1.to_owned() + } else { + false + } + } + + fn remove_auth_request(&mut self, challenge: &str, cx: &mut Context) { + self.auth_requests.retain(|r, _| r.challenge != challenge); + cx.notify(); + } + fn set_onboarding_layout(&mut self, window: &mut Window, cx: &mut Context) { let panel = Arc::new(onboarding::init(window, cx)); let center = DockItem::panel(panel); @@ -1016,6 +1040,11 @@ impl ChatSpace { }); } + fn set_no_nip17_relays(&mut self, cx: &mut Context) { + self.has_nip17_relays = false; + cx.notify(); + } + fn load_local_account(&mut self, window: &mut Window, cx: &mut Context) { let task = cx.background_spawn(async move { let client = nostr_client(); @@ -1059,11 +1088,6 @@ impl ChatSpace { .detach(); } - fn set_no_nip17_relays(&mut self, cx: &mut Context) { - self.has_nip17_relays = false; - cx.notify(); - } - fn on_settings(&mut self, _ev: &Settings, window: &mut Window, cx: &mut Context) { let view = preferences::init(window, cx); diff --git a/crates/coop/src/views/account.rs b/crates/coop/src/views/account.rs index 7153099..cc8b2ae 100644 --- a/crates/coop/src/views/account.rs +++ b/crates/coop/src/views/account.rs @@ -15,6 +15,7 @@ use gpui::{ use i18n::{shared_t, t}; use nostr_connect::prelude::*; use nostr_sdk::prelude::*; +use smallvec::{smallvec, SmallVec}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; @@ -44,6 +45,7 @@ pub struct Account { // Panel name: SharedString, focus_handle: FocusHandle, + _tasks: SmallVec<[Task<()>; 1]>, } impl Account { @@ -59,6 +61,7 @@ impl Account { loading: false, name: "Account".into(), focus_handle: cx.focus_handle(), + _tasks: smallvec![], }) } @@ -89,24 +92,25 @@ impl Account { // Handle auth url with the default browser signer.auth_url_handler(CoopAuthUrlHandler); - // Handle connection - cx.spawn_in(window, async move |_this, cx| { - let client = nostr_client(); + self._tasks.push( + // Handle connection + cx.spawn_in(window, async move |_this, cx| { + let client = nostr_client(); - match signer.bunker_uri().await { - Ok(_) => { - // Set the client's signer with the current nostr connect instance - client.set_signer(signer).await; + match signer.bunker_uri().await { + Ok(_) => { + // Set the client's signer with the current nostr connect instance + client.set_signer(signer).await; + } + Err(e) => { + cx.update(|window, cx| { + window.push_notification(e.to_string(), cx); + }) + .ok(); + } } - Err(e) => { - cx.update(|window, cx| { - window.push_notification(e.to_string(), cx); - }) - .ok(); - } - } - }) - .detach(); + }), + ); } fn set_proxy(&mut self, window: &mut Window, cx: &mut Context) { @@ -240,24 +244,26 @@ impl Account { } fn logout(&mut self, _window: &mut Window, cx: &mut Context) { - cx.background_spawn(async move { - let client = nostr_client(); - let ingester = ingester(); + self._tasks.push( + // Reset the nostr client in the background + cx.background_spawn(async move { + let client = nostr_client(); + let ingester = ingester(); - let filter = Filter::new() - .kind(Kind::ApplicationSpecificData) - .identifier(ACCOUNT_IDENTIFIER); + let filter = Filter::new() + .kind(Kind::ApplicationSpecificData) + .identifier(ACCOUNT_IDENTIFIER); - // Delete account - client.database().delete(filter).await.ok(); + // Delete account + client.database().delete(filter).await.ok(); - // Unset the client's signer - client.unset_signer().await; + // Unset the client's signer + client.unset_signer().await; - // Notify the channel about the signer being unset - ingester.send(Signal::SignerUnset).await; - }) - .detach(); + // Notify the channel about the signer being unset + ingester.send(Signal::SignerUnset).await; + }), + ); } fn set_loading(&mut self, status: bool, cx: &mut Context) { diff --git a/crates/coop/src/views/chat/mod.rs b/crates/coop/src/views/chat/mod.rs index ecd9ead..a4d3186 100644 --- a/crates/coop/src/views/chat/mod.rs +++ b/crates/coop/src/views/chat/mod.rs @@ -168,26 +168,28 @@ impl Chat { } /// Load all messages belonging to this room - fn load_messages(&self, window: &mut Window, cx: &mut Context) { + fn load_messages(&mut self, window: &mut Window, cx: &mut Context) { let load_messages = self.room.read(cx).load_messages(cx); - cx.spawn_in(window, async move |this, cx| { - match load_messages.await { - Ok(events) => { - this.update(cx, |this, cx| { - this.insert_messages(events, cx); - }) - .ok(); - } - Err(e) => { - cx.update(|window, cx| { - window.push_notification(e.to_string(), cx); - }) - .ok(); - } - }; - }) - .detach(); + self._tasks.push( + // Run the task in the background + cx.spawn_in(window, async move |this, cx| { + match load_messages.await { + Ok(events) => { + this.update(cx, |this, cx| { + this.insert_messages(events, cx); + }) + .ok(); + } + Err(e) => { + cx.update(|window, cx| { + window.push_notification(e.to_string(), cx); + }) + .ok(); + } + }; + }), + ); } #[allow(dead_code)] diff --git a/crates/coop/src/views/onboarding.rs b/crates/coop/src/views/onboarding.rs index 62c84a3..2b1b776 100644 --- a/crates/coop/src/views/onboarding.rs +++ b/crates/coop/src/views/onboarding.rs @@ -9,7 +9,7 @@ use gpui::prelude::FluentBuilder; use gpui::{ div, img, px, relative, svg, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement, - Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, + Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, }; use i18n::{shared_t, t}; use nostr_connect::prelude::*; @@ -66,8 +66,8 @@ pub struct Onboarding { // Panel name: SharedString, focus_handle: FocusHandle, - #[allow(dead_code)] - subscriptions: SmallVec<[Subscription; 2]>, + _subscriptions: SmallVec<[Subscription; 2]>, + _tasks: SmallVec<[Task<()>; 1]>, } impl Onboarding { @@ -104,10 +104,11 @@ impl Onboarding { nostr_connect, nostr_connect_uri, qr_code, - subscriptions, connecting: false, name: "Onboarding".into(), focus_handle: cx.focus_handle(), + _subscriptions: subscriptions, + _tasks: smallvec![], } } @@ -131,36 +132,37 @@ impl Onboarding { cx.notify(); }); - cx.spawn_in(window, async move |this, cx| { - let client = nostr_client(); - let connect = this.read_with(cx, |this, cx| this.nostr_connect.read(cx).clone()); + self._tasks.push( + // Wait for Nostr Connect approval + cx.spawn_in(window, async move |this, cx| { + let client = nostr_client(); + let connect = this.read_with(cx, |this, cx| this.nostr_connect.read(cx).clone()); - if let Ok(Some(signer)) = connect { - match signer.bunker_uri().await { - Ok(uri) => { - this.update(cx, |this, cx| { - this.set_connecting(cx); - this.write_uri_to_disk(&uri, cx); - }) - .ok(); + if let Ok(Some(signer)) = connect { + match signer.bunker_uri().await { + Ok(uri) => { + this.update(cx, |this, cx| { + this.set_connecting(cx); + this.write_uri_to_disk(&uri, cx); + }) + .ok(); - // Set the client's signer with the current nostr connect instance - client.set_signer(signer).await; - } - Err(e) => { - log::warn!("Nostr Connect instance (QR Code) is timeout. TODO: fix this"); - this.update_in(cx, |_, window, cx| { - window.push_notification( - Notification::error(e.to_string()).title("Nostr Connect"), - cx, - ); - }) - .ok(); - } - }; - } - }) - .detach(); + // Set the client's signer with the current nostr connect instance + client.set_signer(signer).await; + } + Err(e) => { + this.update_in(cx, |_, window, cx| { + window.push_notification( + Notification::error(e.to_string()).title("Nostr Connect"), + cx, + ); + }) + .ok(); + } + }; + } + }), + ) } fn set_proxy(&mut self, window: &mut Window, cx: &mut Context) { diff --git a/crates/coop/src/views/sidebar/list_item.rs b/crates/coop/src/views/sidebar/list_item.rs index d42c541..b877a88 100644 --- a/crates/coop/src/views/sidebar/list_item.rs +++ b/crates/coop/src/views/sidebar/list_item.rs @@ -11,9 +11,7 @@ use registry::room::RoomKind; use registry::Registry; use settings::AppSettings; use theme::ActiveTheme; -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}; @@ -167,10 +165,6 @@ impl RenderOnce for RoomListItem { .child(created_at), ), ) - .context_menu(move |this, _window, _cx| { - // TODO: add share chat room - this.menu(t!("profile.view"), Box::new(OpenProfile(public_key))) - }) .hover(|this| this.bg(cx.theme().elevated_surface_background)) .on_click(move |event, window, cx| { handler(event, window, cx); diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 4dc0161..3a893db 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -176,12 +176,12 @@ impl AppSettings { !self.setting_values.authenticated_relays.is_empty() && self.setting_values.auto_auth } - pub fn is_authenticated_relays(&self, url: &RelayUrl) -> bool { + pub fn is_authenticated(&self, url: &RelayUrl) -> bool { self.setting_values.authenticated_relays.contains(url) } pub fn push_relay(&mut self, relay_url: &RelayUrl, cx: &mut Context) { - if !self.is_authenticated_relays(relay_url) { + if !self.is_authenticated(relay_url) { self.setting_values .authenticated_relays .push(relay_url.to_owned());