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:
+1
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
+37
-17
@@ -247,23 +247,26 @@ class AccountManager internal constructor(
|
||||
}
|
||||
|
||||
private suspend fun loadInternalAccount(npub: String): Result<AccountState.LoggedIn> {
|
||||
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)
|
||||
|
||||
+3
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
+4
-3
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user