From b368958a910ee57361b181bda87154d2ac25daf9 Mon Sep 17 00:00:00 2001 From: Avi Date: Tue, 1 Sep 2026 09:48:38 -0500 Subject: [PATCH] fix: derive imported profile name --- frontend/src/screens/ImportProfileModal.tsx | 43 +++++++++++++++++++++ src/profiles.rs | 33 ++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 frontend/src/screens/ImportProfileModal.tsx diff --git a/frontend/src/screens/ImportProfileModal.tsx b/frontend/src/screens/ImportProfileModal.tsx new file mode 100644 index 0000000..09430fb --- /dev/null +++ b/frontend/src/screens/ImportProfileModal.tsx @@ -0,0 +1,43 @@ +import { useEffect, useRef, useState, type FormEvent } from 'react'; +import { Button } from '../components/Button'; +import { ErrorText } from '../components/ErrorText'; +import { Modal } from '../components/Modal'; +import { useApp } from '../state/AppProvider'; + +export function ImportProfileModal({ open, onClose }: { open: boolean; onClose: () => void }) { + const { importProfile } = useApp(); + const [secret, setSecret] = useState(''); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const labelRef = useRef(null); + + useEffect(() => { + if (open) { + setSecret(''); setError(null); setSaving(false); + requestAnimationFrame(() => labelRef.current?.focus()); + } + }, [open]); + + const submit = async (event: FormEvent) => { + event.preventDefault(); + if (!secret.trim() || saving) return; + setSaving(true); setError(null); + try { await importProfile('', secret.trim()); onClose(); } + catch (err) { setError(err instanceof Error ? err.message : String(err)); setSaving(false); } + }; + + return +
+

Import an account using its private key. The key stays in your local vault and is never displayed.

+
+ + setSecret(e.target.value)} placeholder="nsec1... or 64-character hex" autoComplete="off" /> + {error && {error}} +
+
+ + +
+
+
; +} diff --git a/src/profiles.rs b/src/profiles.rs index bf0f43a..9e03a98 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -136,6 +136,16 @@ pub fn import_profile( }; let created_at = unix_timestamp()?; let metadata = fetch_profile_metadata(&public_key, &relays::enabled_urls(settings)); + let label = if label.trim().is_empty() { + metadata + .as_ref() + .and_then(|m| m.display_name.as_deref().or(m.name.as_deref())) + .filter(|name| !name.trim().is_empty()) + .map(str::to_string) + .unwrap_or_else(|| shorten_npub(&keys.public_key())) + } else { + label + }; let is_active = vault.active_profile.is_none(); if is_active { vault.active_profile = Some(public_key.clone()); @@ -520,6 +530,13 @@ fn fetch_profile_metadata(npub: &str, relay_urls: &[String]) -> Option .flatten() } +fn shorten_npub(public_key: &PublicKey) -> String { + public_key + .to_bech32() + .map(|npub| format!("{}…{}", &npub[..10], &npub[npub.len() - 6..])) + .unwrap_or_else(|_| "Imported account".to_string()) +} + async fn publish_metadata_async( keys: &Keys, label: &str, @@ -786,6 +803,22 @@ mod tests { assert_eq!(vault.active_profile.as_deref(), Some("npub1alice")); } + #[test] + fn import_profile_allows_empty_label() { + let mut vault = Vault::empty(); + let keys = Keys::generate(); + let summary = import_profile( + &mut vault, + String::new(), + &keys.secret_key().to_bech32().unwrap(), + None, + &offline_settings(), + ) + .unwrap(); + assert!(!summary.label.is_empty()); + assert_eq!(summary.npub, keys.public_key().to_bech32().unwrap()); + } + #[test] fn summaries_never_include_secret_keys() { let vault = populated_vault();