1
0
forked from GRIN/grim

ui: profile/contacts, requests spinner, receipt fixes, backup file, send + advanced

- Republish kind 0 right after claiming a username (was invisible until restart).
- Request card shows a 'Paying…' spinner instead of a dead greyed button.
- Receipt: count confirmations 1/10…10/10 (was stuck at 0/10, jumped to done
  at one block); hide the network-fee row on received payments.
- Settings: one 'Back up to a file' flow (GOBLIN-*.backup) replacing copy-nsec
  / copy-JSON; import accepts a .backup file via the native picker.
- Advanced: 'Run your own node' opens the node-connection page (incl. an
  integrated-node option); Repair confirms in accent; Restore warns in red.
- Send: drop the 1/10/100/Max chips; Note becomes an Add-note editor.
- Remove the dead profile-picture upload UI and scrub picture wording.
- Localize all new strings across 6 locales; drift test green.
This commit is contained in:
2ro
2026-06-16 00:32:02 -04:00
parent 313a14b82c
commit 9dba2163fa
9 changed files with 564 additions and 279 deletions
+26 -13
View File
@@ -406,6 +406,7 @@ goblin:
pending: "Ausstehend"
confs: "%{c}/%{r} Bestätigungen"
waiting_to_confirm: "Warte auf Bestätigung"
paying: "Zahlung läuft…"
you: "Du"
to: "An"
from: "Von"
@@ -448,16 +449,11 @@ goblin:
title: "Einstellungen"
connected_nostr: "Mit nostr verbunden"
connecting_relays: "Verbinde mit Relays…"
pic_updated: "Profilbild aktualisiert"
uploading_pic: "Lade Bild hoch…"
claim_first: "Erst einen Benutzernamen sichern — Bilder hängen daran"
identity: "Identität"
copy_npub: "npub kopieren (öffentlich)"
backup_nsec: "Geheimen Schlüssel sichern (nsec)"
export_identity: "Identitäts-Backup exportieren (verschlüsselt)"
rotate_key: "nostr-Schlüssel wechseln"
import_identity: "Identität importieren (nsec / Backup)"
backup_note: "Gerätewechsel? Sichere BEIDES: deine Seed-Phrase (Guthaben) und ein Identitäts-Backup (Name + Schlüssel)."
import_identity: "Identität importieren (.backup / nsec)"
backup_note: "Gerät wechseln? Sichere BEIDES: deine Seed-Phrase (Guthaben) und deine Identitäts-.backup-Datei (Name + Schlüssel)."
wallet: "Wallet"
display_unit: "Anzeigeeinheit"
relays: "Relays"
@@ -512,7 +508,7 @@ goblin:
sp_copy: "Slatepack kopieren"
rotate_line1: "• Du bekommst einen brandneuen ZUFÄLLIGEN Schlüssel; das alte npub empfängt nichts mehr. Es gibt keine Ableitungskette zwischen beiden."
rotate_line2: "• Der neue Schlüssel ist NICHT aus deinem Seed wiederherstellbar — sichere die neue nsec direkt nach dem Wechsel."
rotate_line3: "• Dein username wird FREIGEGEBEN und dein Profilbild gelöscht — sichere danach denselben oder einen neuen Namen (sobald frei, kann ihn jeder andere ebenfalls greifen)."
rotate_line3: "• Dein Benutzername wird FREIGEGEBEN — beanspruche direkt danach denselben oder einen neuen Namen (sobald frei, kann ihn auch jede andere Person nehmen)."
rotate_line4: "• Zahlungen, die noch an den alten Schlüssel unterwegs sind, WERDEN gestört — warte zuerst, bis ausstehende Zahlungen abgeschlossen sind."
rotate_line5: "• Kontakte, die dein npub direkt gespeichert haben, müssen dich neu finden — teile dein neues npub oder den neu gesicherten username."
cancel: "Abbrechen"
@@ -531,18 +527,27 @@ goblin:
rotation_failed: "Wechsel fehlgeschlagen"
close: "Schließen"
import_identity_title: "Identität importieren"
import_blurb: "Ersetzt die nostr-Identität dieses Walletsfüge eine reine nsec oder ein exportiertes Identitäts-Backup ein (das Backup stellt auch deinen Benutzernamen und Verlauf wieder her). Sichere zuerst den aktuellen Schlüssel, falls du ihn noch brauchst."
import_nsec_hint: "nsec1… oder Identitäts-Backup-JSON"
import_blurb: "Ersetzt die nostr-Identität dieser Wallet — wähle eine GOBLIN-.backup-Datei oder füge einen nsec ein. Eine Sicherung stellt auch Benutzername und Verlauf wieder her. Sichere zuerst den aktuellen Schlüssel, falls du ihn noch brauchst."
import_nsec_hint: "nsec1… oder eingefügte Sicherung"
backup_password_hint: "Backup-Passwort (nur wenn anderswo exportiert)"
import_btn: "Importieren"
importing: "Importiere…"
identity_replaced: "Identität ersetzt"
now_using: "Jetzt aktiv: %{npub}"
import_failed: "Import fehlgeschlagen"
backup_file: "In Datei sichern"
choose_backup_file: "Eine .backup-Datei wählen"
backup_read_failed: "Datei konnte nicht gelesen werden."
backup_saved: "Sicherung gespeichert"
backup_saved_sub: "Bewahre die .backup-Datei sicher auf — wer sie UND dein Passwort hat, kann deine Identität wiederherstellen."
backup_file_title: "Identität sichern"
backup_file_blurb: "Erstellt eine verschlüsselte .backup-Datei mit Benutzername und Schlüssel. Gib dein Wallet-Passwort ein, um sie zu versiegeln."
backup_write_failed: "Datei konnte nicht gespeichert werden."
create_backup: "Sicherung erstellen"
registered: "%{name} registriert"
released_msg: "Freigegeben — der Name ist frei"
release_confirm: "%{name} freigeben?"
release_blurb: "Er ist frei, sobald er verfügbar wird — jeder kann ihn beanspruchen, auch der nächste Schlüssel, zu dem du wechselst. Dein Profilbild wird damit gelöscht. Du kannst 10 Minuten lang keinen anderen Benutzernamen registrieren."
release_blurb: "Sobald er frei ist, ist er verfügbar — jeder kann ihn beanspruchen, auch dein nächster rotierter Schlüssel. Du kannst 10 Minuten lang keinen anderen Benutzernamen registrieren."
releasing: "Gebe frei…"
keep_it: "Behalten"
release_it: "Freigeben"
@@ -583,6 +588,10 @@ goblin:
delete: "Wallet löschen"
delete_desc: "Diese Wallet dauerhaft von diesem Gerät entfernen. Ohne deinen Seed sind Guthaben nicht wiederherstellbar."
delete_confirm: "Zum Löschen erneut tippen"
manage_node: "Node-Verbindung verwalten"
repair_confirm: "Ja, jetzt reparieren"
repair_confirm_note: "Die Reparatur scannt die Chain neu und kann einige Minuten dauern."
restore_confirm_note: "Dies löscht lokale Daten und baut sie aus deinem Seed neu auf — das kann einige Minuten dauern."
privacy:
title: "Netzwerk-Privatsphäre"
intro: "Goblin sendet seinen privaten Datenverkehr durch das Nym mixnet — ein Netzwerk mit fünf Sprüngen, das verbirgt, wer mit wem kommuniziert, sodass ein Relay eine Zahlung nicht zu dir zurückverfolgen kann."
@@ -590,8 +599,8 @@ goblin:
payments_blurb: "Jede nostr-Nachricht, die einen slatepack trägt."
usernames: "usernames"
usernames_blurb: "NIP-05-Namensabfragen zu und von goblin.st."
price_avatars: "Kurs & Avatare"
price_avatars_blurb: "Die Kursvorschau und Kontaktbilder."
price_avatars: "Preis"
price_avatars_blurb: "Der Live-Wechselkurs neben den Beträgen."
over_mixnet: "Über das mixnet"
direct_connection: "Direkte Verbindung"
grin_node: "Grin-Node"
@@ -740,6 +749,10 @@ goblin:
max: "Max"
note_label: "Notiz"
note_hint: "Notiz hinzufügen…"
add_note: "Notiz hinzufügen"
edit_note: "Notiz bearbeiten"
note_cancel: "Abbrechen"
note_save: "Speichern"
review_btn: "Prüfen"
confirm_request: "Anfrage bestätigen"
review_title: "Prüfen"
+26 -13
View File
@@ -406,6 +406,7 @@ goblin:
pending: "Pending"
confs: "%{c}/%{r} confirmations"
waiting_to_confirm: "Waiting to confirm"
paying: "Paying…"
you: "You"
to: "To"
from: "From"
@@ -448,16 +449,11 @@ goblin:
title: "Settings"
connected_nostr: "Connected to nostr"
connecting_relays: "Connecting to relays…"
pic_updated: "Profile picture updated"
uploading_pic: "Uploading picture…"
claim_first: "Claim a username first — pictures ride on it"
identity: "Identity"
copy_npub: "Copy npub (public)"
backup_nsec: "Back up secret key (nsec)"
export_identity: "Export identity backup (encrypted)"
rotate_key: "Rotate nostr key"
import_identity: "Import identity (nsec / backup)"
backup_note: "Moving devices? Back up BOTH: your seed phrase (funds) and an identity backup (name + key)."
import_identity: "Import identity (.backup / nsec)"
backup_note: "Moving devices? Back up BOTH: your seed phrase (funds) and your identity .backup file (name + key)."
wallet: "Wallet"
display_unit: "Display unit"
relays: "Relays"
@@ -512,7 +508,7 @@ goblin:
sp_copy: "Copy slatepack"
rotate_line1: "• You get a brand-new RANDOM key; the old npub stops receiving. There is no derivation chain between them."
rotate_line2: "• The new key is NOT recoverable from your seed — back up the new nsec right after rotating."
rotate_line3: "• Your username is RELEASED and your profile picture deleted — claim the same or a new name right after (anyone else can grab it too once it's free)."
rotate_line3: "• Your username is RELEASED — claim the same or a new name right after (anyone else can grab it too once it's free)."
rotate_line4: "• Payments still in flight to the old key WILL be disrupted — wait for pending payments to finish first."
rotate_line5: "• Contacts who saved your npub directly must re-find you — share your new npub or re-claimed username."
cancel: "Cancel"
@@ -531,18 +527,27 @@ goblin:
rotation_failed: "Rotation failed"
close: "Close"
import_identity_title: "Import identity"
import_blurb: "Replaces this wallet's nostr identity — paste a bare nsec or an exported identity backup (the backup also restores your username and history). Back up the current key first if you still need it."
import_nsec_hint: "nsec1… or identity backup JSON"
import_blurb: "Replaces this wallet's nostr identity — choose a GOBLIN .backup file, or paste a bare nsec. A backup also restores your username and history. Back up the current key first if you still need it."
import_nsec_hint: "nsec1… or pasted backup"
backup_password_hint: "Backup password (only if exported elsewhere)"
import_btn: "Import"
importing: "Importing…"
identity_replaced: "Identity replaced"
now_using: "Now using: %{npub}"
import_failed: "Import failed"
backup_file: "Back up to a file"
choose_backup_file: "Choose a .backup file"
backup_read_failed: "Couldn't read that file."
backup_saved: "Backup saved"
backup_saved_sub: "Keep the .backup file safe — anyone with it AND your password can restore your identity."
backup_file_title: "Back up identity"
backup_file_blurb: "Creates one encrypted .backup file with your username and key. Enter your wallet password to seal it."
backup_write_failed: "Couldn't save the file."
create_backup: "Create backup"
registered: "Registered %{name}"
released_msg: "Released — the name is up for grabs"
release_confirm: "Release %{name}?"
release_blurb: "It's up for grabs the moment it's free — anyone can claim it, including the next key you rotate to. Your profile picture is deleted with it. You won't be able to register another username for 10 minutes."
release_blurb: "It's up for grabs the moment it's free — anyone can claim it, including the next key you rotate to. You won't be able to register another username for 10 minutes."
releasing: "Releasing…"
keep_it: "Keep it"
release_it: "Release it"
@@ -583,6 +588,10 @@ goblin:
delete: "Delete wallet"
delete_desc: "Permanently remove this wallet from this device. Without your seed, funds can't be recovered."
delete_confirm: "Tap again to delete"
manage_node: "Manage node connection"
repair_confirm: "Yes, repair now"
repair_confirm_note: "Repair re-scans the chain and can take a few minutes."
restore_confirm_note: "This erases local data and rebuilds it from your seed — it can take several minutes."
privacy:
title: "Network privacy"
intro: "Goblin sends its private traffic through the Nym mixnet — a five-hop network that hides who is talking to whom, so a relay can't link a payment back to you."
@@ -590,8 +599,8 @@ goblin:
payments_blurb: "Every nostr message carrying a slatepack."
usernames: "usernames"
usernames_blurb: "NIP-05 name lookups to and from goblin.st."
price_avatars: "Price & avatars"
price_avatars_blurb: "The rate preview and contact pictures."
price_avatars: "Price"
price_avatars_blurb: "The live fiat rate shown next to amounts."
over_mixnet: "Over the mixnet"
direct_connection: "Direct connection"
grin_node: "Grin node"
@@ -740,6 +749,10 @@ goblin:
max: "Max"
note_label: "Note"
note_hint: "Add a note…"
add_note: "Add a note"
edit_note: "Edit note"
note_cancel: "Cancel"
note_save: "Save"
review_btn: "Review"
confirm_request: "Confirm request"
review_title: "Review"
+26 -13
View File
@@ -406,6 +406,7 @@ goblin:
pending: "En attente"
confs: "%{c}/%{r} confirmations"
waiting_to_confirm: "En attente de confirmation"
paying: "Paiement…"
you: "Vous"
to: "À"
from: "De"
@@ -448,16 +449,11 @@ goblin:
title: "Réglages"
connected_nostr: "Connecté à nostr"
connecting_relays: "Connexion aux relais…"
pic_updated: "Photo de profil mise à jour"
uploading_pic: "Envoi de la photo…"
claim_first: "Réservez d'abord un nom d'utilisateur — la photo en dépend"
identity: "Identité"
copy_npub: "Copier le npub (public)"
backup_nsec: "Sauvegarder la clé secrète (nsec)"
export_identity: "Exporter la sauvegarde d'identité (chiffrée)"
rotate_key: "Renouveler la clé nostr"
import_identity: "Importer une identité (nsec / sauvegarde)"
backup_note: "Changement d'appareil ? Sauvegardez LES DEUX : votre phrase de récupération (fonds) et une sauvegarde d'identité (nom + clé)."
import_identity: "Importer l'identité (.backup / nsec)"
backup_note: "Changement d'appareil ? Sauvegardez les DEUX : votre phrase seed (fonds) et votre fichier .backup d'identité (nom + clé)."
wallet: "Portefeuille"
display_unit: "Unité d'affichage"
relays: "Relais"
@@ -512,7 +508,7 @@ goblin:
sp_copy: "Copier le slatepack"
rotate_line1: "• Vous obtenez une toute nouvelle clé ALÉATOIRE ; l'ancien npub cesse de recevoir. Il n'y a aucune chaîne de dérivation entre eux."
rotate_line2: "• La nouvelle clé n'est PAS récupérable depuis votre phrase de récupération — sauvegardez le nouveau nsec juste après le renouvellement."
rotate_line3: "• Votre username est LIBÉRÉ et votre photo de profil supprimée — réservez le même nom ou un nouveau juste après (n'importe qui peut le prendre une fois libre)."
rotate_line3: "• Votre nom d'utilisateur est LIBÉRÉ — réclamez le même ou un nouveau juste après (une fois libre, n'importe qui peut le prendre)."
rotate_line4: "• Les paiements encore en cours vers l'ancienne clé SERONT interrompus — attendez d'abord la fin des paiements en attente."
rotate_line5: "• Les contacts qui ont enregistré votre npub directement doivent vous retrouver — partagez votre nouveau npub ou votre username re-réservé."
cancel: "Annuler"
@@ -531,18 +527,27 @@ goblin:
rotation_failed: "Échec du renouvellement"
close: "Fermer"
import_identity_title: "Importer une identité"
import_blurb: "Remplace l'identité nostr de ce portefeuille — collez un nsec brut ou une sauvegarde d'identité exportée (la sauvegarde restaure aussi votre nom d'utilisateur et votre historique). Sauvegardez d'abord la clé actuelle si vous en avez encore besoin."
import_nsec_hint: "nsec1… ou JSON de sauvegarde d'identité"
import_blurb: "Remplace l'identité nostr de ce portefeuille — choisissez un fichier .backup GOBLIN, ou collez un nsec. Une sauvegarde restaure aussi votre nom d'utilisateur et votre historique. Sauvegardez d'abord la clé actuelle si besoin."
import_nsec_hint: "nsec1… ou sauvegarde collée"
backup_password_hint: "Mot de passe de sauvegarde (uniquement si exportée ailleurs)"
import_btn: "Importer"
importing: "Importation…"
identity_replaced: "Identité remplacée"
now_using: "Utilise maintenant : %{npub}"
import_failed: "Échec de l'importation"
backup_file: "Sauvegarder dans un fichier"
choose_backup_file: "Choisir un fichier .backup"
backup_read_failed: "Impossible de lire ce fichier."
backup_saved: "Sauvegarde enregistrée"
backup_saved_sub: "Conservez le fichier .backup en lieu sûr — quiconque l'a AVEC votre mot de passe peut restaurer votre identité."
backup_file_title: "Sauvegarder l'identité"
backup_file_blurb: "Crée un fichier .backup chiffré avec votre nom d'utilisateur et votre clé. Saisissez le mot de passe du portefeuille pour le sceller."
backup_write_failed: "Impossible d'enregistrer le fichier."
create_backup: "Créer la sauvegarde"
registered: "%{name} enregistré"
released_msg: "Libéré — le nom est disponible"
release_confirm: "Libérer %{name} ?"
release_blurb: "Il est disponible dès qu'il est libre — n'importe qui peut le réserver, y compris la prochaine clé vers laquelle vous renouvelez. Votre photo de profil est supprimée avec lui. Vous ne pourrez pas enregistrer un autre nom d'utilisateur pendant 10 minutes."
release_blurb: "Dès qu'il est libre, il est disponible — n'importe qui peut le réclamer, y compris votre prochaine clé. Vous ne pourrez pas enregistrer un autre nom d'utilisateur pendant 10 minutes."
releasing: "Libération…"
keep_it: "Le garder"
release_it: "Le libérer"
@@ -583,6 +588,10 @@ goblin:
delete: "Supprimer le portefeuille"
delete_desc: "Supprimer définitivement ce portefeuille de cet appareil. Sans votre seed, les fonds sont irrécupérables."
delete_confirm: "Touchez à nouveau pour supprimer"
manage_node: "Gérer la connexion au nœud"
repair_confirm: "Oui, réparer maintenant"
repair_confirm_note: "La réparation réanalyse la chaîne et peut prendre quelques minutes."
restore_confirm_note: "Cela efface les données locales et les reconstruit depuis votre seed — cela peut prendre plusieurs minutes."
privacy:
title: "Confidentialité réseau"
intro: "Goblin envoie son trafic privé via le mixnet Nym — un réseau à cinq sauts qui masque qui parle à qui, afin qu'un relais ne puisse pas relier un paiement à vous."
@@ -590,8 +599,8 @@ goblin:
payments_blurb: "Chaque message nostr transportant un slatepack."
usernames: "usernames"
usernames_blurb: "Recherches de noms NIP-05 vers et depuis goblin.st."
price_avatars: "Cours et avatars"
price_avatars_blurb: "L'aperçu du cours et les photos de contacts."
price_avatars: "Prix"
price_avatars_blurb: "Le taux en temps réel affiché à côté des montants."
over_mixnet: "Via le mixnet"
direct_connection: "Connexion directe"
grin_node: "Nœud grin"
@@ -740,6 +749,10 @@ goblin:
max: "Max"
note_label: "Note"
note_hint: "Ajouter une note…"
add_note: "Ajouter une note"
edit_note: "Modifier la note"
note_cancel: "Annuler"
note_save: "Enregistrer"
review_btn: "Vérifier"
confirm_request: "Confirmer la demande"
review_title: "Vérification"
+26 -13
View File
@@ -406,6 +406,7 @@ goblin:
pending: "В ожидании"
confs: "%{c}/%{r} подтверждений"
waiting_to_confirm: "Ожидание подтверждения"
paying: "Оплата…"
you: "Вы"
to: "Кому"
from: "От"
@@ -448,16 +449,11 @@ goblin:
title: "Настройки"
connected_nostr: "Подключено к nostr"
connecting_relays: "Подключение к реле…"
pic_updated: "Фото профиля обновлено"
uploading_pic: "Загрузка фото…"
claim_first: "Сначала займите имя — фото привязывается к нему"
identity: "Личность"
copy_npub: "Копировать npub (публичный)"
backup_nsec: "Резерв секретного ключа (nsec)"
export_identity: "Экспорт резерва личности (зашифр.)"
rotate_key: "Сменить ключ nostr"
import_identity: "Импорт личности (nsec / резерв)"
backup_note: "Меняете устройство? Сохраните ОБА: seed-фразу (средства) и резерв личности (имя + ключ)."
import_identity: "Импорт личности (.backup / nsec)"
backup_note: "Меняете устройство? Сохраните ОБА: seed-фразу (средства) и файл .backup личности (имя + ключ)."
wallet: "Кошелёк"
display_unit: "Единица отображения"
relays: "Реле"
@@ -512,7 +508,7 @@ goblin:
sp_copy: "Копировать slatepack"
rotate_line1: "• Вы получите совершенно новый СЛУЧАЙНЫЙ ключ; старый npub перестанет принимать. Между ними нет цепочки вывода."
rotate_line2: "• Новый ключ НЕЛЬЗЯ восстановить из seed — сохраните новый nsec сразу после смены."
rotate_line3: "• Ваш username ОСВОБОЖДАЕТСЯ, а фото профиля удаляется — займите то же или новое имя сразу после (любой другой тоже может его занять, как только оно свободно)."
rotate_line3: "• Ваше имя пользователя ОСВОБОЖДАЕТСЯ — сразу после заявите то же или новое имя (как только свободно, его может занять кто угодно)."
rotate_line4: "• Платежи, всё ещё идущие к старому ключу, БУДУТ нарушены — сначала дождитесь завершения ожидающих платежей."
rotate_line5: "• Контакты, сохранившие ваш npub напрямую, должны найти вас заново — поделитесь новым npub или заново занятым username."
cancel: "Отмена"
@@ -531,18 +527,27 @@ goblin:
rotation_failed: "Смена не удалась"
close: "Закрыть"
import_identity_title: "Импорт личности"
import_blurb: "Заменяет nostr-личность этого кошелька — вставьте чистый nsec или экспортированный резерв личности (резерв также восстанавливает имя и историю). Сначала сохраните текущий ключ, если он ещё нужен."
import_nsec_hint: "nsec1… или JSON резерва личности"
import_blurb: "Заменяет nostr-личность этого кошелька — выберите файл GOBLIN .backup или вставьте nsec. Резервная копия также восстанавливает имя и историю. Сначала сохраните текущий ключ, если он ещё нужен."
import_nsec_hint: "nsec1… или вставленная копия"
backup_password_hint: "Пароль резерва (только если экспортирован в другом месте)"
import_btn: "Импорт"
importing: "Импорт…"
identity_replaced: "Личность заменена"
now_using: "Сейчас используется: %{npub}"
import_failed: "Импорт не удался"
backup_file: "Сохранить в файл"
choose_backup_file: "Выбрать файл .backup"
backup_read_failed: "Не удалось прочитать файл."
backup_saved: "Резервная копия сохранена"
backup_saved_sub: "Храните файл .backup в безопасности — любой, у кого есть он И ваш пароль, может восстановить вашу личность."
backup_file_title: "Резервная копия личности"
backup_file_blurb: "Создаёт один зашифрованный файл .backup с именем и ключом. Введите пароль кошелька, чтобы запечатать его."
backup_write_failed: "Не удалось сохранить файл."
create_backup: "Создать копию"
registered: "Зарегистрировано %{name}"
released_msg: "Освобождено — имя свободно для занятия"
release_confirm: "Освободить %{name}?"
release_blurb: "Имя свободно для занятия сразу после освобождения — любой может его занять, включая следующий ключ, на который вы смените. Фото профиля удаляется вместе с ним. Вы не сможете зарегистрировать другое имя в течение 10 минут."
release_blurb: "Как только оно свободно, его можно занять — кто угодно, включая ваш следующий ключ. Вы не сможете зарегистрировать другое имя в течение 10 минут."
releasing: "Освобождение…"
keep_it: "Оставить"
release_it: "Освободить"
@@ -583,6 +588,10 @@ goblin:
delete: "Удалить кошелёк"
delete_desc: "Безвозвратно удалить этот кошелёк с этого устройства. Без seed-фразы средства не восстановить."
delete_confirm: "Нажмите ещё раз для удаления"
manage_node: "Управление подключением к узлу"
repair_confirm: "Да, восстановить сейчас"
repair_confirm_note: "Восстановление повторно сканирует цепочку и может занять несколько минут."
restore_confirm_note: "Это стирает локальные данные и восстанавливает их из seed-фразы — может занять несколько минут."
privacy:
title: "Сетевая приватность"
intro: "Goblin отправляет приватный трафик через mixnet Nym — сеть из пяти переходов, скрывающую, кто с кем общается, чтобы реле не могло связать платёж с вами."
@@ -590,8 +599,8 @@ goblin:
payments_blurb: "Каждое nostr-сообщение, несущее slatepack."
usernames: "usernames"
usernames_blurb: "Поиск имён NIP-05 к и от goblin.st."
price_avatars: "Курс и аватары"
price_avatars_blurb: "Предпросмотр курса и фото контактов."
price_avatars: "Цена"
price_avatars_blurb: "Текущий курс рядом с суммами."
over_mixnet: "Через mixnet"
direct_connection: "Прямое соединение"
grin_node: "Узел Grin"
@@ -740,6 +749,10 @@ goblin:
max: "Макс"
note_label: "Заметка"
note_hint: "Добавить заметку…"
add_note: "Добавить заметку"
edit_note: "Изменить заметку"
note_cancel: "Отмена"
note_save: "Сохранить"
review_btn: "Проверить"
confirm_request: "Подтвердить запрос"
review_title: "Проверка"
+26 -13
View File
@@ -406,6 +406,7 @@ goblin:
pending: "Beklemede"
confs: "%{c}/%{r} onay"
waiting_to_confirm: "Onay bekleniyor"
paying: "Ödeniyor…"
you: "Sen"
to: "Alıcı"
from: "Gönderen"
@@ -448,16 +449,11 @@ goblin:
title: "Ayarlar"
connected_nostr: "nostr'a bağlı"
connecting_relays: "Relaylara bağlanılıyor…"
pic_updated: "Profil fotoğrafı güncellendi"
uploading_pic: "Fotoğraf yükleniyor…"
claim_first: "Önce bir kullanıcı adı al — fotoğraflar ona bağlıdır"
identity: "Kimlik"
copy_npub: "npub kopyala (genel)"
backup_nsec: "Gizli anahtarı yedekle (nsec)"
export_identity: "Kimlik yedeğini dışa aktar (şifreli)"
rotate_key: "nostr anahtarını değiştir"
import_identity: "Kimlik içe aktar (nsec / yedek)"
backup_note: "Cihaz mı değiştiriyorsun? İKİSİNİ de yedekle: tohum kelimeleri (para) ve kimlik yedeği (ad + anahtar)."
import_identity: "Kimlik içe aktar (.backup / nsec)"
backup_note: "Cihaz mı değiştiriyorsun? İKİSİNİ de yedekle: seed ifaden (bakiye) ve kimlik .backup dosyan (ad + anahtar)."
wallet: "Cüzdan"
display_unit: "Görüntüleme birimi"
relays: "Relaylar"
@@ -512,7 +508,7 @@ goblin:
sp_copy: "Slatepack kopyala"
rotate_line1: "• Tamamen yeni RASTGELE bir anahtar alırsın; eski npub artık almaz. Aralarında türetme zinciri yoktur."
rotate_line2: "• Yeni anahtar tohumundan kurtarılamaz — anahtarı değiştirdikten hemen sonra yeni nsec'i yedekle."
rotate_line3: "• username'in BIRAKILIR ve profil fotoğrafın silinir — hemen aynı ya da yeni bir adı al (boştayken başkası da kapabilir)."
rotate_line3: "• Kullanıcı adın SERBEST BIRAKILIR — hemen ardından aynı adı ya da yeni bir ad al (serbest kaldığında başkası da kapabilir)."
rotate_line4: "• Eski anahtara hâlâ yoldaki ödemeler KESİNTİYE uğrar — önce bekleyen ödemelerin bitmesini bekle."
rotate_line5: "• npub'unu doğrudan kaydeden kişiler seni yeniden bulmalı — yeni npub'unu ya da yeniden aldığın username'i paylaş."
cancel: "İptal"
@@ -531,18 +527,27 @@ goblin:
rotation_failed: "Değiştirme başarısız"
close: "Kapat"
import_identity_title: "Kimlik içe aktar"
import_blurb: "Bu cüzdanın nostr kimliğini değiştirir — çıplak bir nsec ya da dışa aktarılmış kimlik yedeği yapıştır (yedek ayrıca kullanıcı adını ve geçmişini de geri yükler). Hâlâ gerekiyorsa önce mevcut anahtarı yedekle."
import_nsec_hint: "nsec1… ya da kimlik yedeği JSON"
import_blurb: "Bu cüzdanın nostr kimliğini değiştirir — bir GOBLIN .backup dosyası seç ya da nsec yapıştır. Yedek ayrıca kullanıcı adını ve geçmişini geri yükler. Hâlâ gerekiyorsa önce mevcut anahtarı yedekle."
import_nsec_hint: "nsec1… veya yapıştırılan yedek"
backup_password_hint: "Yedek parolası (yalnızca başka yerde dışa aktarıldıysa)"
import_btn: "İçe aktar"
importing: "İçe aktarılıyor…"
identity_replaced: "Kimlik değiştirildi"
now_using: "Şu an kullanılan: %{npub}"
import_failed: "İçe aktarma başarısız"
backup_file: "Dosyaya yedekle"
choose_backup_file: "Bir .backup dosyası seç"
backup_read_failed: "Dosya okunamadı."
backup_saved: "Yedek kaydedildi"
backup_saved_sub: ".backup dosyasını güvende tut — hem ona hem de parolana sahip olan kimliğini geri yükleyebilir."
backup_file_title: "Kimliği yedekle"
backup_file_blurb: "Kullanıcı adın ve anahtarınla tek bir şifreli .backup dosyası oluşturur. Mühürlemek için cüzdan parolanı gir."
backup_write_failed: "Dosya kaydedilemedi."
create_backup: "Yedek oluştur"
registered: "%{name} kaydedildi"
released_msg: "Bırakıldı — ad artık alınabilir"
release_confirm: "%{name} bırakılsın mı?"
release_blurb: "Boşaldığı an alınabilir — değiştireceğin yeni anahtar dahil herkes kapabilir. Profil fotoğrafın da onunla silinir. 10 dakika boyunca başka kullanıcı adı kaydedemezsin."
release_blurb: "Serbest kalır kalmaz herkes alabilir — döndüğün bir sonraki anahtar dahil. 10 dakika boyunca başka bir kullanıcı adı kaydedemezsin."
releasing: "Bırakılıyor…"
keep_it: "Vazgeç"
release_it: "Bırak"
@@ -583,6 +588,10 @@ goblin:
delete: "Cüzdanı sil"
delete_desc: "Bu cüzdanı bu cihazdan kalıcı olarak kaldır. Tohumun olmadan fonlar kurtarılamaz."
delete_confirm: "Silmek için tekrar dokun"
manage_node: "Düğüm bağlantısını yönet"
repair_confirm: "Evet, şimdi onar"
repair_confirm_note: "Onarım zinciri yeniden tarar ve birkaç dakika sürebilir."
restore_confirm_note: "Bu, yerel verileri siler ve seed'inizden yeniden oluşturur — birkaç dakika sürebilir."
privacy:
title: "Ağ gizliliği"
intro: "Goblin özel trafiğini Nym mixnet üzerinden gönderir — kimin kiminle konuştuğunu gizleyen beş atlamalı bir ağ, böylece bir relay bir ödemeyi sana bağlayamaz."
@@ -590,8 +599,8 @@ goblin:
payments_blurb: "Slatepack taşıyan her nostr mesajı."
usernames: "usernamelar"
usernames_blurb: "goblin.st'ye ve oradan NIP-05 ad aramaları."
price_avatars: "Fiyat ve avatarlar"
price_avatars_blurb: "Kur önizlemesi ve kişi fotoğrafları."
price_avatars: "Fiyat"
price_avatars_blurb: "Tutarların yanında gösterilen anlık kur."
over_mixnet: "Mixnet üzerinden"
direct_connection: "Doğrudan bağlantı"
grin_node: "Grin düğümü"
@@ -740,6 +749,10 @@ goblin:
max: "Maks"
note_label: "Not"
note_hint: "Bir not ekle…"
add_note: "Not ekle"
edit_note: "Notu düzenle"
note_cancel: "İptal"
note_save: "Kaydet"
review_btn: "İncele"
confirm_request: "İsteği onayla"
review_title: "İncele"
+26 -13
View File
@@ -406,6 +406,7 @@ goblin:
pending: "待处理"
confs: "%{c}/%{r} 次确认"
waiting_to_confirm: "等待确认"
paying: "支付中…"
you: "你"
to: "收款方"
from: "付款方"
@@ -448,16 +449,11 @@ goblin:
title: "设置"
connected_nostr: "已连接 nostr"
connecting_relays: "正在连接中继…"
pic_updated: "头像已更新"
uploading_pic: "正在上传头像…"
claim_first: "请先注册用户名 — 头像依附于它"
identity: "身份"
copy_npub: "复制 npub(公开)"
backup_nsec: "备份私钥(nsec"
export_identity: "导出身份备份(加密)"
rotate_key: "轮换 nostr 密钥"
import_identity: "导入身份(nsec / 备份"
backup_note: "更换设备?请同时备份:助记词(资金)和身份备份(用户名 + 密钥)。"
import_identity: "导入身份(.backup / nsec"
backup_note: "更换设备?两者都要备份:你的助记词(资金)和身份 .backup 文件(名称 + 密钥)。"
wallet: "钱包"
display_unit: "显示单位"
relays: "中继"
@@ -512,7 +508,7 @@ goblin:
sp_copy: "复制 slatepack"
rotate_line1: "• 你会得到一个全新的随机密钥;旧 npub 将停止接收。两者之间没有任何派生关系。"
rotate_line2: "• 新密钥无法从助记词恢复 — 轮换后请立即备份新的 nsec。"
rotate_line3: "• 你的 username 将被释放,头像也会被删除 — 请立即重新注册同名或新用户名(一旦释放,任何人都可抢注)。"
rotate_line3: "• 你的用户名将被释放——请立即认领相同或新的名称(一旦释放,他人也可抢注)。"
rotate_line4: "• 正在发往旧密钥的付款将受影响 — 请先等待待处理付款完成。"
rotate_line5: "• 直接保存了你 npub 的联系人需要重新查找你 — 分享你的新 npub 或重新注册的 username。"
cancel: "取消"
@@ -531,18 +527,27 @@ goblin:
rotation_failed: "轮换失败"
close: "关闭"
import_identity_title: "导入身份"
import_blurb: "替换此钱包的 nostr 身份粘贴 nsec 或导出的身份备份(备份还会恢复你的用户名和历史记录)。若仍需要当前密钥,请先备份。"
import_nsec_hint: "nsec1… 或身份备份 JSON"
import_blurb: "替换此钱包的 nostr 身份——选择一个 GOBLIN .backup 文件,或粘贴 nsec。备份也会恢复你的用户名和历史。如果仍需要当前密钥,请先备份。"
import_nsec_hint: "nsec1… 或粘贴的备份"
backup_password_hint: "备份密码(仅当在他处导出时需要)"
import_btn: "导入"
importing: "正在导入…"
identity_replaced: "身份已替换"
now_using: "当前使用:%{npub}"
import_failed: "导入失败"
backup_file: "备份到文件"
choose_backup_file: "选择 .backup 文件"
backup_read_failed: "无法读取该文件。"
backup_saved: "备份已保存"
backup_saved_sub: "妥善保管 .backup 文件——同时拥有它和你密码的人都能恢复你的身份。"
backup_file_title: "备份身份"
backup_file_blurb: "创建一个包含你的用户名和密钥的加密 .backup 文件。输入钱包密码以封存它。"
backup_write_failed: "无法保存文件。"
create_backup: "创建备份"
registered: "已注册 %{name}"
released_msg: "已释放 — 用户名可被抢注"
release_confirm: "释放 %{name}"
release_blurb: "释放后立即可被抢注 — 任何人都能注册,包括你下次轮换到的密钥。头像也会一并删除。10 分钟内你无法注册用户名。"
release_blurb: "一旦释放即可被认领——任何人都可以,包括你接下来轮换到的密钥。10 分钟内你无法注册另一个用户名。"
releasing: "正在释放…"
keep_it: "保留"
release_it: "释放"
@@ -583,6 +588,10 @@ goblin:
delete: "删除钱包"
delete_desc: "从此设备永久移除该钱包。没有助记词,资金将无法找回。"
delete_confirm: "再次点击以删除"
manage_node: "管理节点连接"
repair_confirm: "是的,立即修复"
repair_confirm_note: "修复会重新扫描链,可能需要几分钟。"
restore_confirm_note: "这会清除本地数据并从助记词重建——可能需要几分钟。"
privacy:
title: "网络隐私"
intro: "Goblin 通过 Nym mixnet 发送其私密流量 — 这是一个五跳网络,可隐藏通信双方的身份,使中继无法将付款关联到你。"
@@ -590,8 +599,8 @@ goblin:
payments_blurb: "每条携带 slatepack 的 nostr 消息。"
usernames: "用户名"
usernames_blurb: "往返 goblin.st 的 NIP-05 名称查询。"
price_avatars: "汇率和头像"
price_avatars_blurb: "汇率预览和联系人头像。"
price_avatars: "价格"
price_avatars_blurb: "金额旁显示的实时法币汇率。"
over_mixnet: "经由 mixnet"
direct_connection: "直接连接"
grin_node: "Grin 节点"
@@ -740,6 +749,10 @@ goblin:
max: "最大"
note_label: "备注"
note_hint: "添加备注…"
add_note: "添加备注"
edit_note: "编辑备注"
note_cancel: "取消"
note_save: "保存"
review_btn: "查看"
confirm_request: "确认请求"
review_title: "查看"
+18 -9
View File
@@ -105,16 +105,25 @@ pub fn receipt_detail(wallet: &Wallet, tx_id: u32) -> Option<ReceiptDetail> {
} else {
Some(tx.data.fee.map(|f| f.fee()).unwrap_or(0))
};
let confs = if tx.data.confirmed {
None
} else {
match tx.height {
Some(h) if h > 0 && data.info.last_confirmed_height >= h => Some((
data.info.last_confirmed_height - h + 1,
data.info.minimum_confirmations,
)),
_ => Some((0, data.info.minimum_confirmations)),
// Confirmation progress toward the spendable threshold (min_confirmations).
// grin flips `confirmed` to true at the FIRST on-chain block, but a payment
// isn't spendable until min_confirmations — so keep counting 1/10 … 10/10
// instead of jumping straight to "complete" at one block (which is why the
// count never appeared to move).
let min_conf = data.info.minimum_confirmations;
let confs = match tx.height {
Some(h) if h > 0 && data.info.last_confirmed_height >= h => {
let count = data.info.last_confirmed_height - h + 1;
if count >= min_conf {
None // matured — fully spendable
} else {
Some((count, min_conf))
}
}
// On-chain but exact height not yet known: at least one block in.
_ if tx.data.confirmed => Some((1.min(min_conf), min_conf)),
// Broadcast but not yet mined.
_ => Some((0, min_conf)),
};
let canceled = is_canceled(tx, meta.as_ref());
let has_identity = meta
+313 -151
View File
@@ -70,6 +70,8 @@ pub struct GoblinWalletView {
rotate: Option<RotateState>,
/// Inline nsec-import state for the Me tab.
import_nsec: Option<ImportState>,
/// Inline "back up identity to a file" flow state.
backup: Option<BackupState>,
/// Amount being entered on the Pay tab.
pay_amount: String,
/// When set, the over-balance "no" animation is playing: the start time (egui
@@ -99,12 +101,6 @@ pub struct GoblinWalletView {
avatars: avatars::AvatarTextures,
/// Manual slatepack page state (GRIM-native send/receive fallback).
slatepack: SlatepackManual,
/// Profile-picture upload in flight.
avatar_busy: bool,
/// Upload worker result: (server hash, processed png) or error.
avatar_slot: std::sync::Arc<std::sync::Mutex<Option<Result<(String, Vec<u8>), String>>>>,
/// Last upload outcome message (cleared on the next attempt).
avatar_msg: Option<String>,
/// Receipt "Cancel payment" tap-twice confirm: the tx_id awaiting a second
/// confirming tap (cleared when another receipt opens or it's fired).
cancel_confirm: Option<u32>,
@@ -139,6 +135,8 @@ struct AdvancedState {
wrong_pass: bool,
/// Armed "really restore?" confirm.
confirm_restore: bool,
/// Armed "really repair?" confirm (repair takes a few minutes).
confirm_repair: bool,
/// Armed "really delete?" confirm.
confirm_delete: bool,
}
@@ -174,6 +172,7 @@ impl Default for GoblinWalletView {
claim: None,
rotate: None,
import_nsec: None,
backup: None,
pay_amount: String::new(),
pay_shake: None,
request_amount: None,
@@ -187,9 +186,6 @@ impl Default for GoblinWalletView {
receive_copied: None,
slatepack: SlatepackManual::default(),
avatars: avatars::AvatarTextures::default(),
avatar_busy: false,
avatar_slot: std::sync::Arc::new(std::sync::Mutex::new(None)),
avatar_msg: None,
cancel_confirm: None,
cancel_msg: None,
copy_flash: None,
@@ -231,6 +227,8 @@ struct ImportState {
backup_password: String,
new_npub: String,
error: String,
/// A native file pick is in flight (Android returns the path asynchronously).
picking: bool,
result: std::sync::Arc<std::sync::Mutex<Option<Result<String, String>>>>,
}
@@ -243,11 +241,23 @@ impl Default for ImportState {
backup_password: String::new(),
new_npub: String::new(),
error: String::new(),
picking: false,
result: std::sync::Arc::new(std::sync::Mutex::new(None)),
}
}
}
/// Inline "back up identity to a file" flow state.
#[derive(Default)]
struct BackupState {
/// Wallet password to unseal the identity for the backup.
password: String,
/// Error to show (wrong password / write failed).
error: Option<String>,
/// The backup file was created.
done: bool,
}
/// Inline username-claim widget state.
struct ClaimState {
input: String,
@@ -1321,6 +1331,23 @@ impl GoblinWalletView {
t!("goblin.receipt.funds_returned").to_string()
},
)
} else if let Some((c, r)) = d.confs {
// On-chain but still maturing toward the spendable
// threshold — show the live X/N count (grin marks a
// tx confirmed at one block; spendable takes N).
if c == 0 && !d.incoming && d.npub.is_some() {
// Sent but not yet picked up / mined.
(
t!("goblin.receipt.pending").to_string(),
t!("goblin.receipt.waiting_to_receive", name => d.title)
.to_string(),
)
} else {
(
t!("goblin.receipt.pending").to_string(),
t!("goblin.receipt.confs", c => c, r => r).to_string(),
)
}
} else if d.confirmed {
(
t!("goblin.receipt.complete").to_string(),
@@ -1333,22 +1360,7 @@ impl GoblinWalletView {
} else {
(
t!("goblin.receipt.pending").to_string(),
match d.confs {
Some((c, r)) => {
t!("goblin.receipt.confs", c => c, r => r)
.to_string()
}
// An outgoing nostr send with no confirmations
// yet hasn't been received — say so honestly,
// rather than implying it's on-chain.
None if !d.incoming && d.npub.is_some() => {
t!("goblin.receipt.waiting_to_receive", name => d.title)
.to_string()
}
None => {
t!("goblin.receipt.waiting_to_confirm").to_string()
}
},
t!("goblin.receipt.waiting_to_confirm").to_string(),
)
};
w::info_row(ui, &status, &sub);
@@ -1368,12 +1380,18 @@ impl GoblinWalletView {
&data::short_npub(npub),
);
}
let fee = match d.fee {
Some(0) => t!("goblin.receipt.fee_none").to_string(),
Some(f) => format!("{}{}", w::amount_str(f), w::TSU),
None => "".to_string(),
};
w::info_row(ui, &t!("goblin.receipt.network_fee"), &fee);
// Only the SENDER pays a network fee, so the row only makes
// sense on outgoing payments. A received payment has no fee
// (data sets it to None) — hide the row entirely instead of
// showing a confusing "—".
if let Some(fee_amount) = d.fee {
let fee = if fee_amount == 0 {
t!("goblin.receipt.fee_none").to_string()
} else {
format!("{}{}", w::amount_str(fee_amount), w::TSU)
};
w::info_row(ui, &t!("goblin.receipt.network_fee"), &fee);
}
w::info_row(
ui,
&t!("goblin.receipt.privacy"),
@@ -1900,17 +1918,36 @@ impl GoblinWalletView {
)),
|ui| {
let already = self.approving.contains(&req.rumor_id);
ui.add_enabled_ui(!already, |ui| {
if approve_button(ui) {
// Guard against double-tap: only enqueue the
// payment once per request id this session.
self.approving.insert(req.rumor_id.clone());
self.request_error = None;
wallet.task(crate::wallet::types::WalletTask::NostrPayRequest(
req.rumor_id.clone(),
));
let working = already
&& wallet
.nostr_service()
.map(|s| s.send_phase() == crate::nostr::send_phase::WORKING)
.unwrap_or(false);
if already {
// Paying: show a centered spinner so the tap clearly
// registered (the card clears itself once it's sent).
ui.vertical_centered(|ui| {
ui.add_space(6.0);
View::small_loading_spinner(ui);
ui.add_space(2.0);
ui.label(
RichText::new(t!("goblin.receipt.paying"))
.font(FontId::new(12.0, fonts::regular()))
.color(t.text_dim),
);
});
if working {
ui.ctx().request_repaint();
}
});
} else if approve_button(ui) {
// Guard against double-tap: only enqueue the
// payment once per request id this session.
self.approving.insert(req.rumor_id.clone());
self.request_error = None;
wallet.task(crate::wallet::types::WalletTask::NostrPayRequest(
req.rumor_id.clone(),
));
}
},
);
});
@@ -2092,34 +2129,17 @@ impl GoblinWalletView {
)
});
// Poll a finished avatar upload.
if let Some(res) = self.avatar_slot.lock().unwrap().take() {
self.avatar_busy = false;
match res {
Ok((hash, png)) => {
if let Some(b) = bare_name.as_deref() {
self.avatars.set_own(ui.ctx(), b, &hash, &png);
}
self.avatar_msg = Some(t!("goblin.settings.pic_updated").to_string());
}
Err(e) => self.avatar_msg = Some(e),
}
}
let hue = data::hue_of(&npub_hex);
let own_tex = bare_name
.as_deref()
.and_then(|_| self.handle_tex(ui.ctx(), wallet, &handle));
let mut pick_picture = false;
let avatar_busy = self.avatar_busy;
let avatar_msg = self.avatar_msg.clone();
w::card(ui, |ui| {
ui.set_min_width(ui.available_width());
ui.horizontal(|ui| {
// Avatar is display-only for now: tapping does nothing (no custom
// picture upload). Letter/identicon pucks only.
// Avatar is a generated identicon (gradient + initial) — Goblin has
// no uploaded profile pictures.
w::avatar_any(ui, &handle, &npub_hex, 56.0, hue, own_tex.as_ref());
let _ = (avatar_busy, &mut pick_picture);
ui.add_space(14.0);
ui.vertical(|ui| {
ui.label(
@@ -2155,42 +2175,7 @@ impl GoblinWalletView {
}
});
});
if avatar_busy {
ui.add_space(6.0);
ui.horizontal(|ui| {
View::small_loading_spinner(ui);
ui.add_space(8.0);
ui.label(
RichText::new(t!("goblin.settings.uploading_pic"))
.font(FontId::new(12.5, fonts::regular()))
.color(t.surface_text_dim),
);
});
ui.ctx().request_repaint();
} else if let Some(msg) = &avatar_msg {
ui.add_space(6.0);
let good = *msg == t!("goblin.settings.pic_updated");
ui.label(
RichText::new(msg)
.font(FontId::new(12.5, fonts::regular()))
.color(if good { t.pos } else { t.neg }),
);
}
});
if pick_picture {
match bare_name.clone() {
Some(name) => {
if let Some(path) = cb.pick_image_file() {
self.avatar_busy = true;
self.avatar_msg = None;
start_avatar_upload(self.avatar_slot.clone(), path, name, wallet);
}
}
None => {
self.avatar_msg = Some(t!("goblin.settings.claim_first").to_string());
}
}
}
ui.add_space(16.0);
// Mark the scroll boundary: rows clipping under the pinned profile
@@ -2222,28 +2207,15 @@ impl GoblinWalletView {
cb.vibrate_copy();
self.copy_flash = Some(std::time::Instant::now());
}
// A real backup is the SECRET key (nsec), not the npub.
if settings_row_btn(ui, &t!("goblin.settings.backup_nsec"), COPY) {
if let Some(nsec) = wallet.nostr_service().and_then(|s| s.nsec()) {
cb.copy_string_to_buffer(nsec);
cb.vibrate_copy();
self.copy_flash = Some(std::time::Instant::now());
}
}
// Encrypted backup file: the identity JSON as stored
// (NIP-49 ncryptsec inside), incl. username + history.
// One encrypted backup FILE (key + username + history sealed
// together) — replaces the old copy-nsec / copy-JSON split.
if settings_row_btn(
ui,
&t!("goblin.settings.export_identity"),
&t!("goblin.settings.backup_file"),
crate::gui::icons::DOWNLOAD_SIMPLE,
) {
if let Some(s) = wallet.nostr_service() {
let json = serde_json::to_string_pretty(&*s.identity.read())
.unwrap_or_default();
cb.copy_string_to_buffer(json);
cb.vibrate_copy();
self.copy_flash = Some(std::time::Instant::now());
}
) && self.backup.is_none()
{
self.backup = Some(BackupState::default());
}
if settings_row_danger(
ui,
@@ -2291,6 +2263,10 @@ impl GoblinWalletView {
.font(FontId::new(12.0, fonts::regular()))
.color(t.text_mute),
);
if self.backup.is_some() {
ui.add_space(8.0);
self.backup_ui(ui, wallet, cb);
}
if self.rotate.is_some() {
ui.add_space(8.0);
self.rotate_ui(ui, wallet, cb);
@@ -2671,6 +2647,7 @@ impl GoblinWalletView {
let repairing = wallet.is_repairing();
let progress = wallet.repairing_progress();
let mut leave = false;
let mut open_node = false;
{
let adv = &mut self.advanced;
ScrollArea::vertical()
@@ -2684,8 +2661,8 @@ impl GoblinWalletView {
);
ui.add_space(16.0);
// Run your own node (the internal node). External nodes stay in
// Settings; this advanced toggle starts a full local node instead.
// Run your own node (the internal node). Opens the node-connection
// page, where you pick the integrated node or an external one.
w::card(ui, |ui| {
ui.set_min_width(ui.available_width());
advanced_head(ui, &t!("goblin.node.integrated"), t.surface_text);
@@ -2701,9 +2678,10 @@ impl GoblinWalletView {
.font(FontId::new(13.0, fonts::medium()))
.color(t.pos),
);
} else if w::big_action_on_card(ui, &t!("goblin.node.integrated")).clicked()
{
wallet.update_connection(&ConnectionMethod::Integrated);
ui.add_space(10.0);
}
if w::big_action_on_card(ui, &t!("goblin.advanced.manage_node")).clicked() {
open_node = true;
}
});
ui.add_space(12.0);
@@ -2728,9 +2706,28 @@ impl GoblinWalletView {
.font(FontId::new(13.0, fonts::medium()))
.color(t.neg),
);
} else if adv.confirm_repair {
// Repair re-scans the chain — it can take a few minutes.
// Warn + confirm in the accent (yellow) before starting.
ui.label(
RichText::new(t!("goblin.advanced.repair_confirm_note"))
.font(FontId::new(13.0, fonts::regular()))
.color(t.text_dim),
);
ui.add_space(10.0);
if w::big_action_on_card_ink(
ui,
&t!("goblin.advanced.repair_confirm"),
t.accent,
)
.clicked()
{
adv.confirm_repair = false;
wallet.repair();
}
} else if w::big_action_on_card(ui, &t!("goblin.advanced.repair")).clicked()
{
wallet.repair();
adv.confirm_repair = true;
}
});
ui.add_space(12.0);
@@ -2742,6 +2739,12 @@ impl GoblinWalletView {
advanced_desc(ui, &t!("goblin.advanced.restore_desc"));
ui.add_space(10.0);
if adv.confirm_restore {
ui.label(
RichText::new(t!("goblin.advanced.restore_confirm_note"))
.font(FontId::new(13.0, fonts::regular()))
.color(t.text_dim),
);
ui.add_space(10.0);
if w::big_action_on_card_ink(
ui,
&t!("goblin.advanced.restore_confirm"),
@@ -2852,6 +2855,11 @@ impl GoblinWalletView {
self.advanced = AdvancedState::default();
self.settings_page = SettingsPage::Main;
}
if open_node {
self.node_url_input.clear();
self.node_secret_input.clear();
self.settings_page = SettingsPage::Node;
}
}
fn node_settings_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
@@ -2869,6 +2877,30 @@ impl GoblinWalletView {
let live = wallet.get_current_connection();
let saved = wallet.get_config().connection();
settings_group(ui, &t!("goblin.node.connection"), |ui| {
// Integrated node (run your own) sits at the top of the picker.
{
let active = matches!(&saved, ConnectionMethod::Integrated);
let row = ui.horizontal(|ui| {
ui.label(
RichText::new(t!("goblin.node.integrated"))
.font(FontId::new(15.0, fonts::medium()))
.color(t.surface_text),
);
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() {
wallet.update_connection(&ConnectionMethod::Integrated);
}
}
for conn in ConnectionsConfig::ext_conn_list() {
let active =
matches!(&saved, ConnectionMethod::External(id, _) if *id == conn.id);
@@ -3526,6 +3558,117 @@ impl GoblinWalletView {
}
/// Inline nsec-import flow: replaces the identity with an imported key.
/// 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) {
let t = theme::tokens();
let bk = self.backup.as_mut().unwrap();
let mut close = false;
w::card(ui, |ui| {
ui.set_min_width(ui.available_width());
if bk.done {
ui.label(
RichText::new(t!("goblin.settings.backup_saved"))
.font(FontId::new(15.0, fonts::semibold()))
.color(t.pos),
);
ui.add_space(4.0);
ui.label(
RichText::new(t!("goblin.settings.backup_saved_sub"))
.font(FontId::new(13.0, fonts::regular()))
.color(t.surface_text_dim),
);
ui.add_space(10.0);
if w::big_action(ui, &t!("goblin.settings.done"), false).clicked() {
close = true;
}
return;
}
ui.label(
RichText::new(t!("goblin.settings.backup_file_title"))
.font(FontId::new(15.0, fonts::semibold()))
.color(t.surface_text),
);
ui.add_space(6.0);
ui.label(
RichText::new(t!("goblin.settings.backup_file_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("backup_pass"))
.focus(false)
.hint_text(t!("goblin.settings.wallet_password"))
.password()
.text_color(t.surface_text)
.body()
.ui(ui, &mut bk.password, cb);
});
if let Some(err) = &bk.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 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.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(half, 44.0),
)),
|ui| {
ui.add_enabled_ui(!bk.password.is_empty(), |ui| {
if w::big_action(ui, &t!("goblin.settings.create_backup"), false)
.clicked()
{
match wallet.create_nostr_backup(&bk.password) {
Ok(envelope) => {
let stamp = chrono::Local::now().format("%Y-%m-%d-%H%M");
let fname = format!("GOBLIN-{stamp}.backup");
match cb.share_data(fname, envelope.into_bytes()) {
Ok(()) => {
bk.done = true;
bk.error = None;
bk.password.clear();
}
Err(_) => {
bk.error = Some(
t!("goblin.settings.backup_write_failed")
.to_string(),
);
}
}
}
Err(e) => bk.error = Some(e),
}
}
});
},
);
});
});
if close {
self.backup = None;
}
}
fn import_nsec_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
let t = theme::tokens();
let import = self.import_nsec.as_mut().unwrap();
@@ -3560,6 +3703,52 @@ impl GoblinWalletView {
.color(t.surface_text_dim),
);
ui.add_space(10.0);
// Native ".backup file" picker. Desktop returns the path now;
// Android returns it asynchronously (poll picked_file()).
if import.picking {
if let Some(path) = cb.picked_file() {
import.picking = false;
if !path.is_empty() {
match std::fs::read_to_string(&path) {
Ok(contents) => import.nsec = contents.trim().to_string(),
Err(_) => {
import.error =
t!("goblin.settings.backup_read_failed").to_string();
}
}
}
} else {
ui.ctx().request_repaint();
}
}
if w::big_action_on_card(ui, &t!("goblin.settings.choose_backup_file"))
.clicked()
{
import.error.clear();
match cb.pick_file() {
Some(path) if !path.is_empty() => {
match std::fs::read_to_string(&path) {
Ok(contents) => import.nsec = contents.trim().to_string(),
Err(_) => {
import.error =
t!("goblin.settings.backup_read_failed").to_string();
}
}
}
// Empty string = Android async pick in flight.
Some(_) => import.picking = true,
None => {}
}
}
if !import.error.is_empty() && import.stage == 1 {
ui.add_space(6.0);
ui.label(
RichText::new(&import.error)
.font(FontId::new(12.5, fonts::regular()))
.color(t.neg),
);
}
ui.add_space(8.0);
w::field_well(ui, |ui| {
TextEdit::new(egui::Id::from("import_nsec"))
.focus(false)
@@ -3727,6 +3916,10 @@ impl GoblinWalletView {
}
s.save_identity();
}
// Publish kind 0 NOW so others can resolve our @name without
// waiting for the next app start — otherwise a just-claimed
// name is invisible over the relays (no kind-0 event exists).
wallet.task(crate::wallet::types::WalletTask::NostrRepublishProfile);
}
ClaimMsg::Released => {
claim.message = Some(t!("goblin.settings.released_msg").to_string());
@@ -4007,37 +4200,6 @@ fn start_release(claim: &mut ClaimState, name: &str, wallet: &Wallet) {
}
/// Process a picked picture and upload it as the avatar for an owned name.
fn start_avatar_upload(
slot: std::sync::Arc<std::sync::Mutex<Option<Result<(String, Vec<u8>), String>>>>,
path: String,
name: String,
wallet: &Wallet,
) {
let Some(service) = wallet.nostr_service() else {
return;
};
let server = service.config.read().nip05_server();
// Reuse the service's keys directly — never round-trip the secret through a
// plaintext nsec String to rebuild keys the service already holds.
let keys = service.keys();
std::thread::spawn(move || {
let res = (|| {
let png = crate::nostr::avatar::process_avatar_file(&path)?;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| e.to_string())?;
let hash = rt.block_on(crate::nostr::nip05::upload_avatar(
&server,
&name,
&keys,
png.clone(),
))?;
Ok((hash, png))
})();
*slot.lock().unwrap() = Some(res);
});
}
/// Draw the small Goblin mascot mark.
pub fn widgets_logo(ui: &mut egui::Ui) {
+77 -41
View File
@@ -141,6 +141,10 @@ pub struct SendFlow {
/// Atomic amount (nanogrin) we last asked the wallet to price, so the review
/// page dispatches one `CalculateFee` per amount instead of every frame.
fee_requested_for: Option<u64>,
/// The note editor sheet is open (tapped "Add note").
note_editing: bool,
/// Draft note held while the editor is open, so Cancel discards it.
note_draft: String,
}
impl Default for SendFlow {
@@ -166,6 +170,8 @@ impl Default for SendFlow {
request: false,
receipt_npub: None,
fee_requested_for: None,
note_editing: false,
note_draft: String::new(),
}
}
}
@@ -944,31 +950,6 @@ impl SendFlow {
}
ui.add_space(16.0);
// Quick chips.
ui.horizontal(|ui| {
ui.add_space((ui.available_width() - 220.0).max(0.0) / 2.0);
for v in ["1", "10", "100", "Max"] {
let label = if v == "Max" {
t!("goblin.send.max")
} else {
std::borrow::Cow::Borrowed(v)
};
if w::chip_outline(ui, &label).clicked() {
if v == "Max" {
let max = wallet
.get_data()
.map(|d| d.info.amount_currently_spendable)
.unwrap_or(0);
self.amount = w::amount_str(max);
} else {
self.amount = v.to_string();
}
}
ui.add_space(8.0);
}
});
ui.add_space(16.0);
let note_id = egui::Id::from("send_note");
// Numpad / typed amount FIRST, then the note BELOW it. On mobile the soft
// keyboard for the note covers the bottom of the screen — keeping the pad
@@ -988,23 +969,78 @@ impl SendFlow {
}
ui.add_space(12.0);
// Note field (under the pad).
w::card(ui, |ui| {
ui.horizontal(|ui| {
ui.label(
RichText::new(t!("goblin.send.note_label"))
.font(FontId::new(14.0, fonts::regular()))
.color(t.surface_text_dim),
);
ui.add_space(8.0);
TextEdit::new(note_id)
.focus(false)
.hint_text(t!("goblin.send.note_hint"))
.text_color(t.surface_text)
.body()
.ui(ui, &mut self.note, cb);
// Note: a tap-to-open editor instead of an always-open field fighting the
// numpad for the keyboard. "Add note" → type → Cancel discards, Save keeps.
if self.note_editing {
w::card(ui, |ui| {
ui.set_min_width(ui.available_width());
w::field_well(ui, |ui| {
TextEdit::new(note_id)
.focus(true)
.hint_text(t!("goblin.send.note_hint"))
.text_color(t.surface_text)
.body()
.ui(ui, &mut self.note_draft, cb);
});
ui.add_space(10.0);
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_ink(
ui,
&t!("goblin.send.note_cancel"),
t.surface_text_dim,
)
.clicked()
{
self.note_editing = false;
self.note_draft.clear();
ui.ctx().memory_mut(|m| m.surrender_focus(note_id));
}
},
);
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_on_card(ui, &t!("goblin.send.note_save")).clicked() {
self.note = self.note_draft.trim().to_string();
self.note_editing = false;
ui.ctx().memory_mut(|m| m.surrender_focus(note_id));
}
},
);
});
});
});
} else if self.note.trim().is_empty() {
if w::big_action(ui, &t!("goblin.send.add_note"), true).clicked() {
self.note_draft = self.note.clone();
self.note_editing = true;
}
} else {
// Show the saved note, with an Edit button to re-open the editor.
w::card(ui, |ui| {
ui.set_min_width(ui.available_width());
ui.label(
RichText::new(format!("\u{201C}{}\u{201D}", self.note.trim()))
.font(FontId::new(14.0, fonts::regular()))
.color(t.surface_text),
);
});
ui.add_space(8.0);
if w::big_action(ui, &t!("goblin.send.edit_note"), true).clicked() {
self.note_draft = self.note.clone();
self.note_editing = true;
}
}
ui.add_space(8.0);
let valid = amount_from_hr_string(&self.amount)