1
0
forked from GRIN/grim

Merge branch 'refs/heads/feat/settings-redesign' into build154

This commit is contained in:
2ro
2026-07-06 21:42:02 -04:00
11 changed files with 812 additions and 195 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)
+23
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."
@@ -812,6 +833,8 @@ goblin:
title: "Richte dein Wallet ein"
create_new: "Neu erstellen"
restore_from_seed: "Aus Seed wiederherstellen"
import_identity: "Identität importieren"
import_identity_hint: "Nach diesem Schritt bringst du deinen vorhandenen Schlüssel mit (eine .backup-Datei oder nsec)."
name_hint: "Wallet-Name"
password_hint: "Passwort"
repeat_password_hint: "Passwort wiederholen"
+23
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."
@@ -812,6 +833,8 @@ goblin:
title: "Set up your wallet"
create_new: "Create new"
restore_from_seed: "Restore from seed"
import_identity: "Import identity"
import_identity_hint: "You'll bring in your existing key (a .backup file or nsec) after this step."
name_hint: "Wallet name"
password_hint: "Password"
repeat_password_hint: "Repeat password"
+23
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."
@@ -812,6 +833,8 @@ goblin:
title: "Configurez votre portefeuille"
create_new: "Créer un nouveau"
restore_from_seed: "Restaurer depuis la phrase"
import_identity: "Importer une identité"
import_identity_hint: "Vous importerez votre clé existante (un fichier .backup ou un nsec) après cette étape."
name_hint: "Nom du portefeuille"
password_hint: "Mot de passe"
repeat_password_hint: "Répéter le mot de passe"
+23
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. Обычно они вам не нужны."
@@ -812,6 +833,8 @@ goblin:
title: "Настройте кошелёк"
create_new: "Создать новый"
restore_from_seed: "Восстановить из seed"
import_identity: "Импорт личности"
import_identity_hint: "После этого шага вы добавите свой существующий ключ (файл .backup или nsec)."
name_hint: "Имя кошелька"
password_hint: "Пароль"
repeat_password_hint: "Повторите пароль"
+23
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."
@@ -812,6 +833,8 @@ goblin:
title: "Cüzdanını kur"
create_new: "Yeni oluştur"
restore_from_seed: "Tohumdan geri yükle"
import_identity: "Kimliği içe aktar"
import_identity_hint: "Bu adımdan sonra mevcut anahtarını (bir .backup dosyası veya nsec) getireceksin."
name_hint: "Cüzdan adı"
password_hint: "Parola"
repeat_password_hint: "Parolayı tekrarla"
+23
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 的底层钱包工具。通常你用不到这些。"
@@ -812,6 +833,8 @@ goblin:
title: "设置你的钱包"
create_new: "新建"
restore_from_seed: "从助记词恢复"
import_identity: "导入身份"
import_identity_hint: "此步骤之后,你将导入现有密钥(.backup 文件或 nsec)。"
name_hint: "钱包名称"
password_hint: "密码"
repeat_password_hint: "重复密码"
+488 -168
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
@@ -192,8 +201,21 @@ impl IdentityCueCtx {
}
}
/// The settings sub-page for the coming frame: reset to the root once the user
/// is off the Settings (Me) tab, otherwise keep the current page. Pure so the
/// leave/enter boundary rule is unit-testable without an egui context. Deep
/// links open a sub-page while setting the tab to Me in the same frame, so they
/// are preserved.
fn settings_page_after(tab: Tab, page: SettingsPage) -> SettingsPage {
if tab == Tab::Me {
page
} else {
SettingsPage::Main
}
}
/// Sub-pages of the Settings tab.
#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum SettingsPage {
Main,
Node,
@@ -205,6 +227,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 +335,7 @@ impl Default for GoblinWalletView {
trust_hold: w::HoldToSend::default(),
money: None,
money_hold: w::HoldToSend::default(),
balance_revealed: false,
}
}
}
@@ -804,6 +833,13 @@ impl GoblinWalletView {
self.login_toast = None;
}
// Settings navigation resets to its ROOT whenever the user is not on the
// Settings (Me) tab, so leaving Settings and coming back always lands on
// the top-level page instead of the last sub-page. One reset at the
// leave/enter boundary — deep links that open a sub-page set the tab to Me
// in the same frame, so they are unaffected (the tab is Me by next frame).
self.settings_page = settings_page_after(self.tab, self.settings_page);
// Transient login-outcome toast (drawn as a Foreground area, so it rides
// above whatever surface or overlay is showing).
self.login_toast_ui(ui.ctx());
@@ -1347,6 +1383,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 +1867,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, total) {
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 +2944,34 @@ 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 {
// Fixed dot count, never digit-matched, so a censored row can't leak
// the amount's magnitude.
censored_amount_dots(item.amount, false)
} 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 +2985,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 +3419,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 +3608,28 @@ 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;
// Trusted Sites (the active Authorize Sessions) lives with the
// nostr rows — it is nostr-identity signing, not a wallet setting
// — but its open handler runs further down, so the flag is here.
let mut open_trusted = 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 +3671,36 @@ impl GoblinWalletView {
) {
open_relays = true;
}
// Federation: which name authority (server) registers and
// verifies names. Shows the current host on the right.
let authority = wallet
// Trusted Sites: the active Authorize Sessions, with a
// one-tap end; the row value is the live session count.
// Nostr-identity signing, so it sits with the keys/relays.
let session_count = 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,
});
.map(|s| s.session_summaries().len())
.unwrap_or(0);
if settings_row_nav(
ui,
&t!("goblin.settings.trusted_sites"),
&session_count.to_string(),
) {
open_trusted = true;
}
}
});
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 +3733,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);
@@ -3657,7 +3745,6 @@ impl GoblinWalletView {
ui.add_space(16.0);
let mut open_node = false;
let mut open_slatepack = false;
let mut open_trusted = false;
settings_group(ui, &t!("goblin.settings.wallet"), |ui| {
if settings_row_nav(ui, &t!("goblin.settings.node"), &node_summary(wallet)) {
open_node = true;
@@ -3687,19 +3774,6 @@ impl GoblinWalletView {
) {
open_slatepack = true;
}
// Trusted Sites: the active Authorize Sessions, with a one-tap
// end. Shows the live count as the row value.
let session_count = wallet
.nostr_service()
.map(|s| s.session_summaries().len())
.unwrap_or(0);
if settings_row_nav(
ui,
&t!("goblin.settings.trusted_sites"),
&session_count.to_string(),
) {
open_trusted = true;
}
});
if open_slatepack {
self.slatepack = SlatepackManual::default();
@@ -3727,6 +3801,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 +3831,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 +3844,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| {
@@ -4122,6 +4196,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.
@@ -5232,109 +5549,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) {
@@ -8203,7 +8417,13 @@ fn settings_row_toggle(ui: &mut egui::Ui, label: &str, sub: &str, on: bool) -> O
let t = theme::tokens();
let mut toggled = None;
ui.horizontal(|ui| {
// Reserve room for the switch and bound the text column, so a long
// label/subtitle WRAPS onto another line instead of running under the
// switch and clipping (longer locales clipped worst). 46px switch + gap.
let toggle_w = 58.0;
let text_w = (ui.available_width() - toggle_w).max(0.0);
ui.vertical(|ui| {
ui.set_width(text_w);
ui.label(
RichText::new(label)
.font(FontId::new(15.0, fonts::medium()))
@@ -8522,6 +8742,51 @@ fn fiat_line(data: &Option<WalletData>) -> Option<w::FiatLine> {
})
}
/// Number of dots a censored money value renders as. FIXED (never the real
/// digit count) so anonymous mode can't leak the balance magnitude.
const CENSOR_DOT_COUNT: usize = 5;
/// The anonymous-mode censor for a money value: always [`CENSOR_DOT_COUNT`]
/// dots, deliberately ignoring the real amount so its magnitude never leaks.
/// `spaced` widens the dots for the balance hero; activity amounts pass false.
fn censored_amount_dots(_atomic: u64, spaced: bool) -> String {
let sep = if spaced { " " } else { "" };
[""; CENSOR_DOT_COUNT].join(sep)
}
/// 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. `total` is
/// passed only so the censor is computed the same way everywhere; it is ignored
/// (the dot count is fixed) so the balance size never leaks.
fn censored_balance_hero(ui: &mut egui::Ui, total: u64) -> 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(censored_amount_dots(total, true))
.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 {
@@ -8595,3 +8860,58 @@ fn fit_news_title_pt(ui: &egui::Ui, text: &str, avail: f32) -> f32 {
}
FLOOR
}
#[cfg(test)]
mod anon_censor_tests {
use super::{CENSOR_DOT_COUNT, SettingsPage, Tab, censored_amount_dots, settings_page_after};
/// The censored money display must be a fixed number of dots that never
/// reflects the real amount — otherwise anonymous mode leaks the magnitude
/// (a bigger balance would show more/longer digits).
#[test]
fn censored_amount_is_fixed_width_regardless_of_size() {
for spaced in [false, true] {
let zero = censored_amount_dots(0, spaced);
let small = censored_amount_dots(1, spaced);
let huge = censored_amount_dots(u64::MAX, spaced);
assert_eq!(zero, small, "censor must not vary with amount");
assert_eq!(small, huge, "censor must not vary with amount");
assert_eq!(
zero.chars().filter(|c| *c == '•').count(),
CENSOR_DOT_COUNT,
"censor must always show exactly {CENSOR_DOT_COUNT} dots"
);
}
}
/// Leaving the Settings tab resets the sub-page to the root, so re-entering
/// always lands on the top-level Settings page. Staying on the Settings tab
/// preserves the current sub-page (so deep links into a sub-page survive).
#[test]
fn settings_nav_resets_to_root_off_settings_tab() {
// Off the Settings tab: any sub-page collapses to the root.
for tab in [Tab::Home, Tab::Pay, Tab::Activity, Tab::Receive] {
assert_eq!(
settings_page_after(tab, SettingsPage::AdvancedPrivacy),
SettingsPage::Main
);
assert_eq!(
settings_page_after(tab, SettingsPage::Username),
SettingsPage::Main
);
}
// On the Settings tab: the current sub-page is preserved.
assert_eq!(
settings_page_after(Tab::Me, SettingsPage::Username),
SettingsPage::Username
);
assert_eq!(
settings_page_after(Tab::Me, SettingsPage::AdvancedPrivacy),
SettingsPage::AdvancedPrivacy
);
assert_eq!(
settings_page_after(Tab::Me, SettingsPage::Main),
SettingsPage::Main
);
}
}
+49 -13
View File
@@ -58,6 +58,11 @@ pub struct OnboardingContent {
ext_url: String,
/// Wallet setup inputs.
restore: bool,
/// The user chose "Import identity" on the wallet step: after the wallet is
/// created the identity step opens the import panel straight away (bringing
/// over an existing key from a `.backup` or nsec), instead of the fresh
/// random key. Composes with either seed choice above it.
want_import: bool,
name: String,
pass: String,
pass2: String,
@@ -108,6 +113,7 @@ impl Default for OnboardingContent {
integrated: false,
ext_url: "https://grincoin.org".to_string(),
restore: false,
want_import: false,
name: "Main wallet".to_string(),
pass: String::new(),
pass2: String::new(),
@@ -414,6 +420,37 @@ impl OnboardingContent {
ui.add_space(10.0);
}
});
ui.add_space(10.0);
// First-class "Import identity" option, standing alongside Create/Restore:
// bring an existing key over (from a .backup file or a bare nsec). It
// composes with either seed choice above — the import panel itself opens
// on the identity step, once the wallet exists.
ui.scope_builder(
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
ui.cursor().min,
Vec2::new(ui.available_width(), 44.0),
)),
|ui| {
if w::chip(
ui,
&t!("goblin.onboarding.wallet.import_identity"),
self.want_import,
)
.clicked()
{
self.want_import = !self.want_import;
}
},
);
if self.want_import {
ui.add_space(6.0);
ui.label(
RichText::new(t!("goblin.onboarding.wallet.import_identity_hint"))
.font(FontId::new(12.5, fonts::regular()))
.color(t.text_mute),
);
}
ui.add_space(14.0);
w::field_well(ui, |ui| {
@@ -864,6 +901,13 @@ impl OnboardingContent {
.nostr_service()
.map(|s| s.identity.read().nip05.is_some())
.unwrap_or(false);
// Came in via the wallet step's "Import identity" button: open the import
// panel straight away (offers both a .backup file and a bare nsec), rather
// than the fresh-key claim card.
if self.want_import && self.import.is_none() {
self.import = Some(OnbImport::default());
self.want_import = false;
}
if self.import.is_some() {
// Returning user is swapping the random key for their existing identity.
self.import_ui(ui, &wallet, cb);
@@ -946,19 +990,11 @@ impl OnboardingContent {
}
});
ui.add_space(10.0);
// Returning user? Let them restore their existing identity (nsec or a
// .backup file) instead of claiming a fresh name on the random key.
let import_resp = ui
.add(
egui::Label::new(
RichText::new(t!("goblin.onboarding.identity.import_existing"))
.font(FontId::new(13.0, fonts::semibold()))
.color(t.accent),
)
.sense(Sense::click()),
)
.on_hover_cursor(egui::CursorIcon::PointingHand);
if import_resp.clicked() {
// Returning user? A centered, first-class "Import identity" button
// (was a left-aligned text link) restores their existing identity
// from a .backup file or a bare nsec instead of the fresh random key.
// The .backup-or-nsec choice lives behind it, in import_ui.
if w::big_action(ui, &t!("goblin.onboarding.wallet.import_identity"), true).clicked() {
self.import = Some(OnbImport::default());
}
ui.add_space(16.0);
+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");
}
}