1
0
forked from GRIN/grim

goblin: Settings redesign (Username + Advanced Privacy pages)

Two new Settings pages, each the single home for its feature.

Username (SettingsPage::Username) consolidates everything name-related
off the main Settings and out of the identity card: claim a name if you
have none, release the one you own, and choose the name authority from a
known list OR a free-typed custom server. Reuses the existing claim_ui and
claim/release worker flow; the old inline name-authority editor and the
main-settings claim card are removed (single home).

Advanced Privacy (SettingsPage::AdvancedPrivacy) replaces the lone
"hide amounts" toggle. Notifications section: hide amounts (existing),
hide names, or hide all details (a generic localized private alert that
leaks no name or amount; an empty amount collapses the Android
notification template to just the private line). Anonymous mode: one
toggle that censors STRICTLY the home balance (dots, tap to reveal; the
fiat rate fetch stays off until revealed) and the activity list (dots for
names and amounts, tap a row to reveal and open). Presentation only, with
no money-path or storage change.

Migration: the existing hide_amounts config keeps its exact meaning
(notification amount hiding); the three new fields default off, so no
surprise on upgrade. Covered by a new config test (227 lib tests).

All new strings via t!() in all six locales; drift test green.
This commit is contained in:
2ro
2026-07-06 20:06:26 -04:00
parent f7178254e0
commit 6af8fd1c26
10 changed files with 637 additions and 169 deletions
@@ -217,9 +217,13 @@ public class BackgroundService extends Service {
}
Intent i = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, i, PendingIntent.FLAG_IMMUTABLE);
// An empty amount is the "hide all details" private alert: the native
// side already composed the whole localized line into `name`, so show it
// verbatim rather than the "X paid Y ツ" scaffolding.
String receivedText = amount.isEmpty() ? name : name + " paid " + amount + "";
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, PAYMENT_CHANNEL_ID)
.setContentTitle("Payment received")
.setContentText(name + " paid " + amount + "")
.setContentText(receivedText)
.setSmallIcon(R.drawable.ic_stat_name)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
@@ -251,9 +255,11 @@ public class BackgroundService extends Service {
}
Intent i = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, i, PendingIntent.FLAG_IMMUTABLE);
// Empty amount = the "hide all details" private alert (see notifyPaymentReceived).
String requestedText = amount.isEmpty() ? name : name + " requested " + amount + "";
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, REQUEST_CHANNEL_ID)
.setContentTitle("Payment requested")
.setContentText(name + " requested " + amount + "")
.setContentText(requestedText)
.setSmallIcon(R.drawable.ic_stat_name)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
+21
View File
@@ -597,6 +597,27 @@ goblin:
avail_invalid: "Namen haben 320 Zeichen: az, 09, _ oder -"
avail_quarantined: "Nicht verfügbar"
avail_unknown: "Prüfung fehlgeschlagen — Verbindungsproblem. Versuche es erneut."
username_none: "Nicht festgelegt"
advanced_privacy: "Erweiterte Privatsphäre"
tap_reveal: "Zum Anzeigen tippen"
notif_someone: "Jemand"
notif_private_received: "Du wurdest bezahlt. Öffne Goblin, um Details zu sehen."
notif_private_requested: "Neue Zahlungsanfrage. Öffne Goblin, um Details zu sehen."
username:
title: "Benutzername"
authority: "Namensautorität"
authority_blurb: "Wo dein Benutzername registriert und überprüft wird. Wähle eine bekannte aus oder richte Goblin auf eine andere Instanz, um dort gehostete Namen zu nutzen."
custom: "Eigene Autorität"
advprivacy:
intro: "Steuere, was deine Benachrichtigungen preisgeben, und verbirg dein Guthaben und deinen Verlauf auf diesem Gerät."
notifications: "Benachrichtigungen"
hide_names: "Namen verbergen"
hide_names_sub: "Wer bezahlt hat, wird in Benachrichtigungen weggelassen"
hide_details: "Alle Details verbergen"
hide_details_sub: "Nur ein privater Hinweis, ohne Name oder Betrag"
anon: "Anonymer Modus"
anon_toggle: "Guthaben und Aktivität verwischen"
anon_sub: "Guthaben und Aktivität erscheinen als Punkte, bis du zum Anzeigen tippst"
advanced:
title: "Erweitert"
intro: "Wallet-Werkzeuge auf niedriger Ebene von GRIM. Normalerweise brauchst du diese nicht."
+21
View File
@@ -597,6 +597,27 @@ goblin:
avail_invalid: "Names are 320 chars: az, 09, _ or -"
avail_quarantined: "Not available"
avail_unknown: "Couldn't check — connection hiccup. Try again."
username_none: "Not set"
advanced_privacy: "Advanced Privacy"
tap_reveal: "Tap to reveal"
notif_someone: "Someone"
notif_private_received: "You got paid. Open Goblin to see."
notif_private_requested: "New payment request. Open Goblin to see."
username:
title: "Username"
authority: "Name authority"
authority_blurb: "Where your username is registered and verified. Pick a known one or point Goblin at another instance to use names hosted there."
custom: "Custom authority"
advprivacy:
intro: "Control what your notifications reveal, and hide your balance and history on this device."
notifications: "Notifications"
hide_names: "Hide names"
hide_names_sub: "Leave out who paid in notifications"
hide_details: "Hide all details"
hide_details_sub: "Show only a private alert, no name or amount"
anon: "Anonymous mode"
anon_toggle: "Blur balance and activity"
anon_sub: "Your balance and activity show as dots until you tap to reveal"
advanced:
title: "Advanced"
intro: "Low-level wallet tools from GRIM. You won't normally need these."
+21
View File
@@ -597,6 +597,27 @@ goblin:
avail_invalid: "Les noms font 3 à 20 caractères : az, 09, _ ou -"
avail_quarantined: "Indisponible"
avail_unknown: "Vérification impossible — souci de connexion. Réessayez."
username_none: "Non défini"
advanced_privacy: "Confidentialité avancée"
tap_reveal: "Toucher pour révéler"
notif_someone: "Quelqu'un"
notif_private_received: "Vous avez été payé. Ouvrez Goblin pour voir."
notif_private_requested: "Nouvelle demande de paiement. Ouvrez Goblin pour voir."
username:
title: "Nom d'utilisateur"
authority: "Autorité de noms"
authority_blurb: "Là où votre nom d'utilisateur est enregistré et vérifié. Choisissez-en une connue ou pointez Goblin vers une autre instance pour utiliser les noms qui y sont hébergés."
custom: "Autorité personnalisée"
advprivacy:
intro: "Contrôlez ce que révèlent vos notifications, et masquez votre solde et votre historique sur cet appareil."
notifications: "Notifications"
hide_names: "Masquer les noms"
hide_names_sub: "Ne pas indiquer qui a payé dans les notifications"
hide_details: "Masquer tous les détails"
hide_details_sub: "Afficher seulement une alerte privée, sans nom ni montant"
anon: "Mode anonyme"
anon_toggle: "Flouter le solde et l'activité"
anon_sub: "Votre solde et votre activité s'affichent en points jusqu'à ce que vous touchiez pour révéler"
advanced:
title: "Avancé"
intro: "Outils de portefeuille bas niveau de GRIM. Vous n'en aurez normalement pas besoin."
+21
View File
@@ -597,6 +597,27 @@ goblin:
avail_invalid: "Имена 3–20 символов: a–z, 09, _ или -"
avail_quarantined: "Недоступно"
avail_unknown: "Не удалось проверить — сбой соединения. Попробуйте снова."
username_none: "Не задано"
advanced_privacy: "Расширенная приватность"
tap_reveal: "Нажмите, чтобы показать"
notif_someone: "Кто-то"
notif_private_received: "Вам заплатили. Откройте Goblin, чтобы увидеть."
notif_private_requested: "Новый запрос платежа. Откройте Goblin, чтобы увидеть."
username:
title: "Имя пользователя"
authority: "Центр имён"
authority_blurb: "Где регистрируется и проверяется ваше имя. Выберите известный центр или укажите Goblin другой сервер, чтобы использовать размещённые там имена."
custom: "Свой центр"
advprivacy:
intro: "Управляйте тем, что раскрывают уведомления, и скрывайте баланс и историю на этом устройстве."
notifications: "Уведомления"
hide_names: "Скрывать имена"
hide_names_sub: "Не показывать в уведомлениях, кто заплатил"
hide_details: "Скрывать все данные"
hide_details_sub: "Показывать только приватное оповещение, без имени и суммы"
anon: "Анонимный режим"
anon_toggle: "Скрывать баланс и активность"
anon_sub: "Баланс и активность показываются точками, пока вы не нажмёте, чтобы показать"
advanced:
title: "Дополнительно"
intro: "Низкоуровневые инструменты кошелька из GRIM. Обычно они вам не нужны."
+21
View File
@@ -597,6 +597,27 @@ goblin:
avail_invalid: "Adlar 320 karakter: az, 09, _ ya da -"
avail_quarantined: "Müsait değil"
avail_unknown: "Kontrol edilemedi — bağlantı sorunu. Tekrar dene."
username_none: "Ayarlı değil"
advanced_privacy: "Gelişmiş Gizlilik"
tap_reveal: "Göstermek için dokun"
notif_someone: "Biri"
notif_private_received: "Ödeme aldın. Görmek için Goblin'i aç."
notif_private_requested: "Yeni ödeme isteği. Görmek için Goblin'i aç."
username:
title: "Kullanıcı adı"
authority: "İsim otoritesi"
authority_blurb: "Kullanıcı adının kaydedildiği ve doğrulandığı yer. Bilinen birini seç ya da orada barındırılan isimleri kullanmak için Goblin'i başka bir sunucuya yönlendir."
custom: "Özel otorite"
advprivacy:
intro: "Bildirimlerinin neyi açığa çıkardığını denetle ve bu cihazda bakiyeni ve geçmişini gizle."
notifications: "Bildirimler"
hide_names: "İsimleri gizle"
hide_names_sub: "Kimin ödediğini bildirimlerde gösterme"
hide_details: "Tüm ayrıntıları gizle"
hide_details_sub: "Yalnızca özel bir uyarı göster, isim ya da tutar olmadan"
anon: "Anonim mod"
anon_toggle: "Bakiyeyi ve etkinliği bulanıklaştır"
anon_sub: "Bakiyen ve etkinliğin, göstermek için dokunana kadar noktalar olarak görünür"
advanced:
title: "Gelişmiş"
intro: "GRIM'den düşük seviyeli cüzdan araçları. Bunlara normalde ihtiyacın olmaz."
+21
View File
@@ -597,6 +597,27 @@ goblin:
avail_invalid: "用户名为 3–20 个字符:az、09、_ 或 -"
avail_quarantined: "不可用"
avail_unknown: "无法检查 — 连接中断。请重试。"
username_none: "未设置"
advanced_privacy: "高级隐私"
tap_reveal: "点按以显示"
notif_someone: "某人"
notif_private_received: "你收到了付款。打开 Goblin 查看。"
notif_private_requested: "新的付款请求。打开 Goblin 查看。"
username:
title: "用户名"
authority: "名称机构"
authority_blurb: "你的用户名注册和验证的地方。选择一个已知机构,或将 Goblin 指向其他实例以使用其托管的名称。"
custom: "自定义机构"
advprivacy:
intro: "控制通知会透露的内容,并在此设备上隐藏你的余额和历史记录。"
notifications: "通知"
hide_names: "隐藏名称"
hide_names_sub: "通知中不显示是谁付款"
hide_details: "隐藏全部详情"
hide_details_sub: "仅显示私密提醒,不含名称或金额"
anon: "匿名模式"
anon_toggle: "模糊余额和活动"
anon_sub: "余额和活动显示为圆点,点按后才会显示"
advanced:
title: "高级"
intro: "来自 GRIM 的底层钱包工具。通常你用不到这些。"
+374 -155
View File
@@ -164,6 +164,10 @@ pub struct GoblinWalletView {
money: Option<MoneyState>,
/// The hold-to-confirm gesture for a money-tier approval.
money_hold: w::HoldToSend,
/// Anonymous mode: whether the home balance has been tapped to reveal this
/// visit. Presentation-only and transient — reset whenever the user leaves
/// the Home tab so a later glance is censored again.
balance_revealed: bool,
}
/// Whether the per-identity cue is drawn on activity rows (owner-approved). The
@@ -173,6 +177,11 @@ pub struct GoblinWalletView {
/// each payment used. Only shown when the wallet holds more than one identity.
const SHOW_ROW_IDENTITY_CUE: bool = true;
/// Known name authorities offered on the Username page as a tappable list, on top
/// of the free-typed custom entry. Kept to servers we actually run; anything else
/// goes through the custom field. `(display, base URL)`.
const KNOWN_AUTHORITIES: &[(&str, &str)] = &[("goblin.st", "https://goblin.st")];
/// Per-frame identity context for the activity rows: whether the wallet holds
/// more than one identity (the cue only shows then) and the primary identity's
/// pubkey hex (the seed for pre-feature rows that carry no owner tag). Computed
@@ -205,6 +214,12 @@ enum SettingsPage {
Language,
Slatepack,
Privacy,
/// Everything username: claim, release, and the name authority (known list
/// plus a free-typed custom one). The single home for names.
Username,
/// Notification privacy (hide amounts / names / all details) plus the
/// anonymous-mode toggle that dots the home balance and activity list.
AdvancedPrivacy,
Advanced,
/// The identity switcher: one wallet, one balance, many nostr identities.
Identities,
@@ -307,6 +322,7 @@ impl Default for GoblinWalletView {
trust_hold: w::HoldToSend::default(),
money: None,
money_hold: w::HoldToSend::default(),
balance_revealed: false,
}
}
}
@@ -1347,6 +1363,11 @@ impl GoblinWalletView {
} else {
Content::SIDE_PANEL_WIDTH * 1.2
};
// Leaving Home re-censors the balance (anonymous mode): the reveal
// is a per-visit tap, never sticky.
if self.tab != Tab::Home {
self.balance_revealed = false;
}
w::centered_column(ui, col_width, |ui| match self.tab {
Tab::Home => self.home_ui(ui, wallet, cb, wide_desktop),
Tab::Pay => self.pay_ui(ui, wallet, cb),
@@ -1826,16 +1847,25 @@ impl GoblinWalletView {
// Distinguish "still updating" from "can't reach the node" so a
// node outage never renders as a silent zero (see balance_hero).
let error = wallet.sync_error();
w::balance_hero(
ui,
total,
spendable,
updating,
error,
wallet.info_sync_progress(),
fiat_line(&data),
56.0,
);
// Anonymous mode: the balance is a row of dots until tapped. The
// fiat lookup is skipped entirely while censored (fiat_line is what
// kicks the rate fetch) and only fires once revealed.
if crate::AppConfig::anonymous_mode() && !self.balance_revealed {
if censored_balance_hero(ui) {
self.balance_revealed = true;
}
} else {
w::balance_hero(
ui,
total,
spendable,
updating,
error,
wallet.info_sync_progress(),
fiat_line(&data),
56.0,
);
}
ui.add_space(20.0);
let (send, receive) = w::send_receive(ui);
if send {
@@ -2894,15 +2924,32 @@ impl GoblinWalletView {
} else {
" "
};
let amount = format!("{}{}{}", sign, w::amount_str(item.amount), w::TSU);
// Anonymous mode dots the name and amount (and drops the avatar/memo so
// nothing leaks); the row still taps through to the full detail, which is
// the "reveal" the spec calls for.
let anon = crate::AppConfig::anonymous_mode();
let amount = if anon {
"•••".to_string()
} else {
format!("{}{}{}", sign, w::amount_str(item.amount), w::TSU)
};
let (note, time) = Self::activity_note_time(item);
let tex = self.handle_tex(ui.ctx(), wallet, &item.title);
let tex = if anon {
None
} else {
self.handle_tex(ui.ctx(), wallet, &item.title)
};
let (title, note_ref, id_ref): (&str, &str, &str) = if anon {
("••••••", "", "")
} else {
(&item.title, &note, item.npub.as_deref().unwrap_or(""))
};
let resp = w::activity_row(
ui,
&item.title,
&note,
title,
note_ref,
&time,
item.npub.as_deref().unwrap_or(""),
id_ref,
&amount,
item.incoming,
item.canceled,
@@ -2916,7 +2963,7 @@ impl GoblinWalletView {
// is 40px, flush to the row's left and vertically centred, so its
// bottom-right corner is at (left+40, mid+20); the badge overhangs that
// corner by ~4px (matching the mock's right:-4/bottom:-4, 14px badge).
if SHOW_ROW_IDENTITY_CUE && id_cue.multi && !item.system {
if !anon && SHOW_ROW_IDENTITY_CUE && id_cue.multi && !item.system {
let seed = item.owner_pubkey.clone().or_else(|| id_cue.primary.clone());
if let Some(seed) = seed {
let r = resp.rect;
@@ -3350,6 +3397,8 @@ impl GoblinWalletView {
SettingsPage::Language => return self.language_settings_ui(ui),
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::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),
@@ -3537,17 +3586,24 @@ impl GoblinWalletView {
// face of the wallet.
w::kicker(ui, &t!("goblin.settings.identity"));
ui.add_space(8.0);
if self.claim.is_none() {
self.claim = Some(ClaimState::default());
}
self.claim_ui(ui, wallet, cb);
ui.add_space(8.0);
// Hoisted above the identity card: the Nostr Relays row now lives
// inside that card (relays are a nostr concern, like the keys), but
// its open handler runs further down — so the flag is declared here.
let mut open_relays = false;
// Username has its own home (claim/release + name authority); the
// row shows the current name (or "Not set") and opens that page.
let mut open_username = false;
w::card(ui, |ui| {
if !npub.is_empty() {
let username = wallet
.nostr_service()
.and_then(|s| s.identity.read().nip05.clone())
.map(|n| n.split('@').next().unwrap_or("").to_string());
let uname_val = username
.unwrap_or_else(|| t!("goblin.settings.username_none").to_string());
if settings_row_nav(ui, &t!("goblin.settings.username"), &uname_val) {
open_username = true;
}
if settings_row_btn(ui, &t!("goblin.settings.copy_npub"), COPY) {
cb.copy_string_to_buffer(npub.clone());
cb.vibrate_copy();
@@ -3589,26 +3645,22 @@ impl GoblinWalletView {
) {
open_relays = true;
}
// Federation: which name authority (server) registers and
// verifies names. Shows the current host on the right.
let authority = wallet
.nostr_service()
.map(|s| s.config.read().home_domain())
.unwrap_or_default();
if settings_row_nav(ui, &t!("goblin.settings.name_authority"), &authority)
&& self.name_authority.is_none()
{
let cur = wallet
.nostr_service()
.map(|s| s.config.read().nip05_server())
.unwrap_or_default();
self.name_authority = Some(NameAuthorityState {
input: cur,
error: None,
});
}
}
});
if open_username {
self.claim = Some(ClaimState::default());
// Seed the free-type authority field with the current server so
// the Username page opens showing where names resolve today.
let cur = wallet
.nostr_service()
.map(|s| s.config.read().nip05_server())
.unwrap_or_default();
self.name_authority = Some(NameAuthorityState {
input: cur,
error: None,
});
self.settings_page = SettingsPage::Username;
}
// Transient confirmation that the copy landed — pairs with the
// haptic tick so the tap feels acknowledged.
if let Some(at) = self.copy_flash {
@@ -3641,10 +3693,6 @@ impl GoblinWalletView {
ui.add_space(8.0);
self.backup_ui(ui, wallet, cb);
}
if self.name_authority.is_some() {
ui.add_space(8.0);
self.name_authority_ui(ui, wallet, cb);
}
if self.rotate.is_some() {
ui.add_space(8.0);
self.rotate_ui(ui, wallet, cb);
@@ -3727,6 +3775,7 @@ impl GoblinWalletView {
ui.add_space(16.0);
let mut open_pairing = false;
let mut open_privacy = false;
let mut open_adv_privacy = false;
settings_group(ui, &t!("goblin.settings.privacy"), |ui| {
// Messages, names, price and avatars ride the mixnet; the grin
// node connects directly. Normal dim value ink: the salmon
@@ -3756,15 +3805,11 @@ impl GoblinWalletView {
) {
open_pairing = true;
}
// Hide received amounts in payment notifications/alerts. Same
// switch widget as the incoming-requests toggle below.
if let Some(v) = settings_row_toggle(
ui,
&t!("goblin.settings.hide_amounts"),
&t!("goblin.settings.hide_amounts_sub"),
crate::AppConfig::hide_amounts(),
) {
crate::AppConfig::set_hide_amounts(v);
// Notification hiding (amounts/names/details) and anonymous mode
// now live together on their own page. Replaces the lone
// hide-amounts toggle that used to sit here.
if settings_row_nav(ui, &t!("goblin.settings.advanced_privacy"), "") {
open_adv_privacy = true;
}
});
if open_pairing {
@@ -3773,6 +3818,9 @@ impl GoblinWalletView {
if open_privacy {
self.settings_page = SettingsPage::Privacy;
}
if open_adv_privacy {
self.settings_page = SettingsPage::AdvancedPrivacy;
}
ui.add_space(16.0);
settings_group(ui, &t!("goblin.settings.requests"), |ui| {
@@ -4119,6 +4167,249 @@ impl GoblinWalletView {
});
}
/// Username page — the single home for everything name-related: claim one if
/// you have none, release the one you own, and choose the name authority from
/// a known list or by free-typing a custom server. Reuses [`claim_ui`] for the
/// claim/release card; the authority controls live only here.
fn username_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
let t = theme::tokens();
if self.sub_header(ui, &t!("goblin.username.title")) {
self.settings_page = SettingsPage::Main;
return;
}
if self.claim.is_none() {
self.claim = Some(ClaimState::default());
}
if self.name_authority.is_none() {
let cur = wallet
.nostr_service()
.map(|s| s.config.read().nip05_server())
.unwrap_or_default();
self.name_authority = Some(NameAuthorityState {
input: cur,
error: None,
});
}
ScrollArea::vertical()
.id_salt("goblin_username_scroll")
.auto_shrink([false; 2])
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
// Claim / release + the owned-name display.
self.claim_ui(ui, wallet, cb);
ui.add_space(18.0);
w::kicker(ui, &t!("goblin.username.authority"));
ui.add_space(8.0);
ui.label(
RichText::new(t!("goblin.username.authority_blurb"))
.font(FontId::new(13.0, fonts::regular()))
.color(t.text_dim),
);
ui.add_space(10.0);
let cur_server = wallet
.nostr_service()
.map(|s| s.config.read().nip05_server())
.unwrap_or_default();
let norm = |u: &str| u.trim().trim_end_matches('/').to_lowercase();
// Known authorities: a tap sets the server. Free-type handles the rest.
let mut chosen: Option<String> = None;
w::card(ui, |ui| {
for (label, url) in KNOWN_AUTHORITIES {
let active = norm(&cur_server) == norm(url);
let row = ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label(
RichText::new(*label)
.font(FontId::new(15.0, fonts::medium()))
.color(t.surface_text),
);
ui.label(
RichText::new(url.replace("https://", ""))
.font(FontId::new(12.5, fonts::regular()))
.color(t.surface_text_dim),
);
});
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if active {
ui.label(
RichText::new(crate::gui::icons::CHECK)
.font(FontId::new(16.0, fonts::regular()))
.color(t.pos),
);
}
});
});
ui.add_space(10.0);
if !active && row.response.interact(Sense::click()).clicked() {
chosen = Some((*url).to_string());
}
}
});
if let Some(url) = chosen {
if let Some(s) = wallet.nostr_service() {
s.config.write().set_nip05_server(Some(url));
crate::nostr::nip05::set_home_domain(&s.config.read().home_domain());
}
if let Some(na) = self.name_authority.as_mut() {
na.input = wallet
.nostr_service()
.map(|s| s.config.read().nip05_server())
.unwrap_or_default();
na.error = None;
}
}
ui.add_space(14.0);
// Free-typed custom authority + Reset/Save.
let (save, reset, input) = {
let na = self.name_authority.as_mut().unwrap();
ui.label(
RichText::new(t!("goblin.username.custom"))
.font(FontId::new(13.0, fonts::medium()))
.color(t.text_dim),
);
ui.add_space(6.0);
w::field_well(ui, |ui| {
TextEdit::new(egui::Id::from("username_authority_input"))
.focus(false)
.hint_text("https://goblin.st")
.text_color(t.surface_text)
.body()
.ui(ui, &mut na.input, cb);
});
if let Some(err) = &na.error {
ui.add_space(6.0);
ui.label(
RichText::new(err)
.font(FontId::new(12.5, fonts::regular()))
.color(t.neg),
);
}
ui.add_space(10.0);
let mut save = false;
let mut reset = false;
ui.horizontal(|ui| {
let half = (ui.available_width() - 10.0) / 2.0;
ui.scope_builder(
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
ui.cursor().min,
Vec2::new(half, 44.0),
)),
|ui| {
if w::big_action_on_card(ui, &t!("goblin.settings.reset")).clicked()
{
reset = true;
}
},
);
ui.add_space(10.0);
ui.scope_builder(
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
ui.cursor().min,
Vec2::new(half, 44.0),
)),
|ui| {
if w::big_action(ui, &t!("goblin.settings.save"), false).clicked() {
save = true;
}
},
);
});
(save, reset, na.input.trim().to_string())
};
if reset {
if let Some(s) = wallet.nostr_service() {
s.config.write().set_nip05_server(None);
crate::nostr::nip05::set_home_domain(&s.config.read().home_domain());
}
if let Some(na) = self.name_authority.as_mut() {
na.input = wallet
.nostr_service()
.map(|s| s.config.read().nip05_server())
.unwrap_or_default();
na.error = None;
}
}
if save {
if !input.starts_with("https://") && !input.starts_with("http://") {
if let Some(na) = self.name_authority.as_mut() {
na.error =
Some(t!("goblin.settings.name_authority_invalid").to_string());
}
} else if let Some(s) = wallet.nostr_service() {
s.config.write().set_nip05_server(Some(input));
crate::nostr::nip05::set_home_domain(&s.config.read().home_domain());
if let Some(na) = self.name_authority.as_mut() {
na.error = None;
}
}
}
ui.add_space(16.0);
});
}
/// 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) {
let t = theme::tokens();
if self.sub_header(ui, &t!("goblin.settings.advanced_privacy")) {
self.settings_page = SettingsPage::Main;
return;
}
ScrollArea::vertical()
.id_salt("goblin_adv_privacy_scroll")
.auto_shrink([false; 2])
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
ui.label(
RichText::new(t!("goblin.advprivacy.intro"))
.font(FontId::new(14.0, fonts::regular()))
.color(t.text_dim),
);
ui.add_space(16.0);
settings_group(ui, &t!("goblin.advprivacy.notifications"), |ui| {
if let Some(v) = settings_row_toggle(
ui,
&t!("goblin.settings.hide_amounts"),
&t!("goblin.settings.hide_amounts_sub"),
crate::AppConfig::hide_amounts(),
) {
crate::AppConfig::set_hide_amounts(v);
}
if let Some(v) = settings_row_toggle(
ui,
&t!("goblin.advprivacy.hide_names"),
&t!("goblin.advprivacy.hide_names_sub"),
crate::AppConfig::notif_hide_names(),
) {
crate::AppConfig::set_notif_hide_names(v);
}
if let Some(v) = settings_row_toggle(
ui,
&t!("goblin.advprivacy.hide_details"),
&t!("goblin.advprivacy.hide_details_sub"),
crate::AppConfig::notif_hide_details(),
) {
crate::AppConfig::set_notif_hide_details(v);
}
});
ui.add_space(16.0);
settings_group(ui, &t!("goblin.advprivacy.anon"), |ui| {
if let Some(v) = settings_row_toggle(
ui,
&t!("goblin.advprivacy.anon_toggle"),
&t!("goblin.advprivacy.anon_sub"),
crate::AppConfig::anonymous_mode(),
) {
crate::AppConfig::set_anonymous_mode(v);
}
});
ui.add_space(16.0);
});
}
/// Advanced (wallet-recovery) page — GRIM's low-level tools surfaced in the
/// goblin style: repair, restore-from-seed, reveal the recovery phrase, and
/// delete. The two destructive actions arm a tap-twice confirm.
@@ -5229,109 +5520,6 @@ impl GoblinWalletView {
}
}
/// Inline nsec-import flow: replaces the identity with an imported key.
/// Inline "change name authority" editor: set the NIP-05 server that registers
/// and verifies names. Lets a user on one instance pay names on another.
fn name_authority_ui(
&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks,
) {
let t = theme::tokens();
let na = self.name_authority.as_mut().unwrap();
let mut close = false;
w::card(ui, |ui| {
ui.set_min_width(ui.available_width());
ui.label(
RichText::new(t!("goblin.settings.name_authority_title"))
.font(FontId::new(15.0, fonts::semibold()))
.color(t.surface_text),
);
ui.add_space(6.0);
ui.label(
RichText::new(t!("goblin.settings.name_authority_blurb"))
.font(FontId::new(13.0, fonts::regular()))
.color(t.surface_text_dim),
);
ui.add_space(10.0);
w::field_well(ui, |ui| {
TextEdit::new(egui::Id::from("name_authority_input"))
.focus(false)
.hint_text("https://goblin.st")
.text_color(t.surface_text)
.body()
.ui(ui, &mut na.input, cb);
});
if let Some(err) = &na.error {
ui.add_space(6.0);
ui.label(
RichText::new(err)
.font(FontId::new(12.5, fonts::regular()))
.color(t.neg),
);
}
ui.add_space(10.0);
ui.horizontal(|ui| {
let third = (ui.available_width() - 20.0) / 3.0;
ui.scope_builder(
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
ui.cursor().min,
Vec2::new(third, 44.0),
)),
|ui| {
if w::big_action_on_card(ui, &t!("goblin.settings.cancel")).clicked() {
close = true;
}
},
);
ui.add_space(10.0);
ui.scope_builder(
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
ui.cursor().min,
Vec2::new(third, 44.0),
)),
|ui| {
if w::big_action_on_card(ui, &t!("goblin.settings.reset")).clicked() {
if let Some(s) = wallet.nostr_service() {
s.config.write().set_nip05_server(None);
crate::nostr::nip05::set_home_domain(
&s.config.read().home_domain(),
);
}
close = true;
}
},
);
ui.add_space(10.0);
ui.scope_builder(
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
ui.cursor().min,
Vec2::new(third, 44.0),
)),
|ui| {
if w::big_action(ui, &t!("goblin.settings.save"), false).clicked() {
let url = na.input.trim().to_string();
if !url.starts_with("https://") && !url.starts_with("http://") {
na.error =
Some(t!("goblin.settings.name_authority_invalid").to_string());
} else if let Some(s) = wallet.nostr_service() {
s.config.write().set_nip05_server(Some(url));
crate::nostr::nip05::set_home_domain(
&s.config.read().home_domain(),
);
close = true;
}
}
},
);
});
});
if close {
self.name_authority = None;
}
}
/// Inline "back up identity to a file" flow: ask for the wallet password,
/// seal the identity, and write a GOBLIN-*.backup file via the native picker.
fn backup_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
@@ -8519,6 +8707,37 @@ fn fiat_line(data: &Option<WalletData>) -> Option<w::FiatLine> {
})
}
/// The anonymous-mode balance: a centered row of dots standing in for the
/// number, tappable to reveal. Returns true on the frame it is tapped. No fiat
/// line is drawn (and no rate fetch is triggered) while censored.
fn censored_balance_hero(ui: &mut egui::Ui) -> bool {
let t = theme::tokens();
let mut clicked = false;
ui.vertical_centered(|ui| {
w::kicker(ui, "Balance");
ui.add_space(6.0);
let resp = ui.add(
egui::Label::new(
RichText::new("• • • •")
.font(FontId::new(56.0, fonts::bold()))
.color(t.text),
)
.sense(Sense::click()),
);
let resp = resp
.on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text(t!("goblin.settings.tap_reveal"));
ui.add_space(4.0);
ui.label(
RichText::new(t!("goblin.settings.tap_reveal"))
.font(FontId::new(12.5, fonts::medium()))
.color(t.text_dim),
);
clicked = resp.clicked();
});
clicked
}
/// Format a value already in the pairing's unit (dollars, BTC, …) with the
/// right symbol/precision. Sats scales the BTC value by 1e8.
fn fmt_pairing(value: f64, p: crate::settings::Pairing) -> String {
+45 -12
View File
@@ -2746,16 +2746,33 @@ async fn handle_wrap(svc: &Arc<NostrService>, wallet: &Wallet, event: Event) {
// on desktop): payer's display name (or short npub) and
// the human-readable amount.
{
let name =
crate::gui::views::goblin::data::contact_title(&svc.store, &sender_hex);
// Honor the "hide amounts" setting: keep the numeric grin
// out of the received-payment alert when the user opted in.
let amount = if crate::AppConfig::hide_amounts() {
"•••".to_string()
// Notification privacy (Advanced Privacy → Notifications):
// "hide details" trumps the finer toggles with a generic
// alert that leaks neither name nor amount (empty amount
// collapses the Java template to just the private line).
if crate::AppConfig::notif_hide_details() {
crate::notify_payment_received(
&t!("goblin.settings.notif_private_received"),
"",
);
} else {
amount_to_hr_string(slate.amount, true)
};
crate::notify_payment_received(&name, &amount);
let name = if crate::AppConfig::notif_hide_names() {
t!("goblin.settings.notif_someone").to_string()
} else {
crate::gui::views::goblin::data::contact_title(
&svc.store,
&sender_hex,
)
};
// Honor the "hide amounts" setting: keep the numeric
// grin out of the alert when the user opted in.
let amount = if crate::AppConfig::hide_amounts() {
"•••".to_string()
} else {
amount_to_hr_string(slate.amount, true)
};
crate::notify_payment_received(&name, &amount);
}
}
match svc
.send_payment_dm(&sender_hex, &reply_text, None, &[])
@@ -2807,9 +2824,25 @@ async fn handle_wrap(svc: &Arc<NostrService>, wallet: &Wallet, event: Event) {
// received-payment notification's dedup. Requester's display name
// (or short npub) and the human-readable amount, with the ツ mark.
if decision == IngestDecision::SurfaceRequest {
let name = crate::gui::views::goblin::data::contact_title(&svc.store, &sender_hex);
let amount = amount_to_hr_string(slate.amount, true);
crate::notify_payment_requested(&name, &amount);
// Same notification-privacy ladder as the received-payment alert.
if crate::AppConfig::notif_hide_details() {
crate::notify_payment_requested(
&t!("goblin.settings.notif_private_requested"),
"",
);
} else {
let name = if crate::AppConfig::notif_hide_names() {
t!("goblin.settings.notif_someone").to_string()
} else {
crate::gui::views::goblin::data::contact_title(&svc.store, &sender_hex)
};
let amount = if crate::AppConfig::hide_amounts() {
"•••".to_string()
} else {
amount_to_hr_string(slate.amount, true)
};
crate::notify_payment_requested(&name, &amount);
}
}
}
IngestDecision::FinalizePost => {
+84
View File
@@ -98,6 +98,19 @@ pub struct AppConfig {
/// so existing configs (and new wallets) keep showing the amount.
hide_amounts: Option<bool>,
/// Hide the payer/requester name in payment notifications/alerts. Default
/// false. Independent of `hide_amounts`; a notification can hide either or
/// both.
notif_hide_names: Option<bool>,
/// Hide every detail in payment notifications/alerts: the alert becomes a
/// generic "you got paid / a request arrived" line with no name and no
/// amount. Default false. When on, it overrides the two finer toggles.
notif_hide_details: Option<bool>,
/// Anonymous mode: censor the wallet home balance and the activity list
/// (dots until tapped to reveal). Presentation-only, no money-path or
/// storage effect. Default false.
anonymous_mode: Option<bool>,
/// Last-known-good Nym ENTRY gateway (base58 identity). Only the gateway
/// CHOICE is remembered — the mixnet keys stay ephemeral — so a warm reconnect
/// can skip re-picking a (possibly dead) random first hop.
@@ -217,6 +230,9 @@ impl Default for AppConfig {
check_updates: Some(true),
app_update: None,
hide_amounts: None,
notif_hide_names: None,
notif_hide_details: None,
anonymous_mode: None,
nym_entry_gateway: None,
nym_last_ipr: None,
}
@@ -642,6 +658,48 @@ impl AppConfig {
w_config.hide_amounts = Some(hide);
w_config.save();
}
/// Whether the payer/requester name is hidden in payment notifications.
pub fn notif_hide_names() -> bool {
Settings::app_config_to_read()
.notif_hide_names
.unwrap_or(false)
}
/// Set whether the payer/requester name is hidden in payment notifications.
pub fn set_notif_hide_names(hide: bool) {
let mut w_config = Settings::app_config_to_update();
w_config.notif_hide_names = Some(hide);
w_config.save();
}
/// Whether payment notifications are reduced to a generic private alert.
pub fn notif_hide_details() -> bool {
Settings::app_config_to_read()
.notif_hide_details
.unwrap_or(false)
}
/// Set whether payment notifications are reduced to a generic private alert.
pub fn set_notif_hide_details(hide: bool) {
let mut w_config = Settings::app_config_to_update();
w_config.notif_hide_details = Some(hide);
w_config.save();
}
/// Whether anonymous mode censors the home balance and activity list.
pub fn anonymous_mode() -> bool {
Settings::app_config_to_read()
.anonymous_mode
.unwrap_or(false)
}
/// Set whether anonymous mode censors the home balance and activity list.
pub fn set_anonymous_mode(on: bool) {
let mut w_config = Settings::app_config_to_update();
w_config.anonymous_mode = Some(on);
w_config.save();
}
}
#[cfg(test)]
@@ -662,4 +720,30 @@ mod tests {
"old config with removed price-cache fields should still load"
);
}
/// A pre-redesign config carries `hide_amounts` but none of the new privacy
/// fields. It must load unchanged (serde fills the absent Options with None),
/// so the notification hide-amounts choice is preserved and the new controls
/// default off: no user surprise on upgrade.
#[test]
fn old_config_without_new_privacy_fields_migrates_sensibly() {
let mut cfg = AppConfig::default();
cfg.hide_amounts = Some(true);
let mut toml = toml::to_string(&cfg).expect("serialize");
// Emulate a pre-redesign file that never knew the new keys.
toml = toml
.lines()
.filter(|l| {
!l.starts_with("notif_hide_names")
&& !l.starts_with("notif_hide_details")
&& !l.starts_with("anonymous_mode")
})
.collect::<Vec<_>>()
.join("\n");
let parsed = toml::from_str::<AppConfig>(&toml).expect("legacy config should load");
assert_eq!(parsed.hide_amounts, Some(true), "existing choice preserved");
assert_eq!(parsed.notif_hide_names, None, "new control defaults off");
assert_eq!(parsed.notif_hide_details, None, "new control defaults off");
assert_eq!(parsed.anonymous_mode, None, "anonymous mode defaults off");
}
}