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:
+26
-13
@@ -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 Wallets — fü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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: "查看"
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user