From f5a48d3c0c05e116fdd7e108866623f8d8539cf1 Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Tue, 25 Aug 2026 16:46:13 -0300 Subject: [PATCH 1/3] regression: dedupe deleted thread message --- .../client/lib/utils/threadMessageUtils.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/lib/utils/threadMessageUtils.ts b/apps/meteor/client/lib/utils/threadMessageUtils.ts index b849972e00154..369a3b459b074 100644 --- a/apps/meteor/client/lib/utils/threadMessageUtils.ts +++ b/apps/meteor/client/lib/utils/threadMessageUtils.ts @@ -54,6 +54,26 @@ export type ThreadMessagesPage = { export type ThreadMessagesInfiniteData = InfiniteData; +const dedupeThreadMessagesById = (messages: IThreadMessage[]): IThreadMessage[] => { + const byId = new Map(); + + for (const message of messages) { + const existing = byId.get(message._id); + if (!existing) { + byId.set(message._id, message); + continue; + } + + const messageTime = new Date(message._updatedAt ?? message.ts).getTime(); + const existingTime = new Date(existing._updatedAt ?? existing.ts).getTime(); + if (messageTime >= existingTime) { + byId.set(message._id, message); + } + } + + return Array.from(byId.values()); +}; + export const mutateThreadMessagesInfiniteData = ( client: QueryClient, queryKey: readonly unknown[], @@ -64,7 +84,7 @@ export const mutateThreadMessagesInfiniteData = ( return old; } - const items = old.pages.flatMap((page) => page.items); + const items = dedupeThreadMessagesById(old.pages.flatMap((page) => page.items)); const originalPageLengths = old.pages.map((page) => page.items.length); const oldTotal = old.pages.at(-1)?.itemCount ?? 0; const oldIds = new Set(items.map((message) => message._id)); From f718ab1ca1094af8ccd282c8b0197725a236f499 Mon Sep 17 00:00:00 2001 From: juliajforesti Date: Tue, 25 Aug 2026 17:00:44 -0300 Subject: [PATCH 2/3] test: add unit tests --- .../lib/utils/threadMessageUtils.spec.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/apps/meteor/client/lib/utils/threadMessageUtils.spec.ts b/apps/meteor/client/lib/utils/threadMessageUtils.spec.ts index f958af9f7f929..5977734bc16c9 100644 --- a/apps/meteor/client/lib/utils/threadMessageUtils.spec.ts +++ b/apps/meteor/client/lib/utils/threadMessageUtils.spec.ts @@ -144,6 +144,51 @@ describe('mutateThreadMessagesInfiniteData', () => { expect(data?.pages[0].itemCount).toBe(200); expect(data?.pageParams).toEqual([75]); }); + + it('removes a message from every cached page when its id was duplicated across pages, not just the first occurrence', () => { + const cache: ThreadMessagesInfiniteData = { + pages: [ + { items: [createMessage('reply-058', 58), createMessage('reply-059', 59), createMessage('reply-060', 60)], itemCount: 200 }, + { items: [createMessage('reply-060', 60), createMessage('reply-061', 61)], itemCount: 200 }, + ], + pageParams: [58, 60], + }; + queryClient.setQueryData(queryKey, cache); + + mutateThreadMessagesInfiniteData(queryClient, queryKey, (messages) => { + const index = messages.findIndex((m) => m._id === 'reply-060'); + messages.splice(index, 1); + }); + + const data = queryClient.getQueryData(queryKey); + const allIds = data?.pages.flatMap((page) => page.items.map((m) => m._id)) ?? []; + + expect(allIds).not.toContain('reply-060'); + expect(data?.pages[0].itemCount).toBe(199); + }); + + it('collapses a duplicated id into a single entry, keeping the most recently updated copy, even when the mutation is a no-op', () => { + const stale = createMessage('reply-060', 60); + const fresh: IThreadMessage = { ...createMessage('reply-060', 60), msg: 'edited', _updatedAt: new Date(9999).toISOString() as any }; + const cache: ThreadMessagesInfiniteData = { + pages: [ + { items: [stale], itemCount: 200 }, + { items: [fresh], itemCount: 200 }, + ], + pageParams: [60, 60], + }; + queryClient.setQueryData(queryKey, cache); + + mutateThreadMessagesInfiniteData(queryClient, queryKey, () => { + // no-op mutation: only the dedup pass should change the cache + }); + + const data = queryClient.getQueryData(queryKey); + const allItems = data?.pages.flatMap((page) => page.items) ?? []; + + expect(allItems).toHaveLength(1); + expect(allItems[0].msg).toBe('edited'); + }); }); describe('upsertThreadMessageInCache', () => { From cc0490a47e97767128165ee25e344059f9ac5ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:55:34 -0300 Subject: [PATCH 3/3] Update apps/meteor/client/lib/utils/threadMessageUtils.ts Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- apps/meteor/client/lib/utils/threadMessageUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/lib/utils/threadMessageUtils.ts b/apps/meteor/client/lib/utils/threadMessageUtils.ts index 369a3b459b074..967c3f278d39d 100644 --- a/apps/meteor/client/lib/utils/threadMessageUtils.ts +++ b/apps/meteor/client/lib/utils/threadMessageUtils.ts @@ -66,7 +66,7 @@ const dedupeThreadMessagesById = (messages: IThreadMessage[]): IThreadMessage[] const messageTime = new Date(message._updatedAt ?? message.ts).getTime(); const existingTime = new Date(existing._updatedAt ?? existing.ts).getTime(); - if (messageTime >= existingTime) { + if (messageTime > existingTime) { byId.set(message._id, message); } }