From 586346a68f59dfc32dfbed5ebbefed3978f708b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 23:58:13 +0000 Subject: [PATCH] fix: keep bottom-bar edits ordered to prevent a stale revert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01MJiPHArXZ7P5EvZa7GN9fP --- .../com/vitorpamplona/amethyst/model/Account.kt | 15 +++++++++------ .../ui/screen/loggedIn/AccountViewModel.kt | 11 ++++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index f8e7233486..e43d7fba6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1025,11 +1025,14 @@ class Account( } } - suspend fun changeBottomBarItems(items: List) { - 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): 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index dec68a0ded..286030e4f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1937,10 +1937,15 @@ class AccountViewModel( fun bottomBarItemsFlow(): StateFlow> = account.settings.syncedSettings.navigation.bottomBarItems - fun changeBottomBarItems(items: List) = - launchSigner { - account.changeBottomBarItems(items) + fun changeBottomBarItems(items: List) { + // 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> = account.settings.syncedSettings.chats.pinnedChatrooms