diff --git a/src/gui/views/goblin/mod.rs b/src/gui/views/goblin/mod.rs index a0f587f..769d671 100644 --- a/src/gui/views/goblin/mod.rs +++ b/src/gui/views/goblin/mod.rs @@ -662,10 +662,15 @@ impl GoblinWalletView { w::avatar_any(ui, &handle, &npub_hex, 28.0, hue, tex.as_ref()); ui.add_space(10.0); ui.vertical(|ui| { - // Smaller so the npub/handle stays on one line. + // Scale the handle to its length: short @names get a + // big, legible size; a long npub shrinks to stay on + // one line. + let len = handle.chars().count() as f32; + let handle_font = + (20.0 - (len - 6.0).max(0.0) * 0.7).clamp(11.0, 16.0); ui.label( RichText::new(&handle) - .font(FontId::new(11.0, fonts::semibold())) + .font(FontId::new(handle_font, fonts::semibold())) .color(t.surface_text), ); ui.label( diff --git a/src/gui/views/goblin/send.rs b/src/gui/views/goblin/send.rs index 0895ebf..24e7311 100644 --- a/src/gui/views/goblin/send.rs +++ b/src/gui/views/goblin/send.rs @@ -771,7 +771,7 @@ impl SendFlow { }); std::thread::spawn(move || { let hue = data::hue_of(&hex); - let profile = service.and_then(|s| s.fetch_profile_blocking(&hex)); + let profile = service.and_then(|s| s.fetch_profile_blocking(&hex, &key_hints)); let res = match (known, profile) { // Already a saved contact — trust it. (Some((name, hue)), _) => LookupResult::Found(Candidate { diff --git a/src/nostr/client.rs b/src/nostr/client.rs index 13e6385..7947abe 100644 --- a/src/nostr/client.rs +++ b/src/nostr/client.rs @@ -162,18 +162,26 @@ impl NostrService { self.keys.clone() } - /// Fetch a pubkey's published kind-0 profile over the connected relay - /// pool (one shot, short timeout). `Some` means the key is a live nostr - /// identity; `None` means no profile is published (new/anonymous key) or - /// the relays were unreachable. Blocking — call from a worker thread. - pub fn fetch_profile_blocking(&self, hex: &str) -> Option { + /// Fetch a pubkey's published kind-0 profile (one shot, short timeout). + /// `Some` means the key is a live nostr identity; `None` means no profile is + /// published (new/anonymous key) or the relays were unreachable. `hints` are + /// extra relays to dial first — the profile may live only on the target's own + /// relays (NIP-65/gossip), which we won't otherwise be connected to. Blocking; + /// call from a worker thread. + pub fn fetch_profile_blocking(&self, hex: &str, hints: &[String]) -> Option { let client = self.client.read().clone()?; let pk = PublicKey::from_hex(hex).ok()?; + let hints: Vec = hints.to_vec(); let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .ok()?; rt.block_on(async { + // Dial the target's own relays so their kind-0 is reachable even when + // it isn't mirrored to any relay we already hold. + if !hints.is_empty() { + connect_relays(&client, &hints).await; + } let filter = Filter::new().kind(Kind::Metadata).author(pk).limit(1); let events = client .fetch_events(filter, Duration::from_secs(10)) @@ -434,6 +442,11 @@ impl NostrService { } } + // NIP-17 delivers to the RECIPIENT's relays, which may differ from ours; + // dial any we don't already hold so the gift wrap actually reaches their + // inbox (otherwise `send_*_to` errors "relay not found" / never arrives). + connect_relays(&client, &urls).await; + let res = tokio::time::timeout( SEND_TIMEOUT, client.send_private_msg_to(urls, receiver, content, tags), @@ -474,6 +487,8 @@ impl NostrService { } } + connect_relays(&client, &urls).await; + let res = tokio::time::timeout( SEND_TIMEOUT, client.send_private_msg_to(urls, receiver, content, tags), @@ -550,10 +565,17 @@ impl NostrService { return; } } + // Any DM relays we've already learned for them are the best hint for where + // their profile lives (their messages came from there). + let hints = self + .store + .contact(sender_hex) + .map(|c| c.relays) + .unwrap_or_default(); let svc = self.clone(); let hex = sender_hex.to_string(); thread::spawn(move || { - let Some(profile) = svc.fetch_profile_blocking(&hex) else { + let Some(profile) = svc.fetch_profile_blocking(&hex, &hints) else { return; }; let Some(nip05) = profile.nip05 else { @@ -756,6 +778,23 @@ async fn nym_socks_ready() -> bool { ) } +/// Add + dial every relay in `urls` so a targeted send reaches relays we don't +/// already hold (NIP-65/gossip: the recipient's relays may differ from ours). +/// `add_relay` is idempotent and `try_connect_relay` returns once connected or +/// the timeout lapses; dialed concurrently so a slow relay doesn't stall the rest. +async fn connect_relays(client: &Client, urls: &[String]) { + let dials = urls.iter().map(|url| { + let url = url.clone(); + async move { + let _ = client.add_relay(&url).await; + let _ = client + .try_connect_relay(&url, Duration::from_secs(12)) + .await; + } + }); + futures::future::join_all(dials).await; +} + /// True when at least one relay has completed its handshake. async fn relays_connected(client: &Client) -> bool { client