diff --git a/src/ipc.rs b/src/ipc.rs index dcb07a1..cd6a09a 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use tokio::sync::Mutex; +use nostr_sdk::prelude::PublicKey; + use crate::app::{App, Nip46ClientSignerHandle}; use crate::errors::AppError; use crate::feed; @@ -331,6 +333,107 @@ pub async fn serve() -> Result<(), AppError> { let mut tasks = JoinSet::new(); + // Persistent identity backfill. Pairing-time enrichment tries for ~2 + // minutes and stops; if the account's kind-0 was never published (or was + // published later), the row would keep its generic "Amber"/"Remote + // Signer" placeholder forever — which is what a new user with a fresh + // Amber account sees, with no way to know the app resolved nothing. + // This loop retries on a slow cadence while the backend runs: every pass + // collects remote rows still wearing a generic placeholder (only once + // the vault is unlocked — an encrypted vault hides them otherwise), + // fetches kind-0 from the enabled relays, and upgrades the row. A user + // rename always wins: is_generic_pairing_label is the sole gate on the + // label write. Cheap when nothing is pending (one vault scan per pass). + { + let app = app.clone(); + tasks.spawn(async move { + loop { + // Collect pending identities without holding the lock + // across network work. + let pending: Vec<(String, PublicKey)> = { + let guard = app.lock().await; + if guard.is_locked() { + Vec::new() + } else { + guard + .vault + .profiles + .iter() + .filter(|p| { + p.signer_mode == SignerMode::Nip46Client + && profiles::is_generic_pairing_label(&p.label) + }) + .filter_map(|p| { + PublicKey::parse(&p.public_key) + .ok() + .map(|key| (p.public_key.clone(), key)) + }) + .collect() + } + }; + if pending.is_empty() { + tokio::time::sleep(Duration::from_secs(300)).await; + continue; + } + let relay_urls = { + let guard = app.lock().await; + relays::enabled_urls(&guard.settings) + }; + for (npub, identity) in pending { + let found = tokio::time::timeout( + Duration::from_secs(90), + tokio::task::spawn_blocking({ + let relay_urls = relay_urls.clone(); + move || profiles::fetch_profile_metadata(&identity, &relay_urls) + }), + ) + .await + .ok() + .and_then(|join| join.ok()) + .flatten(); + let Some(meta) = found else { continue }; + let mut guard = app.lock().await; + let mut changed = false; + // Re-resolve by pubkey string: another request (rename, + // a pairing-time enrichment) may have edited the row + // while this fetch was in flight. + if let Some(row) = guard + .vault + .profiles + .iter_mut() + .find(|p| p.public_key == npub) + { + if row.picture.is_none() && meta.picture.is_some() { + row.picture = meta.picture.clone(); + changed = true; + } + if row.nip05.is_none() && meta.nip05.is_some() { + row.nip05 = meta.nip05.clone(); + changed = true; + } + 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 profiles::is_generic_pairing_label(&row.label) { + row.label = name.to_string(); + changed = true; + } + } + } + if changed { + let _ = guard.save_vault(); + eprintln!("[backfill] resolved profile identity via kind-0: {npub}"); + } + } + tokio::time::sleep(Duration::from_secs(120)).await; + } + }); + } + while let Some(line) = line_rx.recv().await { if line.trim().is_empty() { continue; diff --git a/src/profiles.rs b/src/profiles.rs index d5b8dfc..2da0bf1 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -553,6 +553,19 @@ pub fn find_stored_profile<'a>( .ok_or_else(|| AppError::profile_not_found(npub)) } +/// Labels the UI assigns when pairing without a user-supplied name +/// (CreateProfileModal defaults to "Amber"; Signer Mode to "Remote +/// Signer"). A remote-signer profile row still carrying one of these has +/// never resolved its real identity from the network, so it stays a +/// candidate for automatic name backfill on every launch. A user rename +/// always falls outside this list and is therefore never overwritten. +pub const GENERIC_PAIRING_LABELS: &[&str] = &["Amber", "Remote Signer"]; + +/// True when `label` is one of the generic pairing placeholders. +pub fn is_generic_pairing_label(label: &str) -> bool { + GENERIC_PAIRING_LABELS.contains(&label) +} + /// Create or refresh the vault profile for a remote (NIP-46) identity. /// /// When a NIP-46 client connection is established the identity lives on the @@ -930,6 +943,20 @@ pub fn delete_profile(vault: &mut Vault, npub: &str) -> Result Vault {