feat: multi-account switcher #14
@@ -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);
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -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"];
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user