diff --git a/crates/coop/src/dialogs/accounts.rs b/crates/coop/src/dialogs/accounts.rs index 7b84b57..b9fe557 100644 --- a/crates/coop/src/dialogs/accounts.rs +++ b/crates/coop/src/dialogs/accounts.rs @@ -10,7 +10,8 @@ use state::{NostrRegistry, SignerEvent}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; -use ui::{h_flex, v_flex, Icon, IconName, Sizable, WindowExtension}; +use ui::indicator::Indicator; +use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, WindowExtension}; use crate::dialogs::connect::ConnectSigner; use crate::dialogs::import::ImportKey; @@ -21,6 +22,9 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity { /// Account selector pub struct AccountSelector { + /// Public key currently being chosen for login + logging_in: Entity>, + /// The error message displayed when an error occurs. error: Entity>, @@ -33,6 +37,7 @@ pub struct AccountSelector { impl AccountSelector { pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let logging_in = cx.new(|_| None); let error = cx.new(|_| None); // Subscribe to the signer events @@ -44,27 +49,53 @@ impl AccountSelector { window.refresh(); } SignerEvent::Error(e) => { - this.error.update(cx, |this, cx| { - *this = Some(e.into()); - cx.notify(); - }); + this.set_error(e.to_string(), cx); } }; }); Self { + logging_in, error, tasks: vec![], _subscription: Some(subscription), } } + fn logging_in(&self, public_key: &PublicKey, cx: &App) -> bool { + self.logging_in.read(cx) == &Some(*public_key) + } + + fn set_logging_in(&mut self, public_key: PublicKey, cx: &mut Context) { + self.logging_in.update(cx, |this, cx| { + *this = Some(public_key); + cx.notify(); + }); + } + + fn set_error(&mut self, error: T, cx: &mut Context) + where + T: Into, + { + self.error.update(cx, |this, cx| { + *this = Some(error.into()); + cx.notify(); + }); + + self.logging_in.update(cx, |this, cx| { + *this = None; + cx.notify(); + }) + } + fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let task = nostr.read(cx).get_signer(&public_key, cx); - let error = self.error.downgrade(); - self.tasks.push(cx.spawn_in(window, async move |_this, cx| { + // Mark the public key as being logged in + self.set_logging_in(public_key, cx); + + self.tasks.push(cx.spawn_in(window, async move |this, cx| { match task.await { Ok(signer) => { nostr.update(cx, |this, cx| { @@ -72,9 +103,8 @@ impl AccountSelector { }); } Err(e) => { - error.update(cx, |this, cx| { - *this = Some(e.to_string().into()); - cx.notify(); + this.update(cx, |this, cx| { + this.set_error(e.to_string(), cx); })?; } }; @@ -120,6 +150,7 @@ impl Render for AccountSelector { let persons = PersonRegistry::global(cx); let nostr = NostrRegistry::global(cx); let npubs = nostr.read(cx).npubs(); + let loading = self.logging_in.read(cx).is_some(); v_flex() .size_full() @@ -139,6 +170,7 @@ impl Render for AccountSelector { for (ix, public_key) in npubs.read(cx).iter().enumerate() { let profile = persons.read(cx).get(public_key, cx); + let logging_in = self.logging_in(public_key, cx); items.push( h_flex() @@ -157,31 +189,35 @@ impl Render for AccountSelector { .child(Avatar::new(profile.avatar()).small()) .child(div().text_sm().child(profile.name())), ) - .child( - h_flex() - .gap_1() - .invisible() - .group_hover("", |this| this.visible()) - .child( - Button::new(format!("del-{ix}")) - .icon(IconName::Close) - .ghost() - .small() - .on_click(cx.listener({ - let public_key = *public_key; - move |this, _ev, _window, cx| { - cx.stop_propagation(); - this.remove(public_key, cx); - } - })), - ), - ) - .on_click(cx.listener({ + .when(logging_in, |this| this.child(Indicator::new().small())) + .when(!logging_in, |this| { + this.child( + h_flex() + .gap_1() + .invisible() + .group_hover("", |this| this.visible()) + .child( + Button::new(format!("del-{ix}")) + .icon(IconName::Close) + .ghost() + .small() + .disabled(logging_in) + .on_click(cx.listener({ + let public_key = *public_key; + move |this, _ev, _window, cx| { + cx.stop_propagation(); + this.remove(public_key, cx); + } + })), + ), + ) + }) + .when(!logging_in, |this| { let public_key = *public_key; - move |this, _ev, window, cx| { + this.on_click(cx.listener(move |this, _ev, window, cx| { this.login(public_key, window, cx); - } - })), + })) + }), ); } @@ -199,6 +235,7 @@ impl Render for AccountSelector { .label("Import") .ghost() .small() + .disabled(loading) .on_click(cx.listener(move |this, _ev, window, cx| { this.open_import(window, cx); })), @@ -209,6 +246,7 @@ impl Render for AccountSelector { .label("Scan QR to connect") .ghost() .small() + .disabled(loading) .on_click(cx.listener(move |this, _ev, window, cx| { this.open_connect(window, cx); })), diff --git a/crates/state/src/constants.rs b/crates/state/src/constants.rs index 5548121..9df12f3 100644 --- a/crates/state/src/constants.rs +++ b/crates/state/src/constants.rs @@ -21,18 +21,18 @@ pub const FIND_DELAY: u64 = 600; /// Default limit for searching pub const FIND_LIMIT: usize = 20; -/// Default timeout for Nostr Connect -pub const NOSTR_CONNECT_TIMEOUT: u64 = 200; - -/// Default Nostr Connect relay -pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nip46.com"; - /// Default subscription id for device gift wrap events pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps"; /// Default subscription id for user gift wrap events pub const USER_GIFTWRAP: &str = "user-gift-wraps"; +/// Default timeout for Nostr Connect +pub const NOSTR_CONNECT_TIMEOUT: u64 = 60; + +/// Default Nostr Connect relay +pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nip46.com"; + /// Default vertex relays pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"]; diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 02691af..ff42195 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -399,7 +399,10 @@ impl NostrRegistry { NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?; let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); - let nip46 = NostrConnect::new(uri, app_keys, timeout, None)?; + let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?; + + // Set the auth URL handler + nip46.auth_url_handler(CoopAuthUrlHandler); Ok(nip46.into_nostr_signer()) }) @@ -419,7 +422,7 @@ impl NostrRegistry { signer.switch(new).await; client.unsubscribe_all().await?; - // Verify and save public key + // Verify and get public key let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; @@ -462,6 +465,7 @@ impl NostrRegistry { })?; } } + Ok(()) })); } @@ -522,8 +526,8 @@ impl NostrRegistry { // Connect and verify the remote signer let task: Task> = cx.background_spawn(async move { - let public_key = async_nip46.get_public_key().await?; let uri = async_nip46.bunker_uri().await?; + let public_key = async_nip46.get_public_key().await?; Ok((public_key, uri)) }); @@ -718,29 +722,6 @@ impl NostrRegistry { self.gossip.read(cx).read_only_relays(public_key) } - /// Generate a direct nostr connection initiated by the client - pub fn nostr_connect(&self, relay: Option) -> (NostrConnect, NostrConnectUri) { - let app_keys = self.app_keys.clone(); - let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); - - // Determine the relay will be used for Nostr Connect - let relay = match relay { - Some(relay) => relay, - None => RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap(), - }; - - // Generate the nostr connect uri - let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME); - - // Generate the nostr connect - let mut signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None).unwrap(); - - // Handle the auth request - signer.auth_url_handler(CoopAuthUrlHandler); - - (signer, uri) - } - /// Get the public key of a NIP-05 address pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task> { let client = self.client();