fix: keep bottom-bar edits ordered to prevent a stale revert

Persisting a bottom-bar edit went through `launchSigner { account.change... }`,
which dispatches on a multi-threaded pool. Because the reactive StateFlow emit
happened inside that coroutine, two quick edits (e.g. tapping Add on several
catalog rows) could complete out of order: the older list would win the flow,
and the settings screen — which re-seeds its editable list from the flow via
`LaunchedEffect(savedItems) { syncFrom(...) }` — would visibly revert the newer
edit and publish the stale list in the NIP-78 event.

Apply the change to the in-memory flow synchronously on the caller (UI) thread
via `Account.applyBottomBarItems` (a non-suspending emit + local save, both
non-blocking) so rapid edits stay strictly ordered, and run only the
sign/encrypt/publish off-thread. Restores the ordering guarantee the previous
synchronous `tryEmit` had.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJiPHArXZ7P5EvZa7GN9fP
This commit is contained in:
Claude
2026-07-27 23:58:13 +00:00
parent 7a41e21272
commit 586346a68f
2 changed files with 17 additions and 9 deletions
@@ -1025,11 +1025,14 @@ class Account(
}
}
suspend fun changeBottomBarItems(items: List<BottomBarEntry>) {
if (settings.changeBottomBarItems(items)) {
sendNewAppSpecificData()
}
}
/**
* Applies the new bottom-bar list to the reactive synced-settings flow and returns whether it
* changed. Non-suspending and only touches in-memory state, so callers invoke it synchronously on
* the UI thread rapid edits then stay strictly ordered instead of racing on the multi-threaded
* signer dispatcher (an out-of-order write would revert the newer edit, which the settings screen
* re-seeds from this flow). Pair a `true` result with [sendNewAppSpecificData] to publish.
*/
fun applyBottomBarItems(items: List<BottomBarEntry>): Boolean = settings.changeBottomBarItems(items)
suspend fun toggleChatroomPin(room: ChatroomKey) {
settings.toggleChatroomPin(room)
@@ -1082,7 +1085,7 @@ class Account(
sendNewAppSpecificData()
}
private suspend fun sendNewAppSpecificData() = sendMyPublicAndPrivateOutbox(appSpecific.saveNewAppSpecificData())
internal suspend fun sendNewAppSpecificData() = sendMyPublicAndPrivateOutbox(appSpecific.saveNewAppSpecificData())
// ---
// NIP-13 proof-of-work publishing
@@ -1937,10 +1937,15 @@ class AccountViewModel(
fun bottomBarItemsFlow(): StateFlow<List<BottomBarEntry>> = account.settings.syncedSettings.navigation.bottomBarItems
fun changeBottomBarItems(items: List<BottomBarEntry>) =
launchSigner {
account.changeBottomBarItems(items)
fun changeBottomBarItems(items: List<BottomBarEntry>) {
// Apply to the reactive flow synchronously on the caller (UI) thread so rapid edits stay
// ordered — launchSigner dispatches on a multi-threaded pool, so wrapping the emit too would
// let two quick edits complete out of order and revert the newer one (the settings screen
// re-seeds its editable list from this flow). Only the sign + encrypt + publish runs off-thread.
if (account.applyBottomBarItems(items)) {
launchSigner { account.sendNewAppSpecificData() }
}
}
fun pinnedChatroomsFlow(): StateFlow<Set<ChatroomKey>> = account.settings.syncedSettings.chats.pinnedChatrooms