1
0
forked from GRIN/grim

tor: network-privacy UI, blueviolet token, big switch + status wiring

Phase 2 slice 3 of the per-user-tor plan: the interactive UX layer on top of
slices 1-2 (wallet-level Tor setting + transport-aware relay selection).

Colors:
- Add a tor_purple token (CSS blueviolet #8A2BE2) to ThemeTokens in all three
  theme consts, exposed via Colors::tor_purple().

Shared Network-privacy component (onboarding + settings never drift):
- network_privacy_panels(): intro copy (what Goblin sends + how privacy works,
  no crypto version numbers), a Grin-node ALWAYS-DIRECT panel (grey dot, full
  width), a private-traffic panel whose header is TOR OFF (red) / TOR ON (green)
  and whose status dots are goblin-yellow when off / blueviolet when on, a large
  Tor switch (w::toggle_large: dormant gray off, blueviolet on, never yellow),
  and a VPN nudge. Repurposes the old read-only privacy_ui.

Onboarding:
- New Step::Privacy rendered before wallet creation; the create/restore triggers
  route here first. New users default OFF. The choice is stashed on
  OnboardingContent and written as the new wallet's EXPLICIT tor setting
  (set_tor_enabled) right after open() (init_nostr builds the service
  synchronously; start() runs later on the sync thread), so a new wallet writes
  an explicit value while upgraders keep None -> Tor ON.

Settings:
- Tor routing row shows On/Off and opens the Network-privacy screen; the big
  switch there flips the wallet tor setting (set_tor_enabled + set_route_over_tor)
  and restarts the service so the pool rebuilds on the new transport.

Status lines:
- Rewire the 3 Tor-only status call sites (home card, Me identity row,
  onboarding identity) to service.transport_status(), adding Connected/
  Connecting (direct) states so a clearnet wallet no longer reads as forever
  connecting over Tor. Fix the Me-row repaint loop to gate on relay-connected
  rather than the Tor-only transport_ready.
- Re-export TransportStatus from crate::nostr (slice-1 miss).

i18n: 16 new keys added to all 10 locales (English placeholder in the 9
non-English files; a following slice translates them).
This commit is contained in:
2ro
2026-07-09 20:23:29 -04:00
parent d48cad5e6a
commit 346422003f
16 changed files with 493 additions and 78 deletions
+17
View File
@@ -363,6 +363,8 @@ goblin:
connected_tor: "Über Tor verbunden"
tor_ready: "Tor bereit · Relays…"
connecting_tor: "Verbinde mit Tor…"
connected_direct: "Connected (direct)"
connecting_direct: "Connecting (direct)…"
cant_reach_node: "Node nicht erreichbar"
node_synced: "Node synchronisiert"
syncing: "Synchronisiere…"
@@ -483,6 +485,8 @@ goblin:
privacy: "Privatsphäre"
tor_routing: "Tor-Routing"
messages_lookups: "Nachrichten & Abfragen"
tor_on: "On"
tor_off: "Off"
auto_accept: "Automatisch annehmen"
pairing: "Preiswährung"
accept_anyone: "Jeder"
@@ -651,6 +655,14 @@ goblin:
direct_connection: "Direkte Verbindung"
grin_node: "Grin-Node"
grin_node_blurb: "Block-Synchronisierung und Übertragung deiner Transaktion ins Netzwerk. Dies sind öffentliche Chain-Daten, für alle gleich, und nicht mit deiner Identität verknüpft."
how_privacy: "How your privacy works"
how_privacy_blurb: "Payments and messages are sealed with authenticated encryption and delivered as private messages, so a relay can't read them or link a payment back to you."
always_direct: "Always direct"
tor_off: "Tor off"
tor_on: "Tor on"
tor_switch: "Route through Tor"
tor_switch_sub: "Send your private traffic through the Tor network to hide your IP from relays."
vpn_note: "With Tor off, relays can see your IP address. We recommend using at least a VPN."
pairing:
title: "Kopplung"
intro: "Womit dein Guthaben und deine Beträge verglichen werden."
@@ -796,6 +808,9 @@ goblin:
pass_prompt: "Dein Wallet-Passwort verschlüsselt und entsperrt jede Identität. Gib es ein, um fortzufahren."
working: "Arbeite…"
onboarding:
privacy:
kicker: "NETWORK PRIVACY"
continue: "Continue"
intro:
private_money_head: "Privates Geld"
private_money_body: "Goblin ist ein Wallet für grin — digitales Bargeld ohne Beträge oder Adressen auf seiner Chain."
@@ -857,6 +872,8 @@ goblin:
key_being_made: "Schlüssel wird erstellt…"
connected_tor: "über Tor verbunden"
connecting_tor: "verbinde über Tor…"
connected_direct: "connected (direct)"
connecting_direct: "connecting (direct)…"
fresh_key_blurb: "Ein Zahlungsschlüssel, der nicht Teil deines Seeds ist — jederzeit rotierbar, ohne deine Mittel zu berühren."
restoring: "Deine Identitäten werden wiederhergestellt…"
clean_slate_blurb: "Lust auf einen Neuanfang? Tausche jederzeit einen brandneuen Schlüssel ein — das neue Du ist nicht mit dem alten verknüpft. Gleiches Wallet, frisches Gesicht."
+18 -1
View File
@@ -363,6 +363,8 @@ goblin:
connected_tor: "Connected over Tor"
tor_ready: "Tor ready · relays…"
connecting_tor: "Connecting to Tor…"
connected_direct: "Connected (direct)"
connecting_direct: "Connecting (direct)…"
cant_reach_node: "Can't reach node"
node_synced: "Node synced"
syncing: "Syncing…"
@@ -483,6 +485,8 @@ goblin:
privacy: "Privacy"
tor_routing: "Tor routing"
messages_lookups: "Messages & lookups"
tor_on: "On"
tor_off: "Off"
auto_accept: "Auto-accept"
pairing: "Price currency"
accept_anyone: "Anyone"
@@ -640,7 +644,7 @@ goblin:
delete_final: "Delete permanently"
privacy:
title: "Network privacy"
intro: "Goblin sends its private traffic through Tor, which hides your IP from the relay — encryption hides the rest, so a relay can't link a payment back to you."
intro: "Goblin sends only a few things over the network. Here's what leaves your device, and how it stays private."
payments: "Payments"
payments_blurb: "Every nostr message carrying a slatepack."
usernames: "usernames"
@@ -651,6 +655,14 @@ goblin:
direct_connection: "Direct connection"
grin_node: "Grin node"
grin_node_blurb: "Block sync and broadcasting your transaction to the network. This is public chain data, the same for everyone, and isn't linked to your identity."
how_privacy: "How your privacy works"
how_privacy_blurb: "Payments and messages are sealed with authenticated encryption and delivered as private messages, so a relay can't read them or link a payment back to you."
always_direct: "Always direct"
tor_off: "Tor off"
tor_on: "Tor on"
tor_switch: "Route through Tor"
tor_switch_sub: "Send your private traffic through the Tor network to hide your IP from relays."
vpn_note: "With Tor off, relays can see your IP address. We recommend using at least a VPN."
pairing:
title: "Pairing"
intro: "What your balance and amounts are shown against."
@@ -796,6 +808,9 @@ goblin:
pass_prompt: "Your wallet password encrypts and unlocks each identity. Enter it to continue."
working: "Working…"
onboarding:
privacy:
kicker: "NETWORK PRIVACY"
continue: "Continue"
intro:
private_money_head: "Private money"
private_money_body: "Goblin is a wallet for grin — digital cash with no amounts or addresses on its chain."
@@ -857,6 +872,8 @@ goblin:
key_being_made: "key being made…"
connected_tor: "connected over Tor"
connecting_tor: "connecting over Tor…"
connected_direct: "connected (direct)"
connecting_direct: "connecting (direct)…"
fresh_key_blurb: "A payment key that isn't part of your seed — rotate it anytime to stay private, without touching your funds."
restoring: "Restoring your identities…"
clean_slate_blurb: "Want a clean slate? Swap in a brand-new key any time — the new you isn't linked to the old one. Same wallet, fresh face."
+17
View File
@@ -363,6 +363,8 @@ goblin:
connected_tor: "Conectado por Tor"
tor_ready: "Tor listo · relés…"
connecting_tor: "Conectando a Tor…"
connected_direct: "Connected (direct)"
connecting_direct: "Connecting (direct)…"
cant_reach_node: "No se puede conectar con el nodo"
node_synced: "Nodo sincronizado"
syncing: "Sincronizando…"
@@ -483,6 +485,8 @@ goblin:
privacy: "Privacidad"
tor_routing: "Enrutamiento por Tor"
messages_lookups: "Mensajes y consultas"
tor_on: "On"
tor_off: "Off"
auto_accept: "Aceptación automática"
pairing: "Moneda de referencia"
accept_anyone: "Cualquiera"
@@ -651,6 +655,14 @@ goblin:
direct_connection: "Conexión directa"
grin_node: "Nodo de Grin"
grin_node_blurb: "Sincronización de bloques y transmisión de tu transacción a la red. Estos son datos públicos de la cadena, iguales para todos, y no están vinculados a tu identidad."
how_privacy: "How your privacy works"
how_privacy_blurb: "Payments and messages are sealed with authenticated encryption and delivered as private messages, so a relay can't read them or link a payment back to you."
always_direct: "Always direct"
tor_off: "Tor off"
tor_on: "Tor on"
tor_switch: "Route through Tor"
tor_switch_sub: "Send your private traffic through the Tor network to hide your IP from relays."
vpn_note: "With Tor off, relays can see your IP address. We recommend using at least a VPN."
pairing:
title: "Referencia de precio"
intro: "Aquello contra lo que se muestran tu saldo y tus montos."
@@ -796,6 +808,9 @@ goblin:
pass_prompt: "Tu contraseña de billetera cifra y desbloquea cada identidad. Ingrésala para continuar."
working: "Procesando…"
onboarding:
privacy:
kicker: "NETWORK PRIVACY"
continue: "Continue"
intro:
private_money_head: "Dinero privado"
private_money_body: "Goblin es una billetera para grin — dinero digital sin montos ni direcciones en su cadena."
@@ -857,6 +872,8 @@ goblin:
key_being_made: "creando clave…"
connected_tor: "conectado por Tor"
connecting_tor: "conectando por Tor…"
connected_direct: "connected (direct)"
connecting_direct: "connecting (direct)…"
fresh_key_blurb: "Una clave de pago que no forma parte de tu frase de recuperación — rótala cuando quieras para mantener la privacidad, sin tocar tus fondos."
restoring: "Restaurando tus identidades…"
clean_slate_blurb: "¿Quieres empezar de cero? Cambia a una clave totalmente nueva cuando quieras — el nuevo tú no está vinculado al anterior. Misma billetera, cara nueva."
+17
View File
@@ -363,6 +363,8 @@ goblin:
connected_tor: "Connecté via Tor"
tor_ready: "Tor prêt · relais…"
connecting_tor: "Connexion à Tor…"
connected_direct: "Connected (direct)"
connecting_direct: "Connecting (direct)…"
cant_reach_node: "Nœud injoignable"
node_synced: "Nœud synchronisé"
syncing: "Synchronisation…"
@@ -483,6 +485,8 @@ goblin:
privacy: "Confidentialité"
tor_routing: "Routage par Tor"
messages_lookups: "Messages et recherches"
tor_on: "On"
tor_off: "Off"
auto_accept: "Acceptation auto"
pairing: "Devise des prix"
accept_anyone: "Tout le monde"
@@ -651,6 +655,14 @@ goblin:
direct_connection: "Connexion directe"
grin_node: "Nœud grin"
grin_node_blurb: "Synchronisation des blocs et diffusion de votre transaction sur le réseau. Ce sont des données de chaîne publiques, identiques pour tous, et non liées à votre identité."
how_privacy: "How your privacy works"
how_privacy_blurb: "Payments and messages are sealed with authenticated encryption and delivered as private messages, so a relay can't read them or link a payment back to you."
always_direct: "Always direct"
tor_off: "Tor off"
tor_on: "Tor on"
tor_switch: "Route through Tor"
tor_switch_sub: "Send your private traffic through the Tor network to hide your IP from relays."
vpn_note: "With Tor off, relays can see your IP address. We recommend using at least a VPN."
pairing:
title: "Appairage"
intro: "Ce à quoi votre solde et vos montants sont comparés."
@@ -796,6 +808,9 @@ goblin:
pass_prompt: "Le mot de passe de votre portefeuille chiffre et déverrouille chaque identité. Saisissez-le pour continuer."
working: "En cours…"
onboarding:
privacy:
kicker: "NETWORK PRIVACY"
continue: "Continue"
intro:
private_money_head: "Argent privé"
private_money_body: "Goblin est un portefeuille pour grin — de l'argent numérique sans montants ni adresses sur sa chaîne."
@@ -857,6 +872,8 @@ goblin:
key_being_made: "clé en cours de création…"
connected_tor: "connecté via Tor"
connecting_tor: "connexion via Tor…"
connected_direct: "connected (direct)"
connecting_direct: "connecting (direct)…"
fresh_key_blurb: "Une clé de paiement qui ne fait pas partie de votre seed — renouvelable à tout moment, sans toucher à vos fonds."
restoring: "Restauration de vos identités…"
clean_slate_blurb: "Envie de repartir à zéro ? Remplacez par une toute nouvelle clé à tout moment — le nouveau vous n'est pas lié à l'ancien. Même portefeuille, nouveau visage."
+17
View File
@@ -363,6 +363,8 @@ goblin:
connected_tor: "Tor経由で接続済み"
tor_ready: "Tor準備完了 · リレー…"
connecting_tor: "Torに接続中…"
connected_direct: "Connected (direct)"
connecting_direct: "Connecting (direct)…"
cant_reach_node: "ノードに接続できません"
node_synced: "ノード同期済み"
syncing: "同期中…"
@@ -483,6 +485,8 @@ goblin:
privacy: "プライバシー"
tor_routing: "Torルーティング"
messages_lookups: "メッセージと検索"
tor_on: "On"
tor_off: "Off"
auto_accept: "自動承認"
pairing: "価格通貨"
accept_anyone: "誰でも"
@@ -651,6 +655,14 @@ goblin:
direct_connection: "直接接続"
grin_node: "Grinノード"
grin_node_blurb: "ブロックの同期と、取引をネットワークにブロードキャストする処理です。これは誰にとっても同じ公開されたチェーンデータであり、あなたのアイデンティティとは結び付いていません。"
how_privacy: "How your privacy works"
how_privacy_blurb: "Payments and messages are sealed with authenticated encryption and delivered as private messages, so a relay can't read them or link a payment back to you."
always_direct: "Always direct"
tor_off: "Tor off"
tor_on: "Tor on"
tor_switch: "Route through Tor"
tor_switch_sub: "Send your private traffic through the Tor network to hide your IP from relays."
vpn_note: "With Tor off, relays can see your IP address. We recommend using at least a VPN."
pairing:
title: "価格通貨"
intro: "残高や金額を表示する際の基準となる通貨です。"
@@ -796,6 +808,9 @@ goblin:
pass_prompt: "ウォレットのパスワードで各アイデンティティが暗号化・解除されます。続けるには入力してください。"
working: "処理中…"
onboarding:
privacy:
kicker: "NETWORK PRIVACY"
continue: "Continue"
intro:
private_money_head: "プライベートなお金"
private_money_body: "Goblinはgrin用のウォレットです — チェーン上に金額もアドレスも残らないデジタルキャッシュです。"
@@ -858,6 +873,8 @@ goblin:
key_being_made: "キーを作成中…"
connected_tor: "Tor経由で接続済み"
connecting_tor: "Tor経由で接続中…"
connected_direct: "connected (direct)"
connecting_direct: "connecting (direct)…"
fresh_key_blurb: "リカバリーフレーズの一部ではない支払い用のキーです — 資金に影響を与えることなく、いつでもローテーションしてプライバシーを保てます。"
restoring: "アイデンティティを復元しています…"
clean_slate_blurb: "まっさらな状態にしたいですか? いつでもまったく新しいキーに切り替えられます — 新しいあなたは古いあなたと結び付きません。同じウォレットで、新しい顔になります。"
+17
View File
@@ -363,6 +363,8 @@ goblin:
connected_tor: "Tor로 연결됨"
tor_ready: "Tor 준비 완료 · 릴레이…"
connecting_tor: "Tor에 연결하는 중…"
connected_direct: "Connected (direct)"
connecting_direct: "Connecting (direct)…"
cant_reach_node: "노드에 연결할 수 없음"
node_synced: "노드 동기화됨"
syncing: "동기화하는 중…"
@@ -483,6 +485,8 @@ goblin:
privacy: "프라이버시"
tor_routing: "Tor 라우팅"
messages_lookups: "메시지 및 조회"
tor_on: "On"
tor_off: "Off"
auto_accept: "자동 수락"
pairing: "기준 통화"
accept_anyone: "누구나"
@@ -651,6 +655,14 @@ goblin:
direct_connection: "직접 연결"
grin_node: "Grin 노드"
grin_node_blurb: "블록 동기화와 거래를 네트워크에 전파하는 과정입니다. 이는 모두에게 동일한 공개 체인 데이터이며 신원과 연결되지 않습니다."
how_privacy: "How your privacy works"
how_privacy_blurb: "Payments and messages are sealed with authenticated encryption and delivered as private messages, so a relay can't read them or link a payment back to you."
always_direct: "Always direct"
tor_off: "Tor off"
tor_on: "Tor on"
tor_switch: "Route through Tor"
tor_switch_sub: "Send your private traffic through the Tor network to hide your IP from relays."
vpn_note: "With Tor off, relays can see your IP address. We recommend using at least a VPN."
pairing:
title: "기준 통화"
intro: "내 잔액과 금액이 어떤 통화로 표시되는지입니다."
@@ -796,6 +808,9 @@ goblin:
pass_prompt: "지갑 비밀번호로 각 신원을 암호화하고 잠금 해제합니다. 계속하려면 입력하세요."
working: "처리하는 중…"
onboarding:
privacy:
kicker: "NETWORK PRIVACY"
continue: "Continue"
intro:
private_money_head: "프라이빗한 돈"
private_money_body: "Goblin은 grin을 위한 지갑입니다 — 체인에 금액도 주소도 남지 않는 디지털 현금입니다."
@@ -857,6 +872,8 @@ goblin:
key_being_made: "키를 만드는 중…"
connected_tor: "Tor로 연결됨"
connecting_tor: "Tor로 연결하는 중…"
connected_direct: "connected (direct)"
connecting_direct: "connecting (direct)…"
fresh_key_blurb: "복구 문구에 속하지 않는 결제 키입니다 — 자금은 그대로 둔 채 언제든 교체해 프라이버시를 지키세요."
restoring: "신원을 복원하는 중…"
clean_slate_blurb: "새 출발을 원하시나요? 언제든 완전히 새로운 키로 바꾸세요 — 새로운 나는 이전과 연결되지 않습니다. 같은 지갑, 새로운 얼굴입니다."
+17
View File
@@ -363,6 +363,8 @@ goblin:
connected_tor: "Подключено через Tor"
tor_ready: "Tor готов · реле…"
connecting_tor: "Подключение к Tor…"
connected_direct: "Connected (direct)"
connecting_direct: "Connecting (direct)…"
cant_reach_node: "Нет связи с узлом"
node_synced: "Узел синхронизирован"
syncing: "Синхронизация…"
@@ -483,6 +485,8 @@ goblin:
privacy: "Приватность"
tor_routing: "Маршрутизация через Tor"
messages_lookups: "Сообщения и поиск"
tor_on: "On"
tor_off: "Off"
auto_accept: "Автоприём"
pairing: "Валюта цены"
accept_anyone: "Любой"
@@ -651,6 +655,14 @@ goblin:
direct_connection: "Прямое соединение"
grin_node: "Узел Grin"
grin_node_blurb: "Синхронизация блоков и трансляция транзакции в сеть. Это публичные данные цепочки, одинаковые для всех, и они не связаны с вашей личностью."
how_privacy: "How your privacy works"
how_privacy_blurb: "Payments and messages are sealed with authenticated encryption and delivered as private messages, so a relay can't read them or link a payment back to you."
always_direct: "Always direct"
tor_off: "Tor off"
tor_on: "Tor on"
tor_switch: "Route through Tor"
tor_switch_sub: "Send your private traffic through the Tor network to hide your IP from relays."
vpn_note: "With Tor off, relays can see your IP address. We recommend using at least a VPN."
pairing:
title: "Привязка"
intro: "К чему привязаны отображаемые баланс и суммы."
@@ -796,6 +808,9 @@ goblin:
pass_prompt: "Пароль кошелька шифрует и разблокирует каждую личность. Введите его, чтобы продолжить."
working: "Обработка…"
onboarding:
privacy:
kicker: "NETWORK PRIVACY"
continue: "Continue"
intro:
private_money_head: "Приватные деньги"
private_money_body: "Goblin — кошелёк для grin: цифровая наличность без сумм и адресов в её цепочке."
@@ -857,6 +872,8 @@ goblin:
key_being_made: "ключ создаётся…"
connected_tor: "подключено через Tor"
connecting_tor: "подключение через Tor…"
connected_direct: "connected (direct)"
connecting_direct: "connecting (direct)…"
fresh_key_blurb: "Платёжный ключ, не связанный с seed-фразой — меняйте его в любой момент, не трогая средства."
restoring: "Восстановление ваших личностей…"
clean_slate_blurb: "Хотите начать с чистого листа? Подставьте совершенно новый ключ в любой момент — новый вы не связан со старым. Тот же кошелёк, новое лицо."
+17
View File
@@ -363,6 +363,8 @@ goblin:
connected_tor: "Tor üzerinden bağlı"
tor_ready: "Tor hazır · relaylar…"
connecting_tor: "Tor'a bağlanılıyor…"
connected_direct: "Connected (direct)"
connecting_direct: "Connecting (direct)…"
cant_reach_node: "Düğüme ulaşılamıyor"
node_synced: "Düğüm eşitlendi"
syncing: "Eşitleniyor…"
@@ -483,6 +485,8 @@ goblin:
privacy: "Gizlilik"
tor_routing: "Tor yönlendirme"
messages_lookups: "Mesajlar ve aramalar"
tor_on: "On"
tor_off: "Off"
auto_accept: "Otomatik kabul"
pairing: "Fiyat para birimi"
accept_anyone: "Herkes"
@@ -651,6 +655,14 @@ goblin:
direct_connection: "Doğrudan bağlantı"
grin_node: "Grin düğümü"
grin_node_blurb: "Blok eşitleme ve işlemini ağa yayma. Bu, herkes için aynı olan genel zincir verisidir ve kimliğinle ilişkilendirilmez."
how_privacy: "How your privacy works"
how_privacy_blurb: "Payments and messages are sealed with authenticated encryption and delivered as private messages, so a relay can't read them or link a payment back to you."
always_direct: "Always direct"
tor_off: "Tor off"
tor_on: "Tor on"
tor_switch: "Route through Tor"
tor_switch_sub: "Send your private traffic through the Tor network to hide your IP from relays."
vpn_note: "With Tor off, relays can see your IP address. We recommend using at least a VPN."
pairing:
title: "Eşleştirme"
intro: "Bakiyenin ve tutarların neye göre gösterildiği."
@@ -796,6 +808,9 @@ goblin:
pass_prompt: "Cüzdan parolan her kimliği şifreler ve açar. Devam etmek için gir."
working: "Çalışıyor…"
onboarding:
privacy:
kicker: "NETWORK PRIVACY"
continue: "Continue"
intro:
private_money_head: "Özel para"
private_money_body: "Goblin, grin için bir cüzdan — zincirinde tutar ya da adres bulunmayan dijital nakit."
@@ -857,6 +872,8 @@ goblin:
key_being_made: "anahtar oluşturuluyor…"
connected_tor: "Tor üzerinden bağlı"
connecting_tor: "Tor üzerinden bağlanılıyor…"
connected_direct: "connected (direct)"
connecting_direct: "connecting (direct)…"
fresh_key_blurb: "Seed'inin parçası olmayan bir ödeme anahtarı — paranı hiç ellemeden istediğin an döndür."
restoring: "Kimliklerin geri yükleniyor…"
clean_slate_blurb: "Temiz bir sayfa mı istiyorsun? İstediğin zaman yepyeni bir anahtar tak — yeni sen eskisine bağlı değil. Aynı cüzdan, yeni yüz."
+17
View File
@@ -363,6 +363,8 @@ goblin:
connected_tor: "已通过 Tor 连接"
tor_ready: "Tor 就绪 · 连接中继…"
connecting_tor: "正在连接 Tor…"
connected_direct: "Connected (direct)"
connecting_direct: "Connecting (direct)…"
cant_reach_node: "无法连接节点"
node_synced: "节点已同步"
syncing: "同步中…"
@@ -483,6 +485,8 @@ goblin:
privacy: "隐私"
tor_routing: "Tor 路由"
messages_lookups: "消息和查询"
tor_on: "On"
tor_off: "Off"
auto_accept: "自动接受"
pairing: "价格货币"
accept_anyone: "任何人"
@@ -651,6 +655,14 @@ goblin:
direct_connection: "直接连接"
grin_node: "Grin 节点"
grin_node_blurb: "区块同步及向网络广播你的交易。这是公开的链上数据,对所有人都一样,且不与你的身份关联。"
how_privacy: "How your privacy works"
how_privacy_blurb: "Payments and messages are sealed with authenticated encryption and delivered as private messages, so a relay can't read them or link a payment back to you."
always_direct: "Always direct"
tor_off: "Tor off"
tor_on: "Tor on"
tor_switch: "Route through Tor"
tor_switch_sub: "Send your private traffic through the Tor network to hide your IP from relays."
vpn_note: "With Tor off, relays can see your IP address. We recommend using at least a VPN."
pairing:
title: "配对"
intro: "你的余额和金额以何种货币显示。"
@@ -796,6 +808,9 @@ goblin:
pass_prompt: "钱包密码用于加密和解锁每个身份。输入以继续。"
working: "处理中…"
onboarding:
privacy:
kicker: "NETWORK PRIVACY"
continue: "Continue"
intro:
private_money_head: "私密货币"
private_money_body: "Goblin 是一个 grin 钱包 — 链上无金额、无地址的数字现金。"
@@ -857,6 +872,8 @@ goblin:
key_being_made: "正在生成密钥…"
connected_tor: "已通过 Tor 连接"
connecting_tor: "正在通过 Tor 连接…"
connected_direct: "connected (direct)"
connecting_direct: "connecting (direct)…"
fresh_key_blurb: "一个不属于助记词的支付密钥——可随时轮换以保护隐私,且不影响你的资金。"
restoring: "正在恢复你的身份…"
clean_slate_blurb: "想要全新开始?随时换上一个全新密钥 — 新的你与旧的毫无关联。同一个钱包,焕然一新。"
+17
View File
@@ -363,6 +363,8 @@ goblin:
connected_tor: "已透過 Tor 連線"
tor_ready: "Tor 就緒 · 連線中繼…"
connecting_tor: "正在連線 Tor…"
connected_direct: "Connected (direct)"
connecting_direct: "Connecting (direct)…"
cant_reach_node: "無法連線節點"
node_synced: "節點已同步"
syncing: "同步中…"
@@ -483,6 +485,8 @@ goblin:
privacy: "隱私"
tor_routing: "Tor 路由"
messages_lookups: "訊息和查詢"
tor_on: "On"
tor_off: "Off"
auto_accept: "自動接受"
pairing: "價格貨幣"
accept_anyone: "任何人"
@@ -651,6 +655,14 @@ goblin:
direct_connection: "直接連線"
grin_node: "Grin 節點"
grin_node_blurb: "區塊同步及向網路廣播你的交易。這是公開的鏈上資料,對所有人都一樣,且不與你的身份關聯。"
how_privacy: "How your privacy works"
how_privacy_blurb: "Payments and messages are sealed with authenticated encryption and delivered as private messages, so a relay can't read them or link a payment back to you."
always_direct: "Always direct"
tor_off: "Tor off"
tor_on: "Tor on"
tor_switch: "Route through Tor"
tor_switch_sub: "Send your private traffic through the Tor network to hide your IP from relays."
vpn_note: "With Tor off, relays can see your IP address. We recommend using at least a VPN."
pairing:
title: "配對"
intro: "你的餘額和金額以何種貨幣顯示。"
@@ -796,6 +808,9 @@ goblin:
pass_prompt: "錢包密碼用於加密和解鎖每個身份。輸入以繼續。"
working: "處理中…"
onboarding:
privacy:
kicker: "NETWORK PRIVACY"
continue: "Continue"
intro:
private_money_head: "私密貨幣"
private_money_body: "Goblin 是一個 grin 錢包 — 鏈上無金額、無地址的數字現金。"
@@ -857,6 +872,8 @@ goblin:
key_being_made: "正在生成金鑰…"
connected_tor: "已透過 Tor 連線"
connecting_tor: "正在透過 Tor 連線…"
connected_direct: "connected (direct)"
connecting_direct: "connecting (direct)…"
fresh_key_blurb: "一個不屬於助記詞的支付金鑰——可隨時輪換以保護隱私,且不影響你的資金。"
restoring: "正在恢復你的身份…"
clean_slate_blurb: "想要全新開始?隨時換上一個全新金鑰 — 新的你與舊的毫無關聯。同一個錢包,煥然一新。"
+5
View File
@@ -81,6 +81,11 @@ impl Colors {
theme::tokens().neg
}
/// Blueviolet ("tor purple") accent for the Tor-on privacy state.
pub fn tor_purple() -> Color32 {
theme::tokens().tor_purple
}
pub fn blue() -> Color32 {
if dark_base() {
Color32::from_rgb(0x7B, 0xA7, 0xFF)
+7
View File
@@ -68,6 +68,10 @@ pub struct ThemeTokens {
pub accent_ink: Color32,
pub pos: Color32,
pub neg: Color32,
/// Blueviolet ("tor purple") accent for the Tor-on privacy state (CSS
/// `blueviolet` = #8A2BE2). Carries the "private traffic on" color on the
/// network-privacy status dots and the active big Tor switch.
pub tor_purple: Color32,
pub chip: Color32,
pub hover: Color32,
/// Avatar background palette (initial ink picked by luminance).
@@ -130,6 +134,7 @@ pub const LIGHT: ThemeTokens = ThemeTokens {
accent_ink: Color32::from_rgb(0x0E, 0x0E, 0x0C),
pos: Color32::from_rgb(0x0E, 0x7C, 0x3A),
neg: Color32::from_rgb(0xB0, 0x48, 0x1E),
tor_purple: Color32::from_rgb(0x8A, 0x2B, 0xE2),
chip: Color32::from_rgb(0xF2, 0xF1, 0xEC),
hover: Color32::from_rgb(0xE9, 0xE7, 0xE0),
avatar_pairs: AVATAR_PAIRS,
@@ -153,6 +158,7 @@ pub const DARK: ThemeTokens = ThemeTokens {
accent_ink: Color32::from_rgb(0x0E, 0x0E, 0x0C),
pos: Color32::from_rgb(0x5B, 0xD2, 0x7A),
neg: Color32::from_rgb(0xFF, 0x8B, 0x5E),
tor_purple: Color32::from_rgb(0x8A, 0x2B, 0xE2),
chip: Color32::from_rgb(0x24, 0x24, 0x20),
hover: Color32::from_rgb(0x2E, 0x2E, 0x29),
avatar_pairs: AVATAR_PAIRS,
@@ -178,6 +184,7 @@ pub const YELLOW: ThemeTokens = ThemeTokens {
accent_ink: Color32::from_rgb(0xFF, 0xD6, 0x0A),
pos: Color32::from_rgb(0x0E, 0x7C, 0x3A),
neg: Color32::from_rgb(0x9E, 0x2E, 0x0E),
tor_purple: Color32::from_rgb(0x8A, 0x2B, 0xE2),
chip: Color32::from_rgba_premultiplied(2, 2, 2, 20),
hover: Color32::from_rgb(0xEF, 0xC8, 0x00),
avatar_pairs: AVATAR_PAIRS,
+201 -66
View File
@@ -1596,15 +1596,18 @@ impl GoblinWalletView {
.color(t.surface_text),
);
ui.label(
// Relay-gated: "Connected over Tor" only once a
// relay is live on the current tunnel generation.
RichText::new(if crate::tor::transport_ready() {
t!("goblin.home.connected_tor")
} else if crate::tor::is_ready() {
t!("goblin.home.tor_ready")
} else {
t!("goblin.home.connecting_tor")
})
// Transport-aware: Tor states are relay-gated,
// while a clearnet wallet reads "Connected
// (direct)" instead of forever "connecting over
// Tor" (which never flips off-Tor).
RichText::new(transport_status_label(
wallet
.nostr_service()
.map(|s| s.transport_status())
.unwrap_or(
crate::nostr::TransportStatus::ConnectingTor,
),
))
.font(FontId::new(11.0, fonts::regular()))
.color(t.surface_text_mute),
);
@@ -3358,7 +3361,7 @@ impl GoblinWalletView {
SettingsPage::Pairing => return self.pairing_settings_ui(ui),
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::Privacy => return self.privacy_ui(ui, wallet),
SettingsPage::Username => return self.username_ui(ui, wallet, cb),
SettingsPage::AdvancedPrivacy => return self.advanced_privacy_ui(ui, wallet, cb),
SettingsPage::Advanced => return self.advanced_ui(ui, wallet, cb),
@@ -3445,17 +3448,16 @@ impl GoblinWalletView {
}
});
// Transport status in place of the redundant second npub line.
// "Connected over Tor" is RELAY-GATED (transport_ready): the
// tunnel being warm is not enough — a relay must actually carry
// our traffic on the current exit. Otherwise show the tunnel is
// up but relays are still connecting/reconnecting.
let transport = if crate::tor::transport_ready() {
t!("goblin.home.connected_tor")
} else if crate::tor::is_ready() {
t!("goblin.home.tor_ready")
} else {
t!("goblin.home.connecting_tor")
};
// Transport-aware: Tor states are relay-gated (a warm tunnel is
// not enough — a relay must carry our traffic), while a clearnet
// wallet reads "Connected (direct)" rather than forever
// "connecting over Tor".
let transport = transport_status_label(
wallet
.nostr_service()
.map(|s| s.transport_status())
.unwrap_or(crate::nostr::TransportStatus::ConnectingTor),
);
ui.label(
RichText::new(transport)
.font(FontId::new(13.0, fonts::regular()))
@@ -3472,7 +3474,10 @@ impl GoblinWalletView {
.font(FontId::new(12.0, fonts::regular()))
.color(t.surface_text_mute),
);
if !crate::tor::transport_ready() || !connected {
// Keep repainting until a relay is live. Gating on the Tor-only
// transport_ready would spin forever on a clearnet wallet, so
// use the transport-neutral relay-connected flag.
if !connected {
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(600));
}
@@ -3701,14 +3706,21 @@ impl GoblinWalletView {
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 Tor; the grin
// node connects directly. Normal dim value ink: the salmon
// privacy color doubled as the destructive-action color on
// this page, making a plain navigable row read as a warning.
// Value shows the wallet's current Tor state (On/Off) in the
// same dim font; the row opens the Network-privacy screen where
// the big switch flips it.
let tor_on = wallet
.nostr_service()
.map(|s| s.tor_routing())
.unwrap_or(true);
if settings_row_nav(
ui,
&t!("goblin.settings.tor_routing"),
&t!("goblin.settings.messages_lookups"),
&if tor_on {
t!("goblin.settings.tor_on")
} else {
t!("goblin.settings.tor_off")
},
) {
open_privacy = true;
}
@@ -4009,56 +4021,39 @@ impl GoblinWalletView {
});
}
/// Network-privacy breakdown: what rides Tor versus what connects
/// directly. Honest by design — no claim that node traffic is mixed, and no
/// toggle to route it (chain sync is heavy and not tied to your identity).
fn privacy_ui(&mut self, ui: &mut egui::Ui) {
let t = theme::tokens();
/// Network-privacy screen: the interactive Tor toggle shared with onboarding.
/// The Grin node is always direct; the private traffic (payments, name
/// lookups, price) rides Tor when the switch is on, else connects directly.
/// Flipping the switch writes the wallet Tor setting and restarts the service
/// so the relay pool is rebuilt on the newly-selected transport.
fn privacy_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
if self.sub_header(ui, &t!("goblin.privacy.title")) {
self.settings_page = SettingsPage::Main;
return;
}
let tor_on = wallet
.nostr_service()
.map(|s| s.tor_routing())
.unwrap_or(true);
let mut toggled = None;
ScrollArea::vertical()
.id_salt("goblin_privacy_scroll")
.auto_shrink([false; 2])
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
ui.label(
RichText::new(t!("goblin.privacy.intro"))
.font(FontId::new(14.0, fonts::regular()))
.color(t.text_dim),
);
ui.add_space(16.0);
let transport = [
(
t!("goblin.privacy.payments"),
t!("goblin.privacy.payments_blurb"),
),
(
t!("goblin.privacy.usernames"),
t!("goblin.privacy.usernames_blurb"),
),
(
t!("goblin.privacy.price_avatars"),
t!("goblin.privacy.price_avatars_blurb"),
),
];
settings_group(ui, &t!("goblin.privacy.over_tor"), |ui| {
for (title, blurb) in &transport {
privacy_line(ui, t.neg, title, blurb);
}
});
ui.add_space(16.0);
settings_group(ui, &t!("goblin.privacy.direct_connection"), |ui| {
privacy_line(
ui,
t.surface_text_mute,
&t!("goblin.privacy.grin_node"),
&t!("goblin.privacy.grin_node_blurb"),
);
});
toggled = network_privacy_panels(ui, tor_on);
ui.add_space(16.0);
});
if let Some(new_val) = toggled {
if let Some(s) = wallet.nostr_service() {
// Persist the explicit choice (auto-saves nostr.toml) and mirror
// the process-global route flag so free-function HTTP callers pick
// the matching transport immediately, then rebuild the pool.
s.config.write().set_tor_enabled(new_val);
crate::tor::set_route_over_tor(new_val);
s.restart(wallet.clone());
}
}
}
/// Username page — the single home for everything name-related: claim one if
@@ -8150,6 +8145,146 @@ fn privacy_line(ui: &mut egui::Ui, dot: Color32, title: &str, blurb: &str) {
ui.add_space(10.0);
}
/// Like [`settings_group`] but with a colored header kicker — used by the
/// Network-privacy private-traffic panel, whose header is red ("TOR OFF") or
/// green ("TOR ON") to signal the current state.
fn settings_group_colored(
ui: &mut egui::Ui,
title: &str,
color: Color32,
add: impl FnOnce(&mut egui::Ui),
) {
ui.label(
RichText::new(title.to_uppercase())
.font(fonts::kicker())
.color(color),
);
ui.add_space(8.0);
w::card(ui, |ui| add(ui));
}
/// The shared Network-privacy body, rendered by BOTH the onboarding privacy
/// step and the Settings privacy screen so the two never drift. Draws the intro
/// copy (what Goblin sends + how privacy works — deliberately no crypto version
/// numbers), the always-direct Grin-node panel, the private-traffic panel whose
/// header/dots reflect `tor_on`, the large Tor switch, and the VPN nudge.
///
/// Returns `Some(new_value)` on the frame the switch is toggled; the caller
/// persists it (Settings writes the wallet Tor setting + restarts the service;
/// onboarding stashes it and writes it before the service starts).
fn network_privacy_panels(ui: &mut egui::Ui, tor_on: bool) -> Option<bool> {
let t = theme::tokens();
ui.label(
RichText::new(t!("goblin.privacy.intro"))
.font(FontId::new(14.0, fonts::regular()))
.color(t.text_dim),
);
ui.add_space(14.0);
ui.label(
RichText::new(t!("goblin.privacy.how_privacy"))
.font(FontId::new(14.0, fonts::semibold()))
.color(t.text),
);
ui.add_space(4.0);
ui.label(
RichText::new(t!("goblin.privacy.how_privacy_blurb"))
.font(FontId::new(13.0, fonts::regular()))
.color(t.text_dim),
);
ui.add_space(16.0);
// Grin node — ALWAYS DIRECT, grey dot, full width. Public chain data, the
// same for everyone, never routed over Tor.
settings_group(ui, &t!("goblin.privacy.always_direct"), |ui| {
ui.set_min_width(ui.available_width());
privacy_line(
ui,
t.surface_text_mute,
&t!("goblin.privacy.grin_node"),
&t!("goblin.privacy.grin_node_blurb"),
);
});
ui.add_space(16.0);
// Private traffic — header "TOR OFF" (red) / "TOR ON" (green); status dots
// goblin-yellow when off (clearnet), blueviolet when on.
let (header, header_color) = if tor_on {
(t!("goblin.privacy.tor_on"), t.pos)
} else {
(t!("goblin.privacy.tor_off"), t.neg)
};
let dot = if tor_on { t.tor_purple } else { t.accent };
let transport = [
(
t!("goblin.privacy.payments"),
t!("goblin.privacy.payments_blurb"),
),
(
t!("goblin.privacy.usernames"),
t!("goblin.privacy.usernames_blurb"),
),
(
t!("goblin.privacy.price_avatars"),
t!("goblin.privacy.price_avatars_blurb"),
),
];
settings_group_colored(ui, &header, header_color, |ui| {
ui.set_min_width(ui.available_width());
for (title, blurb) in &transport {
privacy_line(ui, dot, title, blurb);
}
});
ui.add_space(18.0);
// Large Tor switch — dormant gray when off, blueviolet when on (never
// yellow). The color state lives on the dots above; this reads on/off.
let mut toggled = None;
w::card(ui, |ui| {
ui.set_min_width(ui.available_width());
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label(
RichText::new(t!("goblin.privacy.tor_switch"))
.font(FontId::new(15.0, fonts::semibold()))
.color(t.surface_text),
);
ui.add_space(2.0);
ui.label(
RichText::new(t!("goblin.privacy.tor_switch_sub"))
.font(FontId::new(12.5, fonts::regular()))
.color(t.surface_text_dim),
);
});
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if w::toggle_large(ui, tor_on).clicked() {
toggled = Some(!tor_on);
}
});
});
});
ui.add_space(12.0);
ui.label(
RichText::new(t!("goblin.privacy.vpn_note"))
.font(FontId::new(12.5, fonts::regular()))
.color(t.text_mute),
);
toggled
}
/// Localized transport-status label for the identity status lines. Tor and
/// clearnet are distinct states so a Tor-off wallet reads "Connected (direct)"
/// instead of forever "connecting over Tor".
fn transport_status_label(status: crate::nostr::TransportStatus) -> std::borrow::Cow<'static, str> {
use crate::nostr::TransportStatus::*;
match status {
ConnectedTor => t!("goblin.home.connected_tor"),
TorReady => t!("goblin.home.tor_ready"),
ConnectingTor => t!("goblin.home.connecting_tor"),
ConnectedDirect => t!("goblin.home.connected_direct"),
ConnectingDirect => t!("goblin.home.connecting_direct"),
}
}
/// Open a URL in the system browser.
fn open_url(ui: &egui::Ui, url: &str) {
ui.ctx().open_url(egui::OpenUrl::new_tab(url));
+82 -10
View File
@@ -47,6 +47,10 @@ enum Step {
WalletSetup,
Words,
ConfirmWords,
/// Network-privacy choice (Tor on/off), shown right before the wallet is
/// created so the new wallet writes an explicit Tor setting before its nostr
/// service starts.
Privacy,
Identity,
}
@@ -95,6 +99,10 @@ pub struct OnboardingContent {
restore_result: std::sync::Arc<std::sync::Mutex<Option<Result<(), String>>>>,
/// Sticky error from the identity restore, shown on the identity step.
restore_error: Option<String>,
/// Tor choice from the Network-privacy step, applied as the new wallet's
/// explicit Tor setting before its nostr service starts. New users default
/// OFF (owner spec): fast onboarding, one tap to turn on, VPN nudge shown.
tor_choice: bool,
}
/// Full-backup restore sub-state on the restore-from-seed words step.
@@ -157,6 +165,8 @@ impl Default for OnboardingContent {
restore_busy: false,
restore_result: std::sync::Arc::new(std::sync::Mutex::new(None)),
restore_error: None,
// New users default to Tor OFF (owner spec).
tor_choice: false,
}
}
}
@@ -196,6 +206,7 @@ impl OnboardingContent {
Step::WalletSetup => self.wallet_setup_ui(ui, cb),
Step::Words => self.words_ui(ui, wallets, cb),
Step::ConfirmWords => self.confirm_ui(ui, wallets, cb),
Step::Privacy => self.privacy_step_ui(ui, wallets),
Step::Identity => done = self.identity_ui(ui, cb),
}
ui.add_space(View::get_bottom_inset() + 24.0);
@@ -557,7 +568,9 @@ impl OnboardingContent {
fn words_ui(
&mut self,
ui: &mut egui::Ui,
wallets: &mut WalletList,
// Wallet creation now happens on the later Privacy step, so this path no
// longer needs the list; kept for signature parity with the step router.
_wallets: &mut WalletList,
cb: &dyn PlatformCallbacks,
) {
let t = theme::tokens();
@@ -673,7 +686,8 @@ impl OnboardingContent {
if ready {
if w::big_action(ui, &label, false).clicked() {
if restore {
self.create_wallet(wallets);
// Network-privacy choice comes before the wallet is created.
self.step = Step::Privacy;
} else {
self.step = Step::ConfirmWords;
}
@@ -796,7 +810,9 @@ impl OnboardingContent {
fn confirm_ui(
&mut self,
ui: &mut egui::Ui,
wallets: &mut WalletList,
// Wallet creation now happens on the later Privacy step, so this path no
// longer needs the list; kept for signature parity with the step router.
_wallets: &mut WalletList,
cb: &dyn PlatformCallbacks,
) {
let t = theme::tokens();
@@ -821,7 +837,8 @@ impl OnboardingContent {
ui.add_space(14.0);
if !self.mnemonic_setup.mnemonic.has_empty_or_invalid() {
if w::big_action(ui, &t!("goblin.onboarding.confirm.create_wallet"), false).clicked() {
self.create_wallet(wallets);
// Network-privacy choice comes before the wallet is created.
self.step = Step::Privacy;
}
} else {
ui.label(
@@ -844,6 +861,34 @@ impl OnboardingContent {
}
}
// ── Network privacy (Tor on/off, before wallet creation) ─────────────
/// Network-privacy step: choose Tor on/off before the wallet is created,
/// using the same shared panels as the Settings privacy screen. Continue
/// creates the wallet, which writes the choice as its explicit Tor setting.
fn privacy_step_ui(&mut self, ui: &mut egui::Ui, wallets: &mut WalletList) {
// Back returns to whichever step led here (restore vs create).
let back = if self.restore {
Step::Words
} else {
Step::ConfirmWords
};
self.step_header(
ui,
&t!("goblin.onboarding.privacy.kicker"),
&t!("goblin.privacy.title"),
back,
);
if let Some(new_val) = super::network_privacy_panels(ui, self.tor_choice) {
self.tor_choice = new_val;
}
ui.add_space(18.0);
if w::big_action(ui, &t!("goblin.onboarding.privacy.continue"), false).clicked() {
self.create_wallet(wallets);
}
self.error_ui(ui);
}
/// Resolve the connection method, create the wallet, open it and move
/// to the identity step.
fn create_wallet(&mut self, wallets: &mut WalletList) {
@@ -882,6 +927,17 @@ impl OnboardingContent {
wallets.add(w.clone());
match w.open(pass) {
Ok(_) => {
// Write this NEW wallet's EXPLICIT Tor choice (from the
// privacy step) before its nostr service starts. init_nostr
// built the service synchronously inside open(); start() runs
// later on the sync thread, so setting it now writes nostr.toml
// and picks the transport before the pool builds. (Cohort rule:
// only new-wallet onboarding writes an explicit value;
// upgraders keep None -> Tor ON.)
if let Some(svc) = w.nostr_service() {
svc.config.write().set_tor_enabled(self.tor_choice);
crate::tor::set_route_over_tor(self.tor_choice);
}
// A full-backup restore: reinstate every identity from the
// backup in a worker once nostr is up (the seed itself was
// already restored through this creation path).
@@ -995,12 +1051,28 @@ impl OnboardingContent {
);
}
ui.label(
// Relay-gated readiness: "connected over Tor" only once a
// relay is actually live, not merely when the tunnel is warm.
RichText::new(if crate::tor::transport_ready() {
t!("goblin.onboarding.identity.connected_tor")
} else {
t!("goblin.onboarding.identity.connecting_tor")
// Transport-aware readiness: Tor states are relay-gated (a
// warm tunnel is not enough), while a clearnet wallet reads
// "connected (direct)" instead of forever "connecting over
// Tor".
RichText::new({
use crate::nostr::TransportStatus::*;
let status = service
.as_ref()
.map(|s| s.transport_status())
.unwrap_or(ConnectingTor);
match status {
ConnectedTor => t!("goblin.onboarding.identity.connected_tor"),
TorReady | ConnectingTor => {
t!("goblin.onboarding.identity.connecting_tor")
}
ConnectedDirect => {
t!("goblin.onboarding.identity.connected_direct")
}
ConnectingDirect => {
t!("goblin.onboarding.identity.connecting_direct")
}
}
})
.font(FontId::new(12.0, fonts::regular()))
.color(t.surface_text_mute),
+26
View File
@@ -238,6 +238,32 @@ pub fn toggle(ui: &mut Ui, on: bool) -> Response {
resp.on_hover_cursor(egui::CursorIcon::PointingHand)
}
/// A large on/off switch for the Network-privacy Tor toggle. Unlike [`toggle`]
/// (brand yellow when on), this is dormant GRAY when OFF and blueviolet ("tor
/// purple") when ON — never yellow — so the privacy state reads distinctly from
/// the app accent. Returns the response; the caller flips the bound state.
pub fn toggle_large(ui: &mut Ui, on: bool) -> Response {
let t = theme::tokens();
let (rect, resp) = ui.allocate_exact_size(Vec2::new(64.0, 36.0), Sense::click());
let track = if on { t.tor_purple } else { t.surface2 };
ui.painter()
.rect_filled(rect, CornerRadius::same(18), track);
let knob_r = 15.0;
let knob_x = if on {
rect.right() - knob_r - 3.0
} else {
rect.left() + knob_r + 3.0
};
let knob = if on {
Color32::WHITE
} else {
t.surface_text_mute
};
ui.painter()
.circle_filled(egui::pos2(knob_x, rect.center().y), knob_r, knob);
resp.on_hover_cursor(egui::CursorIcon::PointingHand)
}
/// A segmented control (e.g. `["Scan", "My Code"]`). Highlights `selected`;
/// returns `Some(i)` when a different segment is tapped.
pub fn segmented(ui: &mut Ui, labels: &[&str], selected: usize) -> Option<usize> {
+1 -1
View File
@@ -45,7 +45,7 @@ pub mod ingest;
pub use ingest::*;
mod client;
pub use client::{HeldIdentityKeys, NostrProfile, NostrService, send_phase};
pub use client::{HeldIdentityKeys, NostrProfile, NostrService, TransportStatus, send_phase};
pub mod avatar;
pub mod nip05;