fix(signer): connect immediately after identity; fetch kind-0 metadata in background

The adopt step blocked on a best-effort relay metadata fetch before
flipping the session to Connected, so after Amber approved, the UI sat
looking dead for many seconds (the 'Amber said yes but nothing changed'
bug). Connected now flips as soon as the identity is verified and
persisted; the real display name / picture / nip05 land asynchronously
via a background task that never overrides a user-chosen label.
This commit is contained in:
Avi 2026-09-12 15:29:53 -05:00
commit 286bbcaa04

View file

@ -1450,58 +1450,22 @@ impl Nip46ClientSigner {
.to_bech32() .to_bech32()
.map_err(|e| format!("Could not encode identity npub: {e}"))?; .map_err(|e| format!("Could not encode identity npub: {e}"))?;
// Best-effort: look up the identity's kind-0 metadata so the profile // NOTE: kind-0 metadata (display name / picture / nip05) is fetched
// row carries the real display name / picture / nip05 instead of a // AFTER the session flips to Connected, as a background enrichment
// bare npub or the generic pairing label. Relay hiccups fall back to // (see below). A slow or silent relay must never delay the connection
// the pairing label — the connection must not depend on them. // becoming usable — this used to block here and made pairing look
let relays_for_meta = { // dead in the UI for many seconds after the signer had already
let app = self.app.lock().await; // approved.
crate::relays::enabled_urls(&app.settings) let pairing_label = label;
}; let display_label = pairing_label.clone();
let meta_npub = identity;
let metadata = match tokio::time::timeout(
Duration::from_secs(3),
tokio::task::spawn_blocking(move || {
profiles::fetch_profile_metadata(&meta_npub, &relays_for_meta)
}),
)
.await
{
Ok(joined) => joined.ok().flatten(),
Err(_elapsed) => None,
};
let display_label = metadata
.as_ref()
.and_then(|m| m.display_name.as_deref().or(m.name.as_deref()))
.map(str::trim)
.filter(|name| !name.is_empty())
.map(str::to_string)
.unwrap_or(label);
// Persist: profile row for the identity, connection + secret store // Persist: profile row for the identity, connection + secret store
// re-keyed under it. Refuses (fails the handshake) if the identity // re-keyed under it. Refuses (fails the handshake) if the identity
// collides with a local profile. // collides with a local profile.
{ {
let mut app = self.app.lock().await; let mut app = self.app.lock().await;
profiles::store_remote_profile(&mut app.vault, &identity_npub, display_label) profiles::store_remote_profile(&mut app.vault, &identity_npub, display_label.clone())
.map_err(|e| e.message().to_string())?; .map_err(|e| e.message().to_string())?;
// Carry the looked-up metadata (picture / nip05) onto the row so
// the profile shows its real identity, not just a name.
if let Some(meta) = &metadata {
if let Some(row) = app
.vault
.profiles
.iter_mut()
.find(|p| p.public_key == identity_npub)
{
if row.picture.is_none() {
row.picture = meta.picture.clone();
}
if row.nip05.is_none() {
row.nip05 = meta.nip05.clone();
}
}
}
// Re-key the stored secret under the identity npub so the // Re-key the stored secret under the identity npub so the
// connection row, VaultRef, and secret store all agree. // connection row, VaultRef, and secret store all agree.
let connect_ref = crate::signer::VaultRef::from_connection(&connection); let connect_ref = crate::signer::VaultRef::from_connection(&connection);
@ -1541,9 +1505,71 @@ impl Nip46ClientSigner {
// so later teardown (disconnect) deletes the secret under the ref it // so later teardown (disconnect) deletes the secret under the ref it
// was actually stored at. // was actually stored at.
if let Some(conn) = inner.connection.as_mut() { if let Some(conn) = inner.connection.as_mut() {
conn.profile_npub = Some(identity_npub); conn.profile_npub = Some(identity_npub.clone());
} }
inner.phase = Nip46Phase::Connected; inner.phase = Nip46Phase::Connected;
drop(inner);
// Background enrichment (post-Connected by design): look up the
// identity's kind-0 metadata so the profile row carries the real
// display name / picture / nip05 instead of the generic pairing
// label. A slow or silent relay must never gate the session — the
// UI polls status and vault, so the row fills in a moment later.
{
let app = self.app.clone();
tokio::spawn(async move {
let relays_for_meta = {
let app = app.lock().await;
crate::relays::enabled_urls(&app.settings)
};
let meta = match tokio::time::timeout(
Duration::from_secs(10),
tokio::task::spawn_blocking(move || {
profiles::fetch_profile_metadata(&identity, &relays_for_meta)
}),
)
.await
{
Ok(Ok(Some(meta))) => meta,
_ => return,
};
let mut app = app.lock().await;
let mut changed = false;
if let Some(row) = app
.vault
.profiles
.iter_mut()
.find(|p| p.public_key == identity_npub)
{
if row.picture.is_none() {
row.picture = meta.picture.clone();
changed = true;
}
if row.nip05.is_none() {
row.nip05 = meta.nip05.clone();
changed = true;
}
// Upgrade the generic pairing label to the real display
// name only while the row still carries the label we set
// during pairing — a user rename always wins.
let real_name = meta
.display_name
.as_deref()
.or(meta.name.as_deref())
.map(str::trim)
.filter(|name| !name.is_empty());
if let Some(name) = real_name {
if row.label == pairing_label {
row.label = name.to_string();
changed = true;
}
}
}
if changed {
let _ = app.save_vault();
}
});
}
Ok(()) Ok(())
} }