From f7657e3adb0cdadcb29064439ba526ebe7b1d20e Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sat, 21 Mar 2026 22:46:01 +0100 Subject: [PATCH] fix: prevent duplicate messages when sending image without text Optimistic message used content: '' while the server normalized it to null, causing the content-based dedup to fail and leaving both the empty temp message and the real attachment message in the list. --- packages/web/src/stores/chatStore.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/web/src/stores/chatStore.ts b/packages/web/src/stores/chatStore.ts index fc16003a..d3e90486 100644 --- a/packages/web/src/stores/chatStore.ts +++ b/packages/web/src/stores/chatStore.ts @@ -261,7 +261,7 @@ export const useChatStore = create((set, get) => ({ id: tempId, channelId: isDm ? '' : channelId, userId: currentUser.id, - content, + content: content || null, replyToId: replyToId ?? null, editedAt: null, createdAt: Date.now(), @@ -370,9 +370,10 @@ export const useChatStore = create((set, get) => ({ // Don't require userId match — for federated messages the home user ID // differs from the replicated user ID, but content match is sufficient // since temp messages are unique within the short optimistic window. + // Normalize both sides: empty string and null are equivalent (server stores null for empty content). const filtered = current.filter(m => { if (!m.id.startsWith('temp_')) return true; - return m.content !== normalizedMessage.content; + return (m.content || null) !== (normalizedMessage.content || null); }); let updated = [...filtered, normalizedMessage]; // Cap per-channel messages to prevent memory growth @@ -392,10 +393,11 @@ export const useChatStore = create((set, get) => ({ // Avoid duplicates if (current.find(m => m.id === normalizedMessage.id)) return state; // Remove any optimistic temp message with same content (no userId check — - // federated messages arrive with a different replicated user ID) + // federated messages arrive with a different replicated user ID). + // Normalize both sides: empty string and null are equivalent (server stores null for empty content). const filtered = current.filter(m => { if (!m.id.startsWith('temp_')) return true; - return m.content !== normalizedMessage.content; + return (m.content || null) !== (normalizedMessage.content || null); }); let updated = [...filtered, normalizedMessage]; // Cap per-channel messages to prevent memory growth