diff --git a/locales/de.yml b/locales/de.yml index 16a28aa3..b85bd91a 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -502,6 +502,7 @@ goblin: theme_yellow: "Gelb" archive: "Archiv" export_archive: "Archiv exportieren" + export_archive_caption: "Kopiert deine Kontakte, deinen Zahlungsverlauf und Anfragen als JSON in die Zwischenablage." wipe_history: "Zahlungsverlauf löschen" wipe_history_confirm: "Zum Löschen erneut tippen — kann nicht rückgängig gemacht werden" about: "Über" diff --git a/locales/en.yml b/locales/en.yml index 070c5cfe..89d14433 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -502,6 +502,7 @@ goblin: theme_yellow: "Yellow" archive: "Archive" export_archive: "Export archive" + export_archive_caption: "Copies your contacts, payment history, and requests to the clipboard as JSON." wipe_history: "Wipe payment history" wipe_history_confirm: "Tap again to wipe — this can't be undone" about: "About" diff --git a/locales/es.yml b/locales/es.yml index 50dc5691..c8afc5f7 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -502,6 +502,7 @@ goblin: theme_yellow: "Amarillo" archive: "Archivo" export_archive: "Exportar archivo" + export_archive_caption: "Copia tus contactos, historial de pagos y solicitudes al portapapeles en formato JSON." wipe_history: "Borrar historial de pagos" wipe_history_confirm: "Toca de nuevo para borrar — esto no se puede deshacer" about: "Acerca de" diff --git a/locales/fr.yml b/locales/fr.yml index fb396baf..baf5d91b 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -502,6 +502,7 @@ goblin: theme_yellow: "Jaune" archive: "Archive" export_archive: "Exporter l'archive" + export_archive_caption: "Copie vos contacts, votre historique de paiement et vos demandes dans le presse-papiers au format JSON." wipe_history: "Effacer l'historique des paiements" wipe_history_confirm: "Appuyez à nouveau pour effacer — action irréversible" about: "À propos" diff --git a/locales/ja.yml b/locales/ja.yml index 400d90dc..141cb558 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -502,6 +502,7 @@ goblin: theme_yellow: "イエロー" archive: "アーカイブ" export_archive: "アーカイブをエクスポート" + export_archive_caption: "連絡先、支払い履歴、リクエストを JSON 形式でクリップボードにコピーします。" wipe_history: "支払い履歴を消去" wipe_history_confirm: "もう一度タップして消去 — 元に戻せません" about: "アプリについて" diff --git a/locales/ko.yml b/locales/ko.yml index c285dbcd..a211aa4f 100644 --- a/locales/ko.yml +++ b/locales/ko.yml @@ -502,6 +502,7 @@ goblin: theme_yellow: "옐로우" archive: "보관함" export_archive: "보관함 내보내기" + export_archive_caption: "연락처, 결제 내역, 요청을 JSON 형식으로 클립보드에 복사합니다." wipe_history: "결제 내역 지우기" wipe_history_confirm: "지우려면 다시 탭하세요 — 되돌릴 수 없습니다" about: "정보" diff --git a/locales/ru.yml b/locales/ru.yml index 754d44ee..acf6d72f 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -502,6 +502,7 @@ goblin: theme_yellow: "Жёлтая" archive: "Архив" export_archive: "Экспорт архива" + export_archive_caption: "Копирует ваши контакты, историю платежей и запросы в буфер обмена в формате JSON." wipe_history: "Стереть историю платежей" wipe_history_confirm: "Нажмите ещё раз, чтобы стереть — это нельзя отменить" about: "О приложении" diff --git a/locales/tr.yml b/locales/tr.yml index a85e449d..5ef523e8 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -502,6 +502,7 @@ goblin: theme_yellow: "Sarı" archive: "Arşiv" export_archive: "Arşivi dışa aktar" + export_archive_caption: "Kişilerinizi, ödeme geçmişinizi ve istekleri JSON olarak panoya kopyalar." wipe_history: "Ödeme geçmişini sil" wipe_history_confirm: "Silmek için tekrar dokun — geri alınamaz" about: "Hakkında" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index f66abb60..4e6fc190 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -502,6 +502,7 @@ goblin: theme_yellow: "黄色" archive: "存档" export_archive: "导出存档" + export_archive_caption: "将您的联系人、付款记录和请求以 JSON 格式复制到剪贴板。" wipe_history: "清除付款记录" wipe_history_confirm: "再次点按以清除 — 无法撤销" about: "关于" diff --git a/src/gui/views/goblin/identicon.rs b/src/gui/views/goblin/identicon.rs index 88646146..3b45131e 100644 --- a/src/gui/views/goblin/identicon.rs +++ b/src/gui/views/goblin/identicon.rs @@ -106,6 +106,21 @@ pub fn gradient_stops(id: &str) -> ((u8, u8, u8), (u8, u8, u8), f32) { (c1, c2, angle as f32) } +/// The anonymous-mode / self avatar as a standalone SVG: a flat Goblin-yellow +/// (`#FED60E`) tile with the SAME Grin mark the gradient avatar composites on top +/// (identical [`GRIN_PATH`], 90% scale, and 67%-black ink), just over a flat fill +/// instead of the per-identity gradient. Keeping the mark geometry identical to +/// [`gradient_avatar_svg`] means the anonymous and self tiles draw the Grin mark +/// exactly the way a normal avatar does. +pub fn censored_avatar_svg(size: u32) -> String { + let target = size as f64 * LOGO_FRAC; + let scale = target / GRIN_NATIVE; + let off = (size as f64 - target) / 2.0; + format!( + r##""## + ) +} + /// The gradient avatar as a standalone SVG document, seeded by `hex` (lowercase /// hex pubkey). `id_suffix` makes the gradient element id unique when several /// are inlined into ONE html document; for a standalone document (how egui diff --git a/src/gui/views/goblin/mod.rs b/src/gui/views/goblin/mod.rs index 3bae9c2e..fdd710e9 100644 --- a/src/gui/views/goblin/mod.rs +++ b/src/gui/views/goblin/mod.rs @@ -1728,23 +1728,6 @@ impl GoblinWalletView { // Mobile header: wordmark left, avatar (opens settings) right. if !wide { ui.add_space(10.0); - let (header_handle, header_hex) = wallet - .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 (avatar_any then draws the deterministic - // pubkey-seeded gradient). - let h = id - .nip05 - .clone() - .map(|n| n.split('@').next().unwrap_or("").to_string()) - .unwrap_or_else(|| data::short_npub(&hex)); - (h, hex) - }) - .unwrap_or_else(|| ("N".to_string(), String::new())); - let header_tex = self.handle_tex(ui.ctx(), wallet, &header_handle); ui.horizontal(|ui| { // Owner-sized: +50% over the original 24px mark so the lockup // carries the same visual weight as the 40-44px right cluster. @@ -1756,15 +1739,9 @@ impl GoblinWalletView { .color(theme::tokens().text), ); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if w::avatar_any( - ui, - &header_handle, - &header_hex, - 40.0, - header_tex.as_ref(), - ) - .clicked() - { + // The user's own avatar is always the flat yellow + Grin + // mark tile (opens settings), never a picture or gradient. + if w::avatar_self(ui, 40.0).clicked() { self.tab = Tab::Me; } // Scan-to-pay, left of the avatar. No frame: a bold white QR @@ -1991,21 +1968,6 @@ impl GoblinWalletView { fn pay_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { let t = theme::tokens(); ui.add_space(8.0); - // Header identity for the avatar (→ settings), mirroring the Home header. - let (header_handle, header_hex) = wallet - .nostr_service() - .map(|s| { - let id = s.identity.read(); - let hex = hex_of(&id.npub); - let h = id - .nip05 - .clone() - .map(|n| n.split('@').next().unwrap_or("").to_string()) - .unwrap_or_else(|| data::short_npub(&hex)); - (h, hex) - }) - .unwrap_or_else(|| ("N".to_string(), String::new())); - let header_tex = self.handle_tex(ui.ctx(), wallet, &header_handle); ui.horizontal(|ui| { // Goblin mark (left), sized to match the right-side controls. ui.add( @@ -2016,7 +1978,9 @@ impl GoblinWalletView { // Right cluster: scan QR (black, no background) then the profile // picture at the far right; all three controls about the same size. ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if w::avatar_any(ui, &header_handle, &header_hex, 40.0, header_tex.as_ref()) + // The user's own avatar is always the flat yellow + Grin mark tile + // (opens settings), never a picture or gradient. + if w::avatar_self(ui, 40.0) .on_hover_cursor(egui::CursorIcon::PointingHand) .clicked() { @@ -2790,11 +2754,20 @@ impl GoblinWalletView { fn activity_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { ui.add_space(8.0); - ui.label( - RichText::new(t!("goblin.activity.title")) - .font(FontId::new(28.0, fonts::bold())) - .color(theme::tokens().text), - ); + ui.horizontal(|ui| { + ui.label( + RichText::new(t!("goblin.activity.title")) + .font(FontId::new(28.0, fonts::bold())) + .color(theme::tokens().text), + ); + // The user's own avatar (opens settings): the flat yellow + Grin mark + // tile, matching the Home and Pay headers. + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if w::avatar_self(ui, 40.0).clicked() { + self.tab = Tab::Me; + } + }); + }); ui.add_space(12.0); // Recent contacts strip (payment-app-style row above the feed). @@ -3369,7 +3342,7 @@ impl GoblinWalletView { SettingsPage::Slatepack => return self.slatepack_ui(ui, wallet, cb), SettingsPage::Privacy => return self.privacy_ui(ui), SettingsPage::Username => return self.username_ui(ui, wallet, cb), - SettingsPage::AdvancedPrivacy => return self.advanced_privacy_ui(ui), + SettingsPage::AdvancedPrivacy => return self.advanced_privacy_ui(ui, wallet, cb), SettingsPage::Advanced => return self.advanced_ui(ui, wallet, cb), SettingsPage::Identities => return self.identities_ui(ui, wallet, cb), SettingsPage::TrustedSites => return self.trusted_sites_ui(ui, wallet, cb), @@ -3822,36 +3795,6 @@ impl GoblinWalletView { self.settings_page = SettingsPage::Language; } - ui.add_space(16.0); - w::kicker(ui, &t!("goblin.settings.archive")); - ui.add_space(8.0); - w::card(ui, |ui| { - if settings_row_btn(ui, &t!("goblin.settings.export_archive"), COPY) { - if let Some(s) = wallet.nostr_service() { - let json = s.store.export_json(&s.npub()); - cb.copy_string_to_buffer(json); - cb.vibrate_copy(); - } - } - // Destructive: danger styling + tap-twice confirm (like the - // receipt's "Cancel payment") before the archive is wiped. - let wipe_label = if self.wipe_confirm { - t!("goblin.settings.wipe_history_confirm") - } else { - t!("goblin.settings.wipe_history") - }; - if settings_row_danger(ui, &wipe_label, crate::gui::icons::X) { - if self.wipe_confirm { - if let Some(s) = wallet.nostr_service() { - s.store.wipe_archive(); - } - self.wipe_confirm = false; - } else { - self.wipe_confirm = true; - } - } - }); - ui.add_space(16.0); settings_group(ui, &t!("goblin.settings.about"), |ui| { if settings_row_nav( @@ -4321,7 +4264,12 @@ impl GoblinWalletView { /// Advanced Privacy page — notification hiding (amounts / names / all /// details) and the anonymous-mode toggle that dots the home balance and the /// activity list. All presentation-only; nothing here touches the money path. - fn advanced_privacy_ui(&mut self, ui: &mut egui::Ui) { + fn advanced_privacy_ui( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + cb: &dyn PlatformCallbacks, + ) { let t = theme::tokens(); if self.sub_header(ui, &t!("goblin.settings.advanced_privacy")) { self.settings_page = SettingsPage::Main; @@ -4376,6 +4324,37 @@ impl GoblinWalletView { } }); ui.add_space(16.0); + // The local archive (contacts + payment history + requests) lives + // here under Advanced privacy. + settings_group(ui, &t!("goblin.settings.archive"), |ui| { + if settings_row_btn(ui, &t!("goblin.settings.export_archive"), COPY) { + if let Some(s) = wallet.nostr_service() { + let json = s.store.export_json(&s.npub()); + cb.copy_string_to_buffer(json); + cb.vibrate_copy(); + } + } + advanced_desc(ui, &t!("goblin.settings.export_archive_caption")); + ui.add_space(10.0); + // Destructive: danger styling + tap-twice confirm (like the + // receipt's "Cancel payment") before the archive is wiped. + let wipe_label = if self.wipe_confirm { + t!("goblin.settings.wipe_history_confirm") + } else { + t!("goblin.settings.wipe_history") + }; + if settings_row_danger(ui, &wipe_label, crate::gui::icons::X) { + if self.wipe_confirm { + if let Some(s) = wallet.nostr_service() { + s.store.wipe_archive(); + } + self.wipe_confirm = false; + } else { + self.wipe_confirm = true; + } + } + }); + ui.add_space(16.0); }); } diff --git a/src/gui/views/goblin/widgets.rs b/src/gui/views/goblin/widgets.rs index faa555a4..c0a81fdd 100644 --- a/src/gui/views/goblin/widgets.rs +++ b/src/gui/views/goblin/widgets.rs @@ -84,39 +84,39 @@ pub fn avatar_any( } } -/// Fixed Goblin yellow for the anonymous-mode censored avatar (#FED60E). A -/// literal constant, never seeded by an identity or read from the theme, so -/// every censored tile is byte-identical and no per-user color can leak. -const CENSOR_AVATAR_FILL: Color32 = Color32::from_rgb(0xFE, 0xD6, 0x0E); - /// The anonymous-mode censored avatar: one uniform tile that replaces every -/// real picture, gradient, or initial while anonymous mode is on. A solid -/// Goblin-yellow circle with the Goblin mark inked dark on top (the same mark -/// [`super::widgets_logo`] draws), tinted with the dark ink so it reads on the -/// yellow in both light and dark themes. Identical for every identity on the +/// real picture, gradient, or initial while anonymous mode is on. A flat +/// Goblin-yellow (`#FED60E`) circle with the GRIN mark composited on top exactly +/// the way a normal gradient avatar draws it (same path, 90% scale, 67%-black +/// ink — see [`super::identicon::censored_avatar_svg`]), just over a flat fill +/// instead of the per-identity gradient. Identical for every identity on the /// home, activity, and Recent surfaces, so nothing about who the counterparty /// is leaks. `size` matches the avatar it stands in for; the row still taps /// through (the returned `Response` senses clicks) so tap-to-reveal is intact. pub fn avatar_censored(ui: &mut Ui, size: f32) -> Response { let (rect, resp) = ui.allocate_exact_size(Vec2::splat(size), Sense::click()); - ui.painter() - .circle_filled(rect.center(), rect.width() / 2.0, CENSOR_AVATAR_FILL); - // Goblin mark centered at ~62% of the tile, inked dark so it reads on the - // yellow regardless of theme. Small marks use the pre-rendered raster (same - // crossover as widgets_logo_sized) for cleaner antialiasing. - let mark = size * 0.62; - let mrect = egui::Rect::from_center_size(rect.center(), Vec2::splat(mark)); - egui::Image::new(if mark <= 32.0 { - egui::include_image!("../../../../img/goblin-logo2-48.png") - } else { - egui::include_image!("../../../../img/goblin-logo2.svg") + let px = (rect.width() * 2.0) as u32; + let svg = super::identicon::censored_avatar_svg(px); + let uri = format!("bytes://gobcensored-{}.svg", rect.width() as u32); + egui::Image::new(egui::ImageSource::Bytes { + uri: uri.into(), + bytes: svg.into_bytes().into(), }) - .tint(Color32::from_rgb(0x0E, 0x0E, 0x0C)) - .fit_to_exact_size(Vec2::splat(mark)) - .paint_at(ui, mrect); + .corner_radius(CornerRadius::same((rect.width() / 2.0) as u8)) + .fit_to_exact_size(rect.size()) + .paint_at(ui, rect); resp } +/// The user's OWN avatar as shown top-right on the front surfaces (home, pay, +/// activity). Always the flat Goblin-yellow tile with the Grin mark — never a +/// custom picture or per-identity gradient — so "you" is a stable brand mark +/// regardless of identity or anonymous mode. Same tile as [`avatar_censored`]; +/// taps through to settings. +pub fn avatar_self(ui: &mut Ui, size: f32) -> Response { + avatar_censored(ui, size) +} + /// Draw a balance/amount: big bold number + smaller ツ mark, tight. /// Geist (sans) per the design; mono is reserved for kernel/block ids. pub fn amount_text(ui: &mut Ui, value: &str, size: f32) {