Merge branch 'refs/heads/feat/settings-redesign' into build154
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."
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
@@ -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"
|
||||
|
||||
@@ -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. Обычно они вам не нужны."
|
||||
@@ -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: "Повторите пароль"
|
||||
|
||||
@@ -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."
|
||||
@@ -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"
|
||||
|
||||
@@ -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 的底层钱包工具。通常你用不到这些。"
|
||||
@@ -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
@@ -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, ¬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 +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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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