diff --git a/src/gui/theme.rs b/src/gui/theme.rs index 842811b9..f46a4127 100644 --- a/src/gui/theme.rs +++ b/src/gui/theme.rs @@ -224,9 +224,22 @@ pub fn tokens() -> &'static ThemeTokens { } } +/// Set each frame by the Pay surface (which paints a bright yellow top under a +/// possibly-dark global theme), so the status bar can pick readable icons for it. +static YELLOW_SURFACE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + +/// Flag whether the bright Pay/yellow surface is currently on screen. +pub fn set_status_surface_yellow(yellow: bool) { + YELLOW_SURFACE.store(yellow, std::sync::atomic::Ordering::Relaxed); +} + /// Whether the status bar should use light (white) icons: true on the dark -/// theme (dark top), false on the light/yellow themes (bright top). +/// theme (dark top), false on the light/yellow themes (bright top). The bright +/// Pay surface forces dark icons even when the global theme is dark. pub fn status_bar_white_icons() -> bool { + if YELLOW_SURFACE.load(std::sync::atomic::Ordering::Relaxed) { + return false; + } tokens().dark_base } diff --git a/src/gui/views/goblin/mod.rs b/src/gui/views/goblin/mod.rs index 95aa3ac7..1ce7b730 100644 --- a/src/gui/views/goblin/mod.rs +++ b/src/gui/views/goblin/mod.rs @@ -380,6 +380,8 @@ impl GoblinWalletView { // widget inside pick up the yellow tokens together. let pay = self.tab == Tab::Pay; let _pay_theme = pay.then(|| theme::scoped(theme::ThemeKind::Yellow)); + // Bright yellow top → dark status-bar icons (see status_bar_white_icons). + theme::set_status_surface_yellow(pay); let panel_fill = if pay { theme::YELLOW.bg } else { t.bg }; egui::CentralPanel::default() .frame(egui::Frame { @@ -946,10 +948,10 @@ impl GoblinWalletView { let dx = 14.0 * (1.0 - p) * (p * std::f32::consts::PI * 9.0).sin(); // Red flash that eases back to the normal ink over the shake. let num = lerp_color(t.neg, t.text, p); - let mark = lerp_color(t.neg, t.text_dim, p); - w::amount_text_centered_shifted(ui, &display, 76.0, num, mark, dx); + // goblin mark keeps its colours; only the digits flash red. + w::amount_text_centered_goblin(ui, &display, 76.0, num, dx); } else { - w::amount_text_centered(ui, &display, 76.0); + w::amount_text_centered_goblin(ui, &display, 76.0, t.text, 0.0); } if let Ok(grin) = display.parse::() { if let Some(preview) = pairing_preview(grin) { diff --git a/src/gui/views/goblin/widgets.rs b/src/gui/views/goblin/widgets.rs index f5603820..0ad8e567 100644 --- a/src/gui/views/goblin/widgets.rs +++ b/src/gui/views/goblin/widgets.rs @@ -227,6 +227,42 @@ pub fn amount_text_centered_shifted( }); } +/// Pay-screen hero amount: the number with the **goblin mark** as its unit (in +/// place of the ツ glyph), centered. `dx` is the over-balance shake offset. +pub fn amount_text_centered_goblin(ui: &mut Ui, value: &str, size: f32, num_ink: Color32, dx: f32) { + let avail = ui.available_width(); + let measure = |ui: &Ui, sz: f32| -> f32 { + ui.painter() + .layout_no_wrap(value.to_string(), FontId::new(sz, fonts::bold()), num_ink) + .size() + .x + }; + // Goblin mark ~half the digit height, with a small gap before it. + let mark_of = |sz: f32| sz * 0.52; + let gap = size * 0.06; + let mut size = size; + let total0 = measure(ui, size) + gap + mark_of(size); + if total0 > avail && total0 > 1.0 { + size = (size * (avail / total0) * 0.97).clamp(14.0, size); + } + let mark = mark_of(size); + let total = measure(ui, size) + gap + mark; + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.add_space(((ui.available_width() - total) / 2.0 + dx).max(0.0)); + ui.label( + RichText::new(value) + .font(FontId::new(size, fonts::bold())) + .color(num_ink), + ); + ui.add_space(gap); + ui.add( + egui::Image::new(egui::include_image!("../../../../img/goblin-logo2.svg")) + .fit_to_exact_size(Vec2::splat(mark)), + ); + }); +} + /// An uppercase letterspaced kicker label. pub fn kicker(ui: &mut Ui, text: &str) { let t = theme::tokens(); diff --git a/src/http/price.rs b/src/http/price.rs index e04c8396..0ea45bf8 100644 --- a/src/http/price.rs +++ b/src/http/price.rs @@ -12,20 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! GRIN price preview, fetched over the Nym mixnet and cached per currency. +//! GRIN price preview, cached per currency. The rate is non-sensitive metadata +//! (no payment info), so — like the update check, and like upstream Grim — it is +//! fetched direct over HTTPS rather than waiting on the mixnet, keeping the +//! preview prompt. Payments, relays and identity stay mixnet-only. use lazy_static::lazy_static; use parking_lot::RwLock; use std::collections::{HashMap, HashSet}; -use std::time::{SystemTime, UNIX_EPOCH}; - -use crate::nym; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// Cache refresh interval (seconds). const REFRESH_SECS: i64 = 300; /// Minimum delay between fetch attempts for a currency, so a failing fetch -/// (e.g. the mixnet still bootstrapping) does not respawn a thread every frame. +/// (e.g. no network) does not respawn a thread every frame. const RETRY_SECS: i64 = 30; lazy_static! { @@ -59,7 +60,7 @@ pub fn grin_rate(vs: &str) -> Option { cached.map(|(rate, _)| rate) } -/// Spawn a background refresh over the mixnet for one currency (deduped per code). +/// Spawn a background refresh for one currency (deduped per code). fn trigger_refresh(vs: String) { let t = now(); { @@ -90,22 +91,26 @@ fn trigger_refresh(vs: String) { }); } -/// Fetch the GRIN/`vs` rate from CoinGecko over the Nym mixnet. +/// Fetch the GRIN/`vs` rate from CoinGecko, direct over HTTPS. async fn fetch_rate(vs: &str) -> Option { let url = format!( "https://api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies={}", vs ); // CoinGecko rejects requests without a User-Agent (403). A static, - // non-identifying UA is fine over the mixnet. - let headers = vec![("User-Agent".to_string(), "goblin-wallet".to_string())]; - let body = nym::http_request("GET", url, None, headers).await?; + // non-identifying UA. + let client = reqwest::Client::builder() + .user_agent("goblin-wallet") + .timeout(Duration::from_secs(20)) + .build() + .ok()?; + let body = client.get(&url).send().await.ok()?.text().await.ok()?; let parsed: Option = serde_json::from_str::(&body) .ok() .and_then(|doc| doc.get("grin")?.get(vs)?.as_f64()); if parsed.is_none() { log::warn!( - "price: unexpected response from rate API (mixnet exit blocked?): {}", + "price: unexpected response from rate API: {}", body.chars().take(120).collect::() ); } diff --git a/src/http/release.rs b/src/http/release.rs index 99e2363c..b32d1997 100644 --- a/src/http/release.rs +++ b/src/http/release.rs @@ -47,42 +47,43 @@ const ARM_ARCH: &str = "arm"; const ARCH: &'static str = ARM_ARCH; /// Base endpoint to download the release. -const BASE_DOWNLOAD_URL: &'static str = "https://code.gri.mw/GUI/grim/releases/download/"; +const BASE_DOWNLOAD_URL: &'static str = "https://github.com/2ro/goblin/releases/download/"; impl ReleaseInfo { - /// Get version number. + /// Release version (the build tag, e.g. "build71"). pub fn version(&self) -> String { - self.tag_name.replace("v", "") + self.tag_name.clone() } - /// Get artifact release name based on current platform. + /// Get artifact release name based on current platform. Matches the assets + /// attached to Goblin's GitHub releases; platforms Goblin doesn't ship + /// (linux-arm, macOS, windows-arm) return None. fn name(&self) -> Option { let os = OperatingSystem::from_target_os(); match os { OperatingSystem::Unknown => None, OperatingSystem::Android => { let name = if ARCH == ARM_ARCH { - format!("grim-{}-android.apk", self.tag_name) + format!("goblin-{}-android-arm.apk", self.tag_name) } else { - format!("grim-{}-android-x86_64.apk", self.tag_name) + format!("goblin-{}-android-x86_64.apk", self.tag_name) }; Some(name) } OperatingSystem::IOS => None, OperatingSystem::Nix => { - let name = if ARCH == ARM_ARCH { - format!("grim-{}-linux-arm.AppImage", self.tag_name) + if ARCH == ARM_ARCH { + None } else { - format!("grim-{}-linux-x86_64.AppImage", self.tag_name) - }; - Some(name) + Some(format!("goblin-{}-linux-x86_64.AppImage", self.tag_name)) + } } - OperatingSystem::Mac => Some(format!("grim-{}-macos-universal.zip", self.tag_name)), + OperatingSystem::Mac => None, OperatingSystem::Windows => { if ARCH == ARM_ARCH { None } else { - Some(format!("grim-{}-win-x86_64.msi", self.tag_name)) + Some(format!("goblin-{}-win-x86_64.zip", self.tag_name)) } } } @@ -119,46 +120,30 @@ impl ReleaseInfo { None } - /// Check if release is update. + /// Whether this release is newer than the running build. Goblin versions by + /// build number ("buildNN" tags) rather than semver, so compare the numbers. pub fn is_update(&self) -> bool { - let cur = crate::VERSION; - let ver = self.version(); - if cur == ver { - return false; - } - let cur_numbers: Vec = cur - .split(".") - .filter_map(|s| s.parse::().ok()) - .collect(); - let ver_numbers: Vec = ver - .split(".") - .filter_map(|s| s.parse::().ok()) - .collect(); - if cur_numbers.len() != ver_numbers.len() { - return true; - } - for (i, num) in ver_numbers.iter().enumerate() { - if num > &cur_numbers.get(i).unwrap() { - if i == 0 { - return true; - } else if i == 1 && cur_numbers.get(0).unwrap() == ver_numbers.get(0).unwrap() { - return true; - } else if i == 2 && cur_numbers.get(1).unwrap() == ver_numbers.get(1).unwrap() { - return true; - } - } - } - false + let cur: u64 = crate::BUILD.trim().parse().unwrap_or(0); + let rel: u64 = self + .tag_name + .trim() + .trim_start_matches("build") + .parse() + .unwrap_or(0); + rel > cur } } -/// API endpoint to check last release. -const REQUEST_URL: &'static str = "https://code.gri.mw/api/v1/repos/gui/grim/releases/latest"; +/// API endpoint to check last release (Goblin's own GitHub releases). +const REQUEST_URL: &'static str = "https://api.github.com/repos/2ro/goblin/releases/latest"; pub async fn retrieve_release() -> Result { let req = hyper::Request::builder() .method(hyper::Method::GET) .uri(REQUEST_URL) + // GitHub's API rejects requests without a User-Agent. + .header("User-Agent", "goblin-wallet") + .header("Accept", "application/vnd.github+json") .body(Empty::::new()) .unwrap(); if let Ok(resp) = HttpClient::send(req).await { diff --git a/src/settings/config.rs b/src/settings/config.rs index 2198a2f4..6aa08606 100644 --- a/src/settings/config.rs +++ b/src/settings/config.rs @@ -198,12 +198,11 @@ impl Default for AppConfig { use_socks_proxy: None, http_proxy_url: None, socks_proxy_url: None, - // Off by default: the legacy update check hits code.gri.mw (GRIM's - // gitea) directly over CLEARNET via the old HttpClient — it leaks - // "this user runs Goblin" metadata (defeating the nothing-clearnet - // mixnet model) and points at the wrong project's releases. Opt-in - // only until reworked to run over the mixnet against Goblin releases. - check_updates: Some(false), + // On by default, like upstream Grim: checks Goblin's own GitHub + // releases direct over HTTPS (see http/release.rs). This is the same + // non-sensitive-metadata-over-clearnet posture Grim uses for its + // update check — payments, relays and identity still stay mixnet-only. + check_updates: Some(true), app_update: None, } }