fix(multi-account): persist display names in encrypted storage, fix npub-only fallback

Three fixes:

1. Display names now stored in AccountInfo/AccountInfoDto and persisted
   in encrypted storage. Populated when metadata arrives from relays via
   metadataVersion LaunchedEffect. Available immediately on next open.

2. loadInternalAccount falls back to loadReadOnlyAccount when no private
   key found in SecureKeyStorage — fixes npub-only accounts that were
   saved as SignerType.Internal from earlier sessions.

3. Dropdown uses persisted displayName first, cache lookup as fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-04-29 09:54:08 +03:00
parent 1032db0c05
commit 156391ec0a
5 changed files with 56 additions and 20 deletions
@@ -30,5 +30,6 @@ import androidx.compose.runtime.Immutable
data class AccountInfo( data class AccountInfo(
val npub: String, val npub: String,
val signerType: SignerType, val signerType: SignerType,
val displayName: String? = null,
val isTransient: Boolean = false, val isTransient: Boolean = false,
) )
@@ -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 // Try to load saved account on startup
DisposableEffect(Unit) { DisposableEffect(Unit) {
relayManager.addDefaultRelays() relayManager.addDefaultRelays()
@@ -247,23 +247,26 @@ class AccountManager internal constructor(
} }
private suspend fun loadInternalAccount(npub: String): Result<AccountState.LoggedIn> { private suspend fun loadInternalAccount(npub: String): Result<AccountState.LoggedIn> {
val privKeyHex = val privKeyHex = secureStorage.getPrivateKey(npub)
secureStorage.getPrivateKey(npub)
?: return Result.failure(Exception("Private key not found for $npub"))
val keyPair = KeyPair(privKey = privKeyHex.hexToByteArray()) if (privKeyHex != null) {
val signer = NostrSignerInternal(keyPair) val keyPair = KeyPair(privKey = privKeyHex.hexToByteArray())
val signer = NostrSignerInternal(keyPair)
val state = val state =
AccountState.LoggedIn( AccountState.LoggedIn(
signer = signer, signer = signer,
pubKeyHex = keyPair.pubKey.toHexKey(), pubKeyHex = keyPair.pubKey.toHexKey(),
npub = keyPair.pubKey.toNpub(), npub = keyPair.pubKey.toNpub(),
nsec = keyPair.privKey?.toNsec(), nsec = keyPair.privKey?.toNsec(),
isReadOnly = false, isReadOnly = false,
) )
_accountState.value = state _accountState.value = state
return Result.success(state) return Result.success(state)
}
// No private key — fall back to read-only
return loadReadOnlyAccount(npub)
} }
private suspend fun loadBunkerAccount( private suspend fun loadBunkerAccount(
@@ -663,13 +666,30 @@ class AccountManager internal constructor(
return Result.success(state) return Result.success(state)
} }
suspend fun ensureCurrentAccountInStorage() { suspend fun ensureCurrentAccountInStorage(displayName: String? = null) {
val current = currentAccount() ?: return 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.saveAccount(info)
accountStorage.setCurrentAccount(current.npub) 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) { suspend fun removeAccountFromStorage(npub: String) {
val current = currentAccount() val current = currentAccount()
accountStorage.deleteAccount(npub) accountStorage.deleteAccount(npub)
@@ -198,6 +198,7 @@ internal data class AccountInfoDto(
val npub: String, val npub: String,
val signerKind: String, // "internal", "remote", "viewonly" val signerKind: String, // "internal", "remote", "viewonly"
val bunkerUri: String? = null, val bunkerUri: String? = null,
val displayName: String? = null,
val isTransient: Boolean = false, val isTransient: Boolean = false,
) { ) {
fun toAccountInfo(): AccountInfo = fun toAccountInfo(): AccountInfo =
@@ -209,6 +210,7 @@ internal data class AccountInfoDto(
"viewonly" -> SignerType.ViewOnly "viewonly" -> SignerType.ViewOnly
else -> SignerType.Internal else -> SignerType.Internal
}, },
displayName = displayName,
isTransient = isTransient, isTransient = isTransient,
) )
@@ -223,6 +225,7 @@ internal data class AccountInfoDto(
is SignerType.ViewOnly -> "viewonly" is SignerType.ViewOnly -> "viewonly"
}, },
bunkerUri = (info.signerType as? SignerType.Remote)?.bunkerUri, bunkerUri = (info.signerType as? SignerType.Remote)?.bunkerUri,
displayName = info.displayName,
isTransient = info.isTransient, isTransient = info.isTransient,
) )
} }
@@ -122,8 +122,8 @@ fun AccountSwitcherDropdown(
allAccounts.forEach { account -> allAccounts.forEach { account ->
val isActive = account.npub == activeNpub val isActive = account.npub == activeNpub
// Resolve display name from cache (no remember — cheap lookup, re-evaluates each recomposition) // Use persisted display name first, fall back to cache lookup
val displayName = resolveDisplayName(account.npub, localCache) val displayName = account.displayName ?: resolveDisplayName(account.npub, localCache)
val shortNpub = npubShortMiddle(account.npub) val shortNpub = npubShortMiddle(account.npub)
val signerLabel = val signerLabel =
when (account.signerType) { when (account.signerType) {
@@ -210,7 +210,8 @@ fun AccountSwitcherDropdown(
// Logout confirmation dialog // Logout confirmation dialog
val logoutNpub = confirmLogoutNpub val logoutNpub = confirmLogoutNpub
if (logoutNpub != null) { if (logoutNpub != null) {
val logoutDisplayName = resolveDisplayName(logoutNpub, localCache) val logoutAccount = allAccounts.find { it.npub == logoutNpub }
val logoutDisplayName = logoutAccount?.displayName ?: resolveDisplayName(logoutNpub, localCache)
AlertDialog( AlertDialog(
onDismissRequest = { confirmLogoutNpub = null }, onDismissRequest = { confirmLogoutNpub = null },