diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/account/AccountInfo.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/account/AccountInfo.kt index 25454c7d9..cecdfe6ca 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/account/AccountInfo.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/account/AccountInfo.kt @@ -30,5 +30,6 @@ import androidx.compose.runtime.Immutable data class AccountInfo( val npub: String, val signerType: SignerType, + val displayName: String? = null, val isTransient: Boolean = false, ) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index caa084ccd..2265b86b5 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -673,6 +673,17 @@ fun App( } } + // Persist display name when metadata arrives from relays + val metadataVersion by localCache.metadataVersion.collectAsState() + LaunchedEffect(metadataVersion) { + val current = accountManager.currentAccount() ?: return@LaunchedEffect + val user = localCache.getUserIfExists(current.pubKeyHex) ?: return@LaunchedEffect + val name = user.toBestDisplayName() + if (name != user.pubkeyDisplayHex()) { + accountManager.updateDisplayName(current.npub, name) + } + } + // Try to load saved account on startup DisposableEffect(Unit) { relayManager.addDefaultRelays() diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt index 17626a82e..eb5e7b2ab 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt @@ -247,23 +247,26 @@ class AccountManager internal constructor( } private suspend fun loadInternalAccount(npub: String): Result { - val privKeyHex = - secureStorage.getPrivateKey(npub) - ?: return Result.failure(Exception("Private key not found for $npub")) + val privKeyHex = secureStorage.getPrivateKey(npub) - val keyPair = KeyPair(privKey = privKeyHex.hexToByteArray()) - val signer = NostrSignerInternal(keyPair) + if (privKeyHex != null) { + val keyPair = KeyPair(privKey = privKeyHex.hexToByteArray()) + val signer = NostrSignerInternal(keyPair) - val state = - AccountState.LoggedIn( - signer = signer, - pubKeyHex = keyPair.pubKey.toHexKey(), - npub = keyPair.pubKey.toNpub(), - nsec = keyPair.privKey?.toNsec(), - isReadOnly = false, - ) - _accountState.value = state - return Result.success(state) + val state = + AccountState.LoggedIn( + signer = signer, + pubKeyHex = keyPair.pubKey.toHexKey(), + npub = keyPair.pubKey.toNpub(), + nsec = keyPair.privKey?.toNsec(), + isReadOnly = false, + ) + _accountState.value = state + return Result.success(state) + } + + // No private key — fall back to read-only + return loadReadOnlyAccount(npub) } private suspend fun loadBunkerAccount( @@ -663,13 +666,30 @@ class AccountManager internal constructor( return Result.success(state) } - suspend fun ensureCurrentAccountInStorage() { + suspend fun ensureCurrentAccountInStorage(displayName: String? = null) { val current = currentAccount() ?: return - val info = AccountInfo(npub = current.npub, signerType = current.signerType) + // Merge with existing stored info to preserve display name if not provided + val existing = accountStorage.loadAccounts().find { it.npub == current.npub } + val info = + AccountInfo( + npub = current.npub, + signerType = current.signerType, + displayName = displayName ?: existing?.displayName, + ) accountStorage.saveAccount(info) accountStorage.setCurrentAccount(current.npub) } + suspend fun updateDisplayName( + npub: String, + displayName: String, + ) { + val accounts = accountStorage.loadAccounts() + val existing = accounts.find { it.npub == npub } ?: return + accountStorage.saveAccount(existing.copy(displayName = displayName)) + refreshAccountList() + } + suspend fun removeAccountFromStorage(npub: String) { val current = currentAccount() accountStorage.deleteAccount(npub) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/DesktopAccountStorage.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/DesktopAccountStorage.kt index 03e5d0a64..51d6c2b84 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/DesktopAccountStorage.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/DesktopAccountStorage.kt @@ -198,6 +198,7 @@ internal data class AccountInfoDto( val npub: String, val signerKind: String, // "internal", "remote", "viewonly" val bunkerUri: String? = null, + val displayName: String? = null, val isTransient: Boolean = false, ) { fun toAccountInfo(): AccountInfo = @@ -209,6 +210,7 @@ internal data class AccountInfoDto( "viewonly" -> SignerType.ViewOnly else -> SignerType.Internal }, + displayName = displayName, isTransient = isTransient, ) @@ -223,6 +225,7 @@ internal data class AccountInfoDto( is SignerType.ViewOnly -> "viewonly" }, bunkerUri = (info.signerType as? SignerType.Remote)?.bunkerUri, + displayName = info.displayName, isTransient = info.isTransient, ) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/account/AccountSwitcherDropdown.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/account/AccountSwitcherDropdown.kt index 2aace03a2..ee8bb2cff 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/account/AccountSwitcherDropdown.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/account/AccountSwitcherDropdown.kt @@ -122,8 +122,8 @@ fun AccountSwitcherDropdown( allAccounts.forEach { account -> val isActive = account.npub == activeNpub - // Resolve display name from cache (no remember — cheap lookup, re-evaluates each recomposition) - val displayName = resolveDisplayName(account.npub, localCache) + // Use persisted display name first, fall back to cache lookup + val displayName = account.displayName ?: resolveDisplayName(account.npub, localCache) val shortNpub = npubShortMiddle(account.npub) val signerLabel = when (account.signerType) { @@ -210,7 +210,8 @@ fun AccountSwitcherDropdown( // Logout confirmation dialog val logoutNpub = confirmLogoutNpub if (logoutNpub != null) { - val logoutDisplayName = resolveDisplayName(logoutNpub, localCache) + val logoutAccount = allAccounts.find { it.npub == logoutNpub } + val logoutDisplayName = logoutAccount?.displayName ?: resolveDisplayName(logoutNpub, localCache) AlertDialog( onDismissRequest = { confirmLogoutNpub = null },