1
0
forked from GRIN/grim

Build 81: show names everywhere, no @, tidier pay entry

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.
This commit is contained in:
2ro
2026-06-15 01:14:36 -04:00
parent f715149302
commit 96daed3c84
7 changed files with 90 additions and 41 deletions
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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();
}
+32 -17
View File
@@ -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<egui::TextureHandle> {
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<char> = name.chars().collect();
let short: String = if chars.len() > 8 {
format!("{}", chars[..8].iter().collect::<String>())
} 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),
);
+27 -20
View File
@@ -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<egui::TextureHandle> {
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)
+14 -2
View File
@@ -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;
}
}
}
});
+11
View File
@@ -725,6 +725,14 @@ async fn run_service(svc: Arc<NostrService>, 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<NostrService>, 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
+4
View File
@@ -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) => {