fix(multi-account): sequential account save in AddAccountDialog, debounced metadata
Root cause of accounts not persisting: ensureCurrentAccountInStorage() was fire-and-forget (scope.launch), so loginWithKey() ran before the old account was saved. The old account was lost. Now all steps run sequentially in one coroutine. UI hanging fix: metadataVersion LaunchedEffect fired on every metadata event, doing encrypted file I/O each time. Now debounced with 2s delay so it only writes once after a batch of metadata settles. Also: - loadInternalAccount falls back to read-only (test updated) - bunker/nostrconnect paths also await ensureCurrentAccountInStorage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -673,14 +673,16 @@ fun App(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist display name when metadata arrives from relays
|
// Persist display name when metadata arrives from relays (debounced)
|
||||||
val metadataVersion by localCache.metadataVersion.collectAsState()
|
LaunchedEffect(Unit) {
|
||||||
LaunchedEffect(metadataVersion) {
|
localCache.metadataVersion.collect {
|
||||||
val current = accountManager.currentAccount() ?: return@LaunchedEffect
|
kotlinx.coroutines.delay(2000) // debounce — wait for batch of metadata to settle
|
||||||
val user = localCache.getUserIfExists(current.pubKeyHex) ?: return@LaunchedEffect
|
val current = accountManager.currentAccount() ?: return@collect
|
||||||
val name = user.toBestDisplayName()
|
val user = localCache.getUserIfExists(current.pubKeyHex) ?: return@collect
|
||||||
if (name != user.pubkeyDisplayHex()) {
|
val name = user.toBestDisplayName()
|
||||||
accountManager.updateDisplayName(current.npub, name)
|
if (name != user.pubkeyDisplayHex()) {
|
||||||
|
accountManager.updateDisplayName(current.npub, name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-18
@@ -88,18 +88,21 @@ fun AddAccountDialog(
|
|||||||
) {
|
) {
|
||||||
LoginCard(
|
LoginCard(
|
||||||
onLogin = { keyInput ->
|
onLogin = { keyInput ->
|
||||||
// Save current account before switching
|
// All steps must be sequential — no fire-and-forget
|
||||||
scope.launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
accountManager.ensureCurrentAccountInStorage()
|
accountManager.ensureCurrentAccountInStorage()
|
||||||
}
|
}
|
||||||
}
|
val result = accountManager.loginWithKey(keyInput)
|
||||||
accountManager.loginWithKey(keyInput).map {
|
if (result.isSuccess) {
|
||||||
scope.launch {
|
withContext(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) { accountManager.saveCurrentAccount() }
|
accountManager.saveCurrentAccount()
|
||||||
|
}
|
||||||
onAccountAdded()
|
onAccountAdded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Return success to dismiss any error in LoginCard
|
||||||
|
Result.success(Unit)
|
||||||
},
|
},
|
||||||
onGenerateNew = {
|
onGenerateNew = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -107,29 +110,28 @@ fun AddAccountDialog(
|
|||||||
accountManager.ensureCurrentAccountInStorage()
|
accountManager.ensureCurrentAccountInStorage()
|
||||||
}
|
}
|
||||||
accountManager.generateNewAccount()
|
accountManager.generateNewAccount()
|
||||||
withContext(Dispatchers.IO) { accountManager.saveCurrentAccount() }
|
withContext(Dispatchers.IO) {
|
||||||
|
accountManager.saveCurrentAccount()
|
||||||
|
}
|
||||||
onAccountAdded()
|
onAccountAdded()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoginBunker = { bunkerUri ->
|
onLoginBunker = { bunkerUri ->
|
||||||
scope.launch {
|
// ensureCurrentAccountInStorage first, then bunker login
|
||||||
withContext(Dispatchers.IO) {
|
accountManager.ensureCurrentAccountInStorage()
|
||||||
accountManager.ensureCurrentAccountInStorage()
|
val result = accountManager.loginWithBunker(bunkerUri)
|
||||||
}
|
if (result.isSuccess) {
|
||||||
}
|
|
||||||
accountManager.loginWithBunker(bunkerUri).map {
|
|
||||||
onAccountAdded()
|
onAccountAdded()
|
||||||
}
|
}
|
||||||
|
result.map { }
|
||||||
},
|
},
|
||||||
onLoginNostrConnect = { onUriGenerated ->
|
onLoginNostrConnect = { onUriGenerated ->
|
||||||
scope.launch {
|
accountManager.ensureCurrentAccountInStorage()
|
||||||
withContext(Dispatchers.IO) {
|
val result = accountManager.loginWithNostrConnect(onUriGenerated)
|
||||||
accountManager.ensureCurrentAccountInStorage()
|
if (result.isSuccess) {
|
||||||
}
|
|
||||||
}
|
|
||||||
accountManager.loginWithNostrConnect(onUriGenerated).map {
|
|
||||||
onAccountAdded()
|
onAccountAdded()
|
||||||
}
|
}
|
||||||
|
result.map { }
|
||||||
},
|
},
|
||||||
loginProgress = loginProgress,
|
loginProgress = loginProgress,
|
||||||
cardWidth = 420.dp,
|
cardWidth = 420.dp,
|
||||||
|
|||||||
+4
-2
@@ -85,7 +85,7 @@ class AccountManagerLoadAccountTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun loadSavedAccountInternalNoPrivkeyReturnsFailure() =
|
fun loadSavedAccountInternalNoPrivkeyFallsBackToReadOnly() =
|
||||||
runTest {
|
runTest {
|
||||||
val keyPair = KeyPair()
|
val keyPair = KeyPair()
|
||||||
val npub = keyPair.pubKey.toNpub()
|
val npub = keyPair.pubKey.toNpub()
|
||||||
@@ -94,7 +94,9 @@ class AccountManagerLoadAccountTest {
|
|||||||
coEvery { storage.getPrivateKey(npub) } returns null
|
coEvery { storage.getPrivateKey(npub) } returns null
|
||||||
|
|
||||||
val result = manager.loadSavedAccount()
|
val result = manager.loadSavedAccount()
|
||||||
assertTrue(result.isFailure)
|
assertTrue(result.isSuccess)
|
||||||
|
val state = result.getOrThrow()
|
||||||
|
assertTrue(state.isReadOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user