From 96daed3c84d0221d6355748c7e02883b62514d73 Mon Sep 17 00:00:00 2001 From: 2ro <17595647+2ro@users.noreply.github.com> Date: Mon, 15 Jun 2026 01:14:36 -0400 Subject: [PATCH] Build 81: show names everywhere, no @, tidier pay entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve a counterparty's @username on every interaction, not just incoming requests — receives, sends and requests all kick off a verify-and-cache, plus a one-time backfill on wallet open — so activity and the recent strip show names, not bare npubs. Usernames now render WITHOUT the @ (kept internally for avatar lookup); the recent row ellipsizes past 8 chars and centers the name under the avatar, while activity shows the full name. Pay screen: the numpad now sits above the note so the soft keyboard can't cover it (and tapping the pad dismisses the note's keyboard), and a no-op key — a second dot, a 0 on a leading zero, the 9-decimal cap — fires a short error haptic instead of doing nothing silently. --- locales/en.yml | 2 +- src/gui/views/goblin/data.rs | 2 +- src/gui/views/goblin/mod.rs | 49 +++++++++++++++++++++------------ src/gui/views/goblin/send.rs | 47 +++++++++++++++++-------------- src/gui/views/goblin/widgets.rs | 16 +++++++++-- src/nostr/client.rs | 11 ++++++++ src/wallet/wallet.rs | 4 +++ 7 files changed, 90 insertions(+), 41 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 0eb1ae1..bc9e11b 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -686,7 +686,7 @@ goblin: tab_my_code: "My Code" request_from: "Request from" send_to: "Send to" - search_hint: "@handle, npub, or name" + search_hint: "handle, npub, or name" suggested: "%{icon} Suggested" no_contacts: "No contacts yet. Find someone by their @handle." no_profile: "no profile" diff --git a/src/gui/views/goblin/data.rs b/src/gui/views/goblin/data.rs index 8014c34..454681c 100644 --- a/src/gui/views/goblin/data.rs +++ b/src/gui/views/goblin/data.rs @@ -194,7 +194,7 @@ pub fn display_name(contact: &Contact) -> String { if let (Some(nip05), Some(_)) = (&contact.nip05, contact.nip05_verified_at) { if let Some((name, domain)) = nip05.split_once('@') { if domain == crate::nostr::relays::HOME_NIP05_DOMAIN { - return format!("@{}", name); + return name.to_string(); } return nip05.clone(); } diff --git a/src/gui/views/goblin/mod.rs b/src/gui/views/goblin/mod.rs index 12e3bf8..872d557 100644 --- a/src/gui/views/goblin/mod.rs +++ b/src/gui/views/goblin/mod.rs @@ -645,7 +645,7 @@ impl GoblinWalletView { let h = id .nip05 .clone() - .map(|n| format!("@{}", n.split('@').next().unwrap_or(""))) + .map(|n| n.split('@').next().unwrap_or("").to_string()) .unwrap_or_else(|| data::short_npub(&hex_of(&id.npub))); (h, s.is_connected(), hex_of(&id.npub)) }) @@ -713,7 +713,9 @@ impl GoblinWalletView { wallet: &Wallet, handle: &str, ) -> Option { - if !handle.starts_with('@') { + // Avatars live on the nip05 server, keyed by handle. Handles no longer + // carry an '@'; skip bare-npub and empty display names (no avatar there). + if handle.is_empty() || handle.starts_with("npub1") { return None; } let server = wallet @@ -816,7 +818,7 @@ impl GoblinWalletView { let h = id .nip05 .clone() - .map(|n| format!("@{}", n.split('@').next().unwrap_or(""))) + .map(|n| n.split('@').next().unwrap_or("").to_string()) .unwrap_or_else(|| data::short_npub(&hex)); (h, hex) }) @@ -927,15 +929,28 @@ impl GoblinWalletView { .show(ui, |ui| { ui.horizontal(|ui| { for ((name, hue, npub), tex) in peers.iter().zip(texs.iter()) { - ui.vertical(|ui| { - let resp = w::avatar_any(ui, name, npub, 48.0, *hue, tex.as_ref()); - ui.add_space(6.0); - let short: String = name.chars().take(6).collect(); - ui.label(RichText::new(short).font(FontId::new(12.0, fonts::medium()))); - if resp.clicked() { - self.profile = Some(npub.clone()); - } - }); + // Fixed-width centered cell so the name sits centered under the + // avatar (not left-aligned to a wider label). + ui.allocate_ui_with_layout( + Vec2::new(72.0, 78.0), + Layout::top_down(Align::Center), + |ui| { + let resp = w::avatar_any(ui, name, npub, 48.0, *hue, tex.as_ref()); + ui.add_space(6.0); + let chars: Vec = name.chars().collect(); + let short: String = if chars.len() > 8 { + format!("{}…", chars[..8].iter().collect::()) + } else { + name.to_string() + }; + ui.label( + RichText::new(short).font(FontId::new(12.0, fonts::medium())), + ); + if resp.clicked() { + self.profile = Some(npub.clone()); + } + }, + ); ui.add_space(12.0); } }); @@ -956,7 +971,7 @@ impl GoblinWalletView { let h = id .nip05 .clone() - .map(|n| format!("@{}", n.split('@').next().unwrap_or(""))) + .map(|n| n.split('@').next().unwrap_or("").to_string()) .unwrap_or_else(|| data::short_npub(&hex)); (h, hex) }) @@ -1070,7 +1085,7 @@ impl GoblinWalletView { // desktop windows get neither input. let typed_hint = !narrow && self.pay_amount.is_empty(); if narrow { - w::numpad(ui, &mut self.pay_amount); + w::numpad(ui, &mut self.pay_amount, cb); } else { w::amount_typed_input(ui, &mut self.pay_amount); if typed_hint { @@ -1788,7 +1803,7 @@ impl GoblinWalletView { identity .nip05 .clone() - .map(|n| format!("@{}", n.split('@').next().unwrap_or(""))) + .map(|n| n.split('@').next().unwrap_or("").to_string()) .unwrap_or_else(|| data::short_npub(&hex_of(&identity.npub))) }) .unwrap_or_else(|| "—".to_string()); @@ -1932,7 +1947,7 @@ impl GoblinWalletView { .map(|n| n.split('@').next().unwrap_or("").to_string()); let handle = bare .clone() - .map(|n| format!("@{n}")) + .map(|n| n.to_string()) .unwrap_or_else(|| data::short_npub(&hex_of(&identity.npub))); ( handle, @@ -3450,7 +3465,7 @@ impl GoblinWalletView { ); ui.add_space(4.0); ui.label( - RichText::new(format!("@{name}")) + RichText::new(name.to_string()) .font(FontId::new(20.0, fonts::bold())) .color(t.surface_text), ); diff --git a/src/gui/views/goblin/send.rs b/src/gui/views/goblin/send.rs index d60fb28..a80c286 100644 --- a/src/gui/views/goblin/send.rs +++ b/src/gui/views/goblin/send.rs @@ -31,14 +31,15 @@ use super::avatars::AvatarTextures; use super::data::{self, display_name, recent_peers, search_contacts, short_npub}; use super::widgets::{self as w, HoldToSend}; -/// Avatar texture for a display handle ("@name"), if one is cached. +/// Avatar texture for a display handle, if one is cached. Handles no longer +/// carry an '@'; bare-npub / empty names have no avatar on the server. fn tex_for( avatars: &mut AvatarTextures, ctx: &egui::Context, wallet: &Wallet, name: &str, ) -> Option { - if !name.starts_with('@') { + if name.is_empty() || name.starts_with("npub1") { return None; } let server = wallet @@ -661,7 +662,7 @@ impl SendFlow { .map(|s| { let nip05 = s.identity.read().nip05.clone(); let handle = nip05 - .map(|n| format!("@{}", n.split('@').next().unwrap_or(""))) + .map(|n| n.split('@').next().unwrap_or("").to_string()) .unwrap_or_else(|| short_npub(&s.public_key().to_hex())); (handle, s.npub(), s.nprofile()) }) @@ -786,7 +787,7 @@ impl SendFlow { let name = p .nip05 .as_deref() - .map(|n| format!("@{}", n.split('@').next().unwrap_or(""))) + .map(|n| n.split('@').next().unwrap_or("").to_string()) .or(p.name) .unwrap_or_else(|| short_npub(&hex)); LookupResult::Found(Candidate { @@ -812,7 +813,7 @@ impl SendFlow { } else if let Some((name, domain)) = nip05::split_identifier(&query) { // Name / @handle → goblin.st (or other) nip05 resolution. self.looking_up = true; - let label = format!("@{name}"); + let label = name.to_string(); std::thread::spawn(move || { let res = match resolve_nip05_blocking(&name, &domain) { Some(r) => { @@ -822,7 +823,7 @@ impl SendFlow { // can't masquerade as goblin handles; the NIP-05 root // convention `_@domain` displays as just the domain. let display = if home { - format!("@{name}") + name.to_string() } else if name == "_" { domain.clone() } else { @@ -964,8 +965,26 @@ impl SendFlow { }); ui.add_space(16.0); - // Note field. - let mut note_focused = false; + let note_id = egui::Id::from("send_note"); + // Numpad / typed amount FIRST, then the note BELOW it. On mobile the soft + // keyboard for the note covers the bottom of the screen — keeping the pad + // above it means the pad stays visible and tappable, instead of being + // hidden behind the keyboard (the old order trapped you in the note). + let note_focused = ui.ctx().memory(|m| m.has_focus(note_id)); + if !View::is_desktop() { + if w::numpad(ui, &mut self.amount, cb) { + // Tapping the pad means you're back on the amount — drop the note's + // focus so its keyboard goes away. + ui.ctx().memory_mut(|m| m.surrender_focus(note_id)); + } + } else if !note_focused { + // Only consume keystrokes for the amount when the note field is + // not focused, so typing a note doesn't also edit the amount. + w::amount_typed_input(ui, &mut self.amount); + } + ui.add_space(12.0); + + // Note field (under the pad). w::card(ui, |ui| { ui.horizontal(|ui| { ui.label( @@ -974,26 +993,14 @@ impl SendFlow { .color(t.surface_text_dim), ); ui.add_space(8.0); - let note_id = egui::Id::from("send_note"); TextEdit::new(note_id) .focus(false) .hint_text(t!("goblin.send.note_hint")) .text_color(t.surface_text) .body() .ui(ui, &mut self.note, cb); - note_focused = ui.ctx().memory(|m| m.has_focus(note_id)); }); }); - ui.add_space(12.0); - - // Numpad (mobile) — desktop also accepts keyboard via egui events. - if !View::is_desktop() { - w::numpad(ui, &mut self.amount); - } else if !note_focused { - // Only consume keystrokes for the amount when the note field is - // not focused, so typing a note doesn't also edit the amount. - w::amount_typed_input(ui, &mut self.amount); - } ui.add_space(8.0); let valid = amount_from_hr_string(&self.amount) diff --git a/src/gui/views/goblin/widgets.rs b/src/gui/views/goblin/widgets.rs index daceaf4..c852746 100644 --- a/src/gui/views/goblin/widgets.rs +++ b/src/gui/views/goblin/widgets.rs @@ -721,7 +721,11 @@ pub fn send_receive(ui: &mut Ui) -> (bool, bool) { } /// A simple numeric keypad. Mutates `amount` string. Returns true if changed. -pub fn numpad(ui: &mut Ui, amount: &mut String) -> bool { +pub fn numpad( + ui: &mut Ui, + amount: &mut String, + cb: &dyn crate::gui::platform::PlatformCallbacks, +) -> bool { let t = theme::tokens(); let mut changed = false; let keys = [ @@ -770,8 +774,16 @@ pub fn numpad(ui: &mut Ui, amount: &mut String) -> bool { col, ); if resp.clicked() { + let before = amount.clone(); apply_key(amount, k); - changed = true; + if *amount == before { + // A no-op key — a second '.', a '0' on a leading zero, the + // 9-decimal cap, or backspace on empty. Nudge with a short + // error haptic instead of silently doing nothing. + cb.vibrate_error(); + } else { + changed = true; + } } } }); diff --git a/src/nostr/client.rs b/src/nostr/client.rs index 3428d6b..d001bf6 100644 --- a/src/nostr/client.rs +++ b/src/nostr/client.rs @@ -725,6 +725,14 @@ async fn run_service(svc: Arc, wallet: Wallet) { // Re-dispatch pending outgoing messages after restart. reconcile(&svc, &wallet).await; + // Backfill @usernames for contacts we only know by npub (e.g. from before + // this resolved on every interaction), so activity shows names not keys. + for contact in svc.store.all_contacts() { + if contact.nip05.is_none() || contact.nip05_verified_at.is_none() { + svc.resolve_contact_identity(&contact.npub); + } + } + svc.store.set_last_connected_at(unix_time()); svc.store.prune_processed(); @@ -1128,6 +1136,9 @@ async fn handle_wrap(svc: &Arc, wallet: &Wallet, client: &Client, match decision { IngestDecision::AutoReceive => { svc.ensure_contact(&sender_hex); + // Resolve the sender's @username so the receive shows their name in + // activity, not a bare npub. + svc.resolve_contact_identity(&sender_hex); match wallet.nostr_receive(&slate) { Ok((_, reply_text)) => { // Record BEFORE dispatching the reply: crash here is diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 30ce823..e413f0f 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -2265,6 +2265,9 @@ async fn handle_task(w: &Wallet, t: WalletTask) { contact.unknown = false; service.store.save_contact(&contact); } + // Resolve the recipient's @username so activity shows + // their name, not a bare npub. + service.resolve_contact_identity(receiver); service.set_send_phase(crate::nostr::send_phase::SENT); } Err(e) => { @@ -2348,6 +2351,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) { contact.unknown = false; service.store.save_contact(&contact); } + service.resolve_contact_identity(receiver); service.set_send_phase(crate::nostr::send_phase::SENT); } Err(e) => {