add loading state
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m43s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 2m1s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled

This commit is contained in:
2026-03-02 14:39:12 +07:00
parent 9eb77c4b76
commit 5a554c2fa4
3 changed files with 84 additions and 65 deletions

View File

@@ -10,7 +10,8 @@ use state::{NostrRegistry, SignerEvent};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; 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::connect::ConnectSigner;
use crate::dialogs::import::ImportKey; use crate::dialogs::import::ImportKey;
@@ -21,6 +22,9 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<AccountSelector> {
/// Account selector /// Account selector
pub struct AccountSelector { pub struct AccountSelector {
/// Public key currently being chosen for login
logging_in: Entity<Option<PublicKey>>,
/// The error message displayed when an error occurs. /// The error message displayed when an error occurs.
error: Entity<Option<SharedString>>, error: Entity<Option<SharedString>>,
@@ -33,6 +37,7 @@ pub struct AccountSelector {
impl AccountSelector { impl AccountSelector {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let logging_in = cx.new(|_| None);
let error = cx.new(|_| None); let error = cx.new(|_| None);
// Subscribe to the signer events // Subscribe to the signer events
@@ -44,27 +49,53 @@ impl AccountSelector {
window.refresh(); window.refresh();
} }
SignerEvent::Error(e) => { SignerEvent::Error(e) => {
this.error.update(cx, |this, cx| { this.set_error(e.to_string(), cx);
*this = Some(e.into());
cx.notify();
});
} }
}; };
}); });
Self { Self {
logging_in,
error, error,
tasks: vec![], tasks: vec![],
_subscription: Some(subscription), _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>) {
self.logging_in.update(cx, |this, cx| {
*this = Some(public_key);
cx.notify();
});
}
fn set_error<T>(&mut self, error: T, cx: &mut Context<Self>)
where
T: Into<SharedString>,
{
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<Self>) { fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let task = nostr.read(cx).get_signer(&public_key, 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 { match task.await {
Ok(signer) => { Ok(signer) => {
nostr.update(cx, |this, cx| { nostr.update(cx, |this, cx| {
@@ -72,9 +103,8 @@ impl AccountSelector {
}); });
} }
Err(e) => { Err(e) => {
error.update(cx, |this, cx| { this.update(cx, |this, cx| {
*this = Some(e.to_string().into()); this.set_error(e.to_string(), cx);
cx.notify();
})?; })?;
} }
}; };
@@ -120,6 +150,7 @@ impl Render for AccountSelector {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let npubs = nostr.read(cx).npubs(); let npubs = nostr.read(cx).npubs();
let loading = self.logging_in.read(cx).is_some();
v_flex() v_flex()
.size_full() .size_full()
@@ -139,6 +170,7 @@ impl Render for AccountSelector {
for (ix, public_key) in npubs.read(cx).iter().enumerate() { for (ix, public_key) in npubs.read(cx).iter().enumerate() {
let profile = persons.read(cx).get(public_key, cx); let profile = persons.read(cx).get(public_key, cx);
let logging_in = self.logging_in(public_key, cx);
items.push( items.push(
h_flex() h_flex()
@@ -157,7 +189,9 @@ impl Render for AccountSelector {
.child(Avatar::new(profile.avatar()).small()) .child(Avatar::new(profile.avatar()).small())
.child(div().text_sm().child(profile.name())), .child(div().text_sm().child(profile.name())),
) )
.child( .when(logging_in, |this| this.child(Indicator::new().small()))
.when(!logging_in, |this| {
this.child(
h_flex() h_flex()
.gap_1() .gap_1()
.invisible() .invisible()
@@ -167,6 +201,7 @@ impl Render for AccountSelector {
.icon(IconName::Close) .icon(IconName::Close)
.ghost() .ghost()
.small() .small()
.disabled(logging_in)
.on_click(cx.listener({ .on_click(cx.listener({
let public_key = *public_key; let public_key = *public_key;
move |this, _ev, _window, cx| { move |this, _ev, _window, cx| {
@@ -176,12 +211,13 @@ impl Render for AccountSelector {
})), })),
), ),
) )
.on_click(cx.listener({ })
.when(!logging_in, |this| {
let public_key = *public_key; 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); this.login(public_key, window, cx);
} }))
})), }),
); );
} }
@@ -199,6 +235,7 @@ impl Render for AccountSelector {
.label("Import") .label("Import")
.ghost() .ghost()
.small() .small()
.disabled(loading)
.on_click(cx.listener(move |this, _ev, window, cx| { .on_click(cx.listener(move |this, _ev, window, cx| {
this.open_import(window, cx); this.open_import(window, cx);
})), })),
@@ -209,6 +246,7 @@ impl Render for AccountSelector {
.label("Scan QR to connect") .label("Scan QR to connect")
.ghost() .ghost()
.small() .small()
.disabled(loading)
.on_click(cx.listener(move |this, _ev, window, cx| { .on_click(cx.listener(move |this, _ev, window, cx| {
this.open_connect(window, cx); this.open_connect(window, cx);
})), })),

View File

@@ -21,18 +21,18 @@ pub const FIND_DELAY: u64 = 600;
/// Default limit for searching /// Default limit for searching
pub const FIND_LIMIT: usize = 20; 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 /// Default subscription id for device gift wrap events
pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps"; pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps";
/// Default subscription id for user gift wrap events /// Default subscription id for user gift wrap events
pub const USER_GIFTWRAP: &str = "user-gift-wraps"; 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 /// Default vertex relays
pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"]; pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];

View File

@@ -399,7 +399,10 @@ impl NostrRegistry {
NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?; NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?;
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); 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()) Ok(nip46.into_nostr_signer())
}) })
@@ -419,7 +422,7 @@ impl NostrRegistry {
signer.switch(new).await; signer.switch(new).await;
client.unsubscribe_all().await?; client.unsubscribe_all().await?;
// Verify and save public key // Verify and get public key
let signer = client.signer().context("Signer not found")?; let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
@@ -462,6 +465,7 @@ impl NostrRegistry {
})?; })?;
} }
} }
Ok(()) Ok(())
})); }));
} }
@@ -522,8 +526,8 @@ impl NostrRegistry {
// Connect and verify the remote signer // Connect and verify the remote signer
let task: Task<Result<(PublicKey, NostrConnectUri), Error>> = let task: Task<Result<(PublicKey, NostrConnectUri), Error>> =
cx.background_spawn(async move { cx.background_spawn(async move {
let public_key = async_nip46.get_public_key().await?;
let uri = async_nip46.bunker_uri().await?; let uri = async_nip46.bunker_uri().await?;
let public_key = async_nip46.get_public_key().await?;
Ok((public_key, uri)) Ok((public_key, uri))
}); });
@@ -718,29 +722,6 @@ impl NostrRegistry {
self.gossip.read(cx).read_only_relays(public_key) 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<RelayUrl>) -> (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 /// Get the public key of a NIP-05 address
pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> { pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
let client = self.client(); let client = self.client();