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:
@@ -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)
|
||||
|
||||
@@ -597,6 +597,27 @@ goblin:
|
||||
avail_invalid: "Namen haben 3–20 Zeichen: a–z, 0–9, _ 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."
|
||||
|
||||
@@ -597,6 +597,27 @@ goblin:
|
||||
avail_invalid: "Names are 3–20 chars: a–z, 0–9, _ 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."
|
||||
|
||||
@@ -597,6 +597,27 @@ goblin:
|
||||
avail_invalid: "Les noms font 3 à 20 caractères : a–z, 0–9, _ 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."
|
||||
|
||||
@@ -597,6 +597,27 @@ goblin:
|
||||
avail_invalid: "Имена 3–20 символов: a–z, 0–9, _ или -"
|
||||
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. Обычно они вам не нужны."
|
||||
|
||||
@@ -597,6 +597,27 @@ goblin:
|
||||
avail_invalid: "Adlar 3–20 karakter: a–z, 0–9, _ 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."
|
||||
|
||||
@@ -597,6 +597,27 @@ goblin:
|
||||
avail_invalid: "用户名为 3–20 个字符:a–z、0–9、_ 或 -"
|
||||
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
@@ -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, ¬e, item.npub.as_deref().unwrap_or(""))
|
||||
};
|
||||
let resp = w::activity_row(
|
||||
ui,
|
||||
&item.title,
|
||||
¬e,
|
||||
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
@@ -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 => {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user