fix(multi-account): npub-only switching + reactive display names
- Add loadReadOnlyAccount() for ViewOnly signer type switching (was mapping to loadInternalAccount which requires private key) - Remove remember() from display name resolution — now re-evaluates each recomposition so names appear once metadata loads from relays - Only show subtitle (npub row) when display name is available; if no display name, show npub as title only - Signer type badge shown as subtitle when no display name Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+22
-1
@@ -641,6 +641,27 @@ class AccountManager internal constructor(
|
|||||||
* Ensures the currently logged-in account is persisted in multi-account storage.
|
* Ensures the currently logged-in account is persisted in multi-account storage.
|
||||||
* Call before switching to a new account to avoid losing the current one.
|
* Call before switching to a new account to avoid losing the current one.
|
||||||
*/
|
*/
|
||||||
|
private fun loadReadOnlyAccount(npub: String): Result<AccountState.LoggedIn> {
|
||||||
|
val pubKeyHex =
|
||||||
|
decodePublicKeyAsHexOrNull(npub)
|
||||||
|
?: return Result.failure(Exception("Invalid npub: $npub"))
|
||||||
|
|
||||||
|
val keyPair = KeyPair(pubKey = pubKeyHex.hexToByteArray())
|
||||||
|
val signer = NostrSignerInternal(keyPair)
|
||||||
|
|
||||||
|
val state =
|
||||||
|
AccountState.LoggedIn(
|
||||||
|
signer = signer,
|
||||||
|
pubKeyHex = pubKeyHex,
|
||||||
|
npub = npub,
|
||||||
|
nsec = null,
|
||||||
|
isReadOnly = true,
|
||||||
|
signerType = SignerType.ViewOnly,
|
||||||
|
)
|
||||||
|
_accountState.value = state
|
||||||
|
return Result.success(state)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun ensureCurrentAccountInStorage() {
|
suspend fun ensureCurrentAccountInStorage() {
|
||||||
val current = currentAccount() ?: return
|
val current = currentAccount() ?: return
|
||||||
val info = AccountInfo(npub = current.npub, signerType = current.signerType)
|
val info = AccountInfo(npub = current.npub, signerType = current.signerType)
|
||||||
@@ -688,7 +709,7 @@ class AccountManager internal constructor(
|
|||||||
when (sType) {
|
when (sType) {
|
||||||
is SignerType.Internal -> loadInternalAccount(target.npub)
|
is SignerType.Internal -> loadInternalAccount(target.npub)
|
||||||
is SignerType.Remote -> loadBunkerAccount(sType.bunkerUri, target.npub)
|
is SignerType.Remote -> loadBunkerAccount(sType.bunkerUri, target.npub)
|
||||||
is SignerType.ViewOnly -> loadInternalAccount(target.npub)
|
is SignerType.ViewOnly -> loadReadOnlyAccount(target.npub)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newState.isFailure) return newState
|
if (newState.isFailure) return newState
|
||||||
|
|||||||
+45
-35
@@ -59,6 +59,21 @@ import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
|||||||
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
|
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve display name from cache. Returns null if no metadata or name matches hex fallback.
|
||||||
|
*/
|
||||||
|
private fun resolveDisplayName(
|
||||||
|
npub: String,
|
||||||
|
localCache: DesktopLocalCache?,
|
||||||
|
): String? {
|
||||||
|
if (localCache == null) return null
|
||||||
|
val pubkeyHex = decodePublicKeyAsHexOrNull(npub) ?: return null
|
||||||
|
val user = localCache.getUserIfExists(pubkeyHex) ?: return null
|
||||||
|
val name = user.toBestDisplayName()
|
||||||
|
// Don't return hex fallback as "display name"
|
||||||
|
return name.takeIf { it != user.pubkeyDisplayHex() }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middle-truncates an npub: "npub1abcdef...wxyz"
|
* Middle-truncates an npub: "npub1abcdef...wxyz"
|
||||||
*/
|
*/
|
||||||
@@ -102,44 +117,47 @@ fun AccountSwitcherDropdown(
|
|||||||
allAccounts.forEach { account ->
|
allAccounts.forEach { account ->
|
||||||
val isActive = account.npub == activeNpub
|
val isActive = account.npub == activeNpub
|
||||||
|
|
||||||
// Resolve display name from cache
|
// Resolve display name from cache (no remember — cheap lookup, re-evaluates each recomposition)
|
||||||
val displayName =
|
val displayName = resolveDisplayName(account.npub, localCache)
|
||||||
remember(account.npub, localCache) {
|
val shortNpub = npubShortMiddle(account.npub)
|
||||||
val pubkeyHex = decodePublicKeyAsHexOrNull(account.npub)
|
val signerLabel =
|
||||||
if (pubkeyHex != null && localCache != null) {
|
when (account.signerType) {
|
||||||
val user = localCache.getUserIfExists(pubkeyHex)
|
is SignerType.Remote -> " · Bunker"
|
||||||
user?.toBestDisplayName()?.takeIf { it != user.pubkeyDisplayHex() }
|
is SignerType.ViewOnly -> " · View only"
|
||||||
} else {
|
is SignerType.Internal -> ""
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
// Row 1: Display name (or npub if no name)
|
// Title: display name or npub
|
||||||
Text(
|
Text(
|
||||||
displayName ?: npubShortMiddle(account.npub),
|
displayName ?: shortNpub,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
fontWeight = if (isActive) FontWeight.SemiBold else FontWeight.Normal,
|
fontWeight = if (isActive) FontWeight.SemiBold else FontWeight.Normal,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
// Row 2: npub (middle-truncated) + signer type badge
|
// Subtitle: only show if different from title
|
||||||
val signerLabel =
|
val subtitle = shortNpub + signerLabel
|
||||||
when (account.signerType) {
|
if (displayName != null) {
|
||||||
is SignerType.Remote -> " · Bunker"
|
Text(
|
||||||
is SignerType.ViewOnly -> " · View only"
|
subtitle,
|
||||||
is SignerType.Internal -> ""
|
maxLines = 1,
|
||||||
}
|
overflow = TextOverflow.Ellipsis,
|
||||||
Text(
|
style = MaterialTheme.typography.labelSmall,
|
||||||
npubShortMiddle(account.npub) + signerLabel,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
maxLines = 1,
|
)
|
||||||
overflow = TextOverflow.Ellipsis,
|
} else if (signerLabel.isNotEmpty()) {
|
||||||
style = MaterialTheme.typography.labelSmall,
|
// No display name but has signer label — show it
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
Text(
|
||||||
)
|
signerLabel.removePrefix(" · "),
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -187,15 +205,7 @@ fun AccountSwitcherDropdown(
|
|||||||
// Logout confirmation dialog
|
// Logout confirmation dialog
|
||||||
val logoutNpub = confirmLogoutNpub
|
val logoutNpub = confirmLogoutNpub
|
||||||
if (logoutNpub != null) {
|
if (logoutNpub != null) {
|
||||||
val logoutDisplayName =
|
val logoutDisplayName = resolveDisplayName(logoutNpub, localCache)
|
||||||
remember(logoutNpub, localCache) {
|
|
||||||
val hex = decodePublicKeyAsHexOrNull(logoutNpub)
|
|
||||||
if (hex != null && localCache != null) {
|
|
||||||
localCache.getUserIfExists(hex)?.toBestDisplayName()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { confirmLogoutNpub = null },
|
onDismissRequest = { confirmLogoutNpub = null },
|
||||||
|
|||||||
Reference in New Issue
Block a user