diff --git a/src/ipc.rs b/src/ipc.rs index db5081d..dc0789c 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -423,12 +423,7 @@ pub async fn serve() -> Result<(), AppError> { 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()); + let real_name = profiles::network_display_name(&meta); if let Some(name) = real_name { if profiles::is_generic_pairing_label(&row.label) { row.label = name.to_string(); diff --git a/src/profiles.rs b/src/profiles.rs index ce36161..6dda70e 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -87,8 +87,14 @@ pub fn create_profile( // Publish kind 0 metadata event so other clients can see the username/display name. // Best-effort: relay failures here never block profile creation. + // + // Generic placeholders are never published: an early build auto-published + // the empty-label default "My Profile" as the account's kind-0, and that + // poisoned metadata then became the "real" name every client (including + // this app's own backfill) resolved forever. A placeholder stays local + // until the user names the profile, and only then goes on the network. let relay_urls = relays::enabled_urls(settings); - if !relay_urls.is_empty() { + if !relay_urls.is_empty() && !is_generic_pairing_label(&label) { publish_metadata_blocking(&keys, &label, None, None, relay_urls); } @@ -142,6 +148,9 @@ pub fn import_profile( .as_ref() .and_then(|m| m.display_name.as_deref().or(m.name.as_deref())) .filter(|name| !name.trim().is_empty()) + // Placeholder kind-0 (see create_profile) is not a name; fall + // back to the shortened npub instead of importing "My Profile". + .filter(|name| !is_generic_pairing_label(name.trim())) .map(str::to_string) .unwrap_or_else(|| shorten_npub(&keys.public_key())) } else { @@ -162,7 +171,7 @@ pub fn import_profile( }); let relay_urls = relays::enabled_urls(settings); - if !relay_urls.is_empty() { + if !relay_urls.is_empty() && !is_generic_pairing_label(&label) { publish_metadata_blocking( &keys, &label, @@ -566,6 +575,23 @@ pub fn is_generic_pairing_label(label: &str) -> bool { GENERIC_PAIRING_LABELS.contains(&label) } +/// The real display name carried by fetched kind-0 metadata, if any. +/// +/// Prefers `display_name` over `name`, rejects blanks, and rejects generic +/// placeholders: early builds published the empty-label default "My Profile" +/// as kind-0, so a placeholder arriving FROM the network is pollution, not a +/// name, and must never be copied over a profile row's label. +pub fn network_display_name(metadata: &Metadata) -> Option { + metadata + .display_name + .as_deref() + .or(metadata.name.as_deref()) + .map(str::trim) + .filter(|name| !name.is_empty()) + .filter(|name| !is_generic_pairing_label(name)) + .map(str::to_string) +} + /// 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 @@ -959,6 +985,26 @@ mod tests { assert!(!is_generic_pairing_label("amber")); assert!(!is_generic_pairing_label("")); } + + #[test] + fn network_display_name_rejects_placeholder_kind0() { + // The exact pollution case: an early build published "My Profile" as + // the account's kind-0. Resolution must treat it as "no name found", + // never as the profile's display name. + let polluted = Metadata::new() + .name("My Profile") + .display_name("My Profile"); + assert_eq!(network_display_name(&polluted), None); + + let blank = Metadata::new().name(" ").display_name(""); + assert_eq!(network_display_name(&blank), None); + + let real = Metadata::new().name("satoshi").display_name("Satoshi ✦"); + assert_eq!(network_display_name(&real).as_deref(), Some("Satoshi ✦")); + + let name_only = Metadata::new().name("satoshi"); + assert_eq!(network_display_name(&name_only).as_deref(), Some("satoshi")); + } use crate::vault::{KdfParams, Vault, VaultCrypto}; fn populated_vault() -> Vault { diff --git a/src/signer/nip46_client.rs b/src/signer/nip46_client.rs index cc85cb8..9b220f1 100644 --- a/src/signer/nip46_client.rs +++ b/src/signer/nip46_client.rs @@ -2583,13 +2583,13 @@ impl Nip46ClientSigner { } // 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()); + // during pairing — a user rename always wins. A + // placeholder coming FROM the network is never a name + // (network_display_name rejects it): early builds + // published the empty-label default "My Profile" as + // kind-0, and copying that over the name the user typed + // at pairing is how display names got lost. + let real_name = profiles::network_display_name(&meta); if let Some(name) = real_name { if row.label == pairing_label { row.label = name.to_string();