Build 63: connect over Nym in seconds, yellow Pay page, gradient-avatar fixes
Nym "Connecting..." for ~1 minute — root cause fixed. open() spawned the wallet sync thread BEFORE init_nostr() created the nostr service, so the sync loop's first pass at service.start() found no service and skipped it; the service only started on the next sync cycle, a full SYNC_DELAY (60s) later. Measured on the dev box: the gap between "identity ready" and "starting service" was 62s, while the relay itself connects over the mixnet in ~2s. Initialising nostr before start_sync collapses that gap to ~1ms (verified 62s -> 1ms). The mixnet sidecar already warms up at app launch, so the relay connect was the only thing waiting — and it waited on loop timing, not on Nym. Added a "first relay Connected ~Nms" log line so the timing is observable from logs going forward. Pay tab is now painted in the yellow theme (Cash App-style brand surface) regardless of the active theme, via a scoped theme override held across the central panel so the fill and every widget pick up the yellow tokens together. Gradient avatar now renders for anonymous npubs on the onboarding identity card and the mobile home header — both hardcoded a flat "N" letter tile while Settings already showed the gradient. The header falls back to the short npub so avatar_any takes the gradient branch.
This commit is contained in:
+30
-2
@@ -15,6 +15,8 @@
|
||||
//! Goblin design tokens: three themes (light/dark/yellow) and density scales,
|
||||
//! taken verbatim from the Goblin design handoff.
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
use egui::Color32;
|
||||
|
||||
use crate::AppConfig;
|
||||
@@ -182,9 +184,35 @@ pub const YELLOW: ThemeTokens = ThemeTokens {
|
||||
dark_base: false,
|
||||
};
|
||||
|
||||
/// Current theme kind from app config (dark is the product default).
|
||||
thread_local! {
|
||||
/// Per-frame theme override (see [`scoped`]). egui renders on one thread, so
|
||||
/// a thread-local Cell scopes a different theme to a single surface without
|
||||
/// touching the persisted app config.
|
||||
static OVERRIDE: Cell<Option<ThemeKind>> = const { Cell::new(None) };
|
||||
}
|
||||
|
||||
/// RAII guard that forces [`kind`]/[`tokens`] to a specific theme for its
|
||||
/// lifetime, restoring the previous value on drop (panic-safe). Used to paint
|
||||
/// one surface — the Pay tab — in the yellow theme regardless of the user's
|
||||
/// chosen theme, à la Cash App's brand-colored pay screen.
|
||||
#[must_use = "the override only lasts while the guard is alive"]
|
||||
pub struct ScopedTheme(Option<ThemeKind>);
|
||||
|
||||
impl Drop for ScopedTheme {
|
||||
fn drop(&mut self) {
|
||||
OVERRIDE.with(|c| c.set(self.0.take()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Override the active theme until the returned guard drops.
|
||||
pub fn scoped(kind: ThemeKind) -> ScopedTheme {
|
||||
ScopedTheme(OVERRIDE.with(|c| c.replace(Some(kind))))
|
||||
}
|
||||
|
||||
/// Current theme kind: a scoped override if one is active, else app config
|
||||
/// (dark is the product default).
|
||||
pub fn kind() -> ThemeKind {
|
||||
AppConfig::theme()
|
||||
OVERRIDE.with(|c| c.get()).unwrap_or_else(AppConfig::theme)
|
||||
}
|
||||
|
||||
/// Current theme tokens.
|
||||
|
||||
+21
-10
@@ -347,10 +347,16 @@ impl GoblinWalletView {
|
||||
});
|
||||
}
|
||||
|
||||
// Central content.
|
||||
// Central content. The Pay tab is painted in the yellow theme (Cash
|
||||
// App-style brand surface) regardless of the user's chosen theme: a
|
||||
// scoped override held across the whole panel so its fill AND every
|
||||
// widget inside pick up the yellow tokens together.
|
||||
let pay = self.tab == Tab::Pay;
|
||||
let _pay_theme = pay.then(|| theme::scoped(theme::ThemeKind::Yellow));
|
||||
let panel_fill = if pay { theme::YELLOW.bg } else { t.bg };
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
fill: t.bg,
|
||||
fill: panel_fill,
|
||||
inner_margin: Margin {
|
||||
left: (View::far_left_inset_margin(ui) + 20.0) as i8,
|
||||
right: (View::get_right_inset() + 20.0) as i8,
|
||||
@@ -713,12 +719,17 @@ impl GoblinWalletView {
|
||||
.nostr_service()
|
||||
.map(|s| {
|
||||
let id = s.identity.read();
|
||||
let hex = hex_of(&id.npub);
|
||||
// With a verified handle show "@name"; otherwise fall back to
|
||||
// the short npub so avatar_any draws the deterministic gradient
|
||||
// (it keys the gradient branch off a leading "npub"), not a
|
||||
// meaningless lettered tile.
|
||||
let h = id
|
||||
.nip05
|
||||
.clone()
|
||||
.map(|n| format!("@{}", n.split('@').next().unwrap_or("")))
|
||||
.unwrap_or_else(|| "N".to_string());
|
||||
(h, hex_of(&id.npub))
|
||||
.unwrap_or_else(|| data::short_npub(&hex));
|
||||
(h, hex)
|
||||
})
|
||||
.unwrap_or_else(|| ("N".to_string(), String::new()));
|
||||
let header_hue = data::hue_of(&header_hex);
|
||||
@@ -2910,7 +2921,8 @@ impl GoblinWalletView {
|
||||
RichText::new(
|
||||
"It's up for grabs the moment it's free — anyone can \
|
||||
claim it, including the next key you rotate to. Your \
|
||||
profile picture is deleted with it.",
|
||||
profile picture is deleted with it. You won't be able \
|
||||
to register another username for 10 minutes.",
|
||||
)
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.surface_text_dim),
|
||||
@@ -3089,8 +3101,8 @@ fn start_claim_flow(claim: &mut ClaimState, name: &str, wallet: &Wallet) {
|
||||
ClaimMsg::Error("That username was just taken".into())
|
||||
}
|
||||
RegisterResult::Rejected(e) if e == "name_change_cooldown" => ClaimMsg::Error(
|
||||
"Easy there — one username change every 10 minutes. \
|
||||
Try again shortly."
|
||||
"You recently released a username — you can register a new \
|
||||
one within 10 minutes."
|
||||
.into(),
|
||||
),
|
||||
RegisterResult::Rejected(e) => ClaimMsg::Error(e),
|
||||
@@ -3126,11 +3138,10 @@ fn start_release(claim: &mut ClaimState, name: &str, wallet: &Wallet) {
|
||||
Ok(rt) => rt,
|
||||
Err(_) => return,
|
||||
};
|
||||
// Release is always allowed server-side (it's what arms the cooldown),
|
||||
// so there's no cooldown rejection to handle here.
|
||||
let msg = match rt.block_on(crate::nostr::nip05::unregister(&server, &name, &keys)) {
|
||||
Ok(()) => ClaimMsg::Released,
|
||||
Err(e) if e.contains("name_change_cooldown") => ClaimMsg::Error(
|
||||
"Easy there — one username change every 10 minutes. Try again shortly.".into(),
|
||||
),
|
||||
Err(e) => ClaimMsg::Error(format!("Couldn't release: {e}")),
|
||||
};
|
||||
*slot.lock().unwrap() = Some(msg);
|
||||
|
||||
@@ -685,7 +685,14 @@ impl OnboardingContent {
|
||||
w::card(ui, |ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.horizontal(|ui| {
|
||||
w::avatar(ui, "N", 44.0, 6);
|
||||
// Same deterministic gradient + Grin mark the rest of the app shows
|
||||
// for this key; only fall back to a placeholder while the key is
|
||||
// still being generated (npub not yet available).
|
||||
if npub.is_empty() {
|
||||
w::avatar(ui, "N", 44.0, 6);
|
||||
} else {
|
||||
w::gradient_avatar(ui, &npub, 44.0);
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
ui.vertical(|ui| {
|
||||
let short = if npub.len() > 20 {
|
||||
|
||||
@@ -535,12 +535,43 @@ async fn run_service(svc: Arc<NostrService>, wallet: Wallet) {
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
let connect_started = std::time::Instant::now();
|
||||
client.connect().await;
|
||||
{
|
||||
let mut w_client = svc.client.write();
|
||||
*w_client = Some(client.clone());
|
||||
}
|
||||
|
||||
// Instrumentation: log the moment the first relay actually reaches Connected
|
||||
// over the mixnet, measured from the connect() call. Cold-start latency is
|
||||
// then read off the wall clock (paired with the "Nym sidecar ready after
|
||||
// ~Nms" line above) instead of guessed. Non-blocking; exits on first success.
|
||||
{
|
||||
let client_probe = client.clone();
|
||||
let svc_probe = svc.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
if relays_connected(&client_probe).await {
|
||||
info!(
|
||||
"nostr: first relay Connected ~{}ms after connect()",
|
||||
connect_started.elapsed().as_millis()
|
||||
);
|
||||
return;
|
||||
}
|
||||
if svc_probe.shutdown.load(Ordering::SeqCst)
|
||||
|| connect_started.elapsed() > Duration::from_secs(150)
|
||||
{
|
||||
warn!(
|
||||
"nostr: no relay Connected within {}ms of connect()",
|
||||
connect_started.elapsed().as_millis()
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Publish identity events (kind 10050 DM relays; kind 0 only when named).
|
||||
publish_identity(&svc, &client).await;
|
||||
|
||||
|
||||
+11
-3
@@ -375,6 +375,17 @@ impl Wallet {
|
||||
self.account_time
|
||||
.store(Utc::now().timestamp(), Ordering::Relaxed);
|
||||
|
||||
// Initialize the nostr identity + service BEFORE spawning the
|
||||
// sync thread. The sync loop starts the service on its first
|
||||
// iteration (wallet.rs, top of the loop); if the thread races
|
||||
// ahead of this, that first iteration finds no service, skips
|
||||
// it, and the service doesn't start until the NEXT cycle — a
|
||||
// full SYNC_DELAY (60s) later. That 60s gap (not the mixnet,
|
||||
// which connects a relay in ~2s) is the "stuck on Connecting…
|
||||
// for a minute" symptom. Synchronous + on this thread, so the
|
||||
// service is guaranteed present when start_sync runs.
|
||||
self.init_nostr(&nostr_password);
|
||||
|
||||
// Start new synchronization thread or wake up existing one.
|
||||
let mut thread_w = self.sync_thread.write();
|
||||
if thread_w.is_none() {
|
||||
@@ -398,9 +409,6 @@ impl Wallet {
|
||||
// Update Slatepack address and secret key.
|
||||
self.update_secret_key_addr()?;
|
||||
|
||||
// Initialize nostr identity and service (non-fatal on failure).
|
||||
self.init_nostr(&nostr_password);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user