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:
+1
-1
@@ -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"
|
||||
|
||||
@@ -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
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user