goblin: Grin-mark anon/self avatars + Export archive caption, archive under Advanced privacy
Anonymous-mode censored avatar now composites the Grin mark (the same path, 90% scale, and 67%-black ink a normal gradient avatar draws) over the flat #FED60E tile instead of the Goblin head. The user's own top-right avatar on the Home, Pay, and Activity surfaces always renders as that same flat yellow + Grin-mark tile (new avatar_self), never a picture or per-identity gradient; Activity gains the settings avatar to match Home and Pay. Export archive gains a caption describing exactly what it copies (contacts, payment history, and requests, to the clipboard as JSON), localized across all nine locales. The standalone Archive section is removed from the main settings page; Export archive and Wipe payment history now live inside Advanced privacy.
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -502,6 +502,7 @@ goblin:
|
||||
theme_yellow: "イエロー"
|
||||
archive: "アーカイブ"
|
||||
export_archive: "アーカイブをエクスポート"
|
||||
export_archive_caption: "連絡先、支払い履歴、リクエストを JSON 形式でクリップボードにコピーします。"
|
||||
wipe_history: "支払い履歴を消去"
|
||||
wipe_history_confirm: "もう一度タップして消去 — 元に戻せません"
|
||||
about: "アプリについて"
|
||||
|
||||
@@ -502,6 +502,7 @@ goblin:
|
||||
theme_yellow: "옐로우"
|
||||
archive: "보관함"
|
||||
export_archive: "보관함 내보내기"
|
||||
export_archive_caption: "연락처, 결제 내역, 요청을 JSON 형식으로 클립보드에 복사합니다."
|
||||
wipe_history: "결제 내역 지우기"
|
||||
wipe_history_confirm: "지우려면 다시 탭하세요 — 되돌릴 수 없습니다"
|
||||
about: "정보"
|
||||
|
||||
@@ -502,6 +502,7 @@ goblin:
|
||||
theme_yellow: "Жёлтая"
|
||||
archive: "Архив"
|
||||
export_archive: "Экспорт архива"
|
||||
export_archive_caption: "Копирует ваши контакты, историю платежей и запросы в буфер обмена в формате JSON."
|
||||
wipe_history: "Стереть историю платежей"
|
||||
wipe_history_confirm: "Нажмите ещё раз, чтобы стереть — это нельзя отменить"
|
||||
about: "О приложении"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -502,6 +502,7 @@ goblin:
|
||||
theme_yellow: "黄色"
|
||||
archive: "存档"
|
||||
export_archive: "导出存档"
|
||||
export_archive_caption: "将您的联系人、付款记录和请求以 JSON 格式复制到剪贴板。"
|
||||
wipe_history: "清除付款记录"
|
||||
wipe_history_confirm: "再次点按以清除 — 无法撤销"
|
||||
about: "关于"
|
||||
|
||||
@@ -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##"<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" viewBox="0 0 {size} {size}" role="img"><rect width="{size}" height="{size}" fill="#FED60E"/><g transform="translate({off:.2},{off:.2}) scale({scale:.4})"><path d="{GRIN_PATH}" fill="#000000" fill-opacity="{LOGO_OPACITY}"/></g></svg>"##
|
||||
)
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
+58
-79
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user