From 1e6e0eefb85ccc5dcbf884aa1eb75c09eb10f068 Mon Sep 17 00:00:00 2001 From: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Date: Sun, 21 Jun 2026 09:58:24 -0400 Subject: [PATCH] desktop: cancel in-flight channel fetch before optimistic reaction write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reaction onMutate snapshotted the cache and wrote the optimistic kind:7 synchronously, but did not cancel an in-flight useChannelMessages fetch first. That queryFn ends with setQueryData(queryKey, mergedHistory) built from a pre-click snapshot, so a fetch resolving just after the click would clobber the optimistic reaction until the next backfill — the same race the sibling send-message onMutate already guards against. Make onMutate async and await queryClient.cancelQueries({ queryKey }) before snapshotting, matching the send-message mutation. Narrow but real; no behavior change when no fetch is in flight (cancelQueries is a no-op). Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- desktop/src/features/messages/hooks.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/desktop/src/features/messages/hooks.ts b/desktop/src/features/messages/hooks.ts index 411d476cf5..3d07f40834 100644 --- a/desktop/src/features/messages/hooks.ts +++ b/desktop/src/features/messages/hooks.ts @@ -519,13 +519,17 @@ export function useToggleReactionMutation() { // channel cache ourselves, optimistically, or the reactor's own reaction // stays invisible until the next channel switch. Mirrors the apply-on- // success cache writes in the edit/delete mutations below. - onMutate: ({ eventId, emoji, remove, channelId }) => { + onMutate: async ({ eventId, emoji, remove, channelId }) => { const pubkey = identityQuery.data?.pubkey; if (!channelId || !pubkey) { return undefined; } const queryKey = channelMessagesKey(channelId); + // Cancel any in-flight channel fetch so its post-write setQueryData + // can't clobber the optimistic reaction with a pre-write snapshot + // (matches the send-message onMutate below). + await queryClient.cancelQueries({ queryKey }); const previous = queryClient.getQueryData(queryKey) ?? []; const pubkeyLower = pubkey.toLowerCase(); const trimmedEmoji = emoji.trim();