From 4c115b684cf349f1c767d041bbb815a4151cad47 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 18 Dec 2025 09:23:02 -0500 Subject: [PATCH] feat: Enhance updateConvoInAllQueries to support moving conversations to the top --- client/src/hooks/SSE/useEventHandlers.ts | 4 +- client/src/utils/convos.spec.ts | 71 ++++++++++++++++++++++++ client/src/utils/convos.ts | 69 ++++++++++++++++++++--- 3 files changed, 134 insertions(+), 10 deletions(-) diff --git a/client/src/hooks/SSE/useEventHandlers.ts b/client/src/hooks/SSE/useEventHandlers.ts index bb4af5cdda..b4f983ecef 100644 --- a/client/src/hooks/SSE/useEventHandlers.ts +++ b/client/src/hooks/SSE/useEventHandlers.ts @@ -310,7 +310,7 @@ export default function useEventHandlers({ if (requestMessage.parentMessageId === Constants.NO_PARENT) { addConvoToAllQueries(queryClient, update); } else { - updateConvoInAllQueries(queryClient, update.conversationId!, (_c) => update); + updateConvoInAllQueries(queryClient, update.conversationId!, (_c) => update, true); } } else if (setConversation) { setConversation((prevState) => { @@ -385,7 +385,7 @@ export default function useEventHandlers({ if (parentMessageId === Constants.NO_PARENT) { addConvoToAllQueries(queryClient, update); } else { - updateConvoInAllQueries(queryClient, update.conversationId!, (_c) => update); + updateConvoInAllQueries(queryClient, update.conversationId!, (_c) => update, true); } } } else if (setConversation) { diff --git a/client/src/utils/convos.spec.ts b/client/src/utils/convos.spec.ts index 7bf94c33c6..c00cb20085 100644 --- a/client/src/utils/convos.spec.ts +++ b/client/src/utils/convos.spec.ts @@ -596,6 +596,77 @@ describe('Conversation Utilities', () => { expect(data!.pages[0].conversations[0].model).toBe('gpt-4'); }); + it('updateConvoInAllQueries with moveToTop moves convo to front and updates updatedAt', () => { + // Add more conversations so 'a' is not at position 0 + const convoC = { conversationId: 'c', updatedAt: '2024-01-03T12:00:00Z' } as TConversation; + queryClient.setQueryData(['allConversations'], { + pages: [{ conversations: [convoC, convoA], nextCursor: null }], + pageParams: [], + }); + + const before = new Date().toISOString(); + updateConvoInAllQueries(queryClient, 'a', (c) => ({ ...c, model: 'gpt-4' }), true); + const data = queryClient.getQueryData>(['allConversations']); + + // 'a' should now be at position 0 + expect(data!.pages[0].conversations[0].conversationId).toBe('a'); + expect(data!.pages[0].conversations[0].model).toBe('gpt-4'); + // updatedAt should be updated + expect( + new Date(data!.pages[0].conversations[0].updatedAt).getTime(), + ).toBeGreaterThanOrEqual(new Date(before).getTime()); + // 'c' should now be at position 1 + expect(data!.pages[0].conversations[1].conversationId).toBe('c'); + }); + + it('updateConvoInAllQueries with moveToTop from second page', () => { + const convoC = { conversationId: 'c', updatedAt: '2024-01-03T12:00:00Z' } as TConversation; + const convoD = { conversationId: 'd', updatedAt: '2024-01-04T12:00:00Z' } as TConversation; + queryClient.setQueryData(['allConversations'], { + pages: [ + { conversations: [convoC, convoD], nextCursor: 'cursor1' }, + { conversations: [convoA, convoB], nextCursor: null }, + ], + pageParams: [], + }); + + updateConvoInAllQueries(queryClient, 'a', (c) => ({ ...c, title: 'Updated' }), true); + const data = queryClient.getQueryData>(['allConversations']); + + // 'a' should now be at front of page 0 + expect(data!.pages[0].conversations[0].conversationId).toBe('a'); + expect(data!.pages[0].conversations[0].title).toBe('Updated'); + // Page 0 should have 3 conversations now + expect(data!.pages[0].conversations.length).toBe(3); + // Page 1 should have 1 conversation (only 'b' remains) + expect(data!.pages[1].conversations.length).toBe(1); + expect(data!.pages[1].conversations[0].conversationId).toBe('b'); + }); + + it('updateConvoInAllQueries with moveToTop when already at position 0 updates in place', () => { + const originalUpdatedAt = convoA.updatedAt; + updateConvoInAllQueries(queryClient, 'a', (c) => ({ ...c, model: 'gpt-4' }), true); + const data = queryClient.getQueryData>(['allConversations']); + + expect(data!.pages[0].conversations[0].conversationId).toBe('a'); + expect(data!.pages[0].conversations[0].model).toBe('gpt-4'); + // updatedAt should still be updated even when already at top + expect(data!.pages[0].conversations[0].updatedAt).not.toBe(originalUpdatedAt); + }); + + it('updateConvoInAllQueries with moveToTop returns original data if convo not found', () => { + const dataBefore = queryClient.getQueryData>(['allConversations']); + updateConvoInAllQueries( + queryClient, + 'nonexistent', + (c) => ({ ...c, model: 'gpt-4' }), + true, + ); + const dataAfter = queryClient.getQueryData>(['allConversations']); + + expect(dataAfter).toEqual(dataBefore); + }); + it('removeConvoFromAllQueries deletes conversation', () => { removeConvoFromAllQueries(queryClient, 'a'); const data = queryClient.getQueryData>(['allConversations']); diff --git a/client/src/utils/convos.ts b/client/src/utils/convos.ts index f60fab40a8..e92d75d2da 100644 --- a/client/src/utils/convos.ts +++ b/client/src/utils/convos.ts @@ -352,6 +352,7 @@ export function updateConvoInAllQueries( queryClient: QueryClient, conversationId: string, updater: (c: TConversation) => TConversation, + moveToTop = false, ) { const queries = queryClient .getQueryCache() @@ -362,15 +363,67 @@ export function updateConvoInAllQueries( if (!oldData) { return oldData; } - return { - ...oldData, - pages: oldData.pages.map((page) => ({ - ...page, - conversations: page.conversations.map((c) => - c.conversationId === conversationId ? updater(c) : c, + + // Find conversation location (single pass with early exit) + let pageIdx = -1; + let convoIdx = -1; + for (let pi = 0; pi < oldData.pages.length; pi++) { + const ci = oldData.pages[pi].conversations.findIndex( + (c) => c.conversationId === conversationId, + ); + if (ci !== -1) { + pageIdx = pi; + convoIdx = ci; + break; + } + } + + if (pageIdx === -1) { + return oldData; + } + + const found = oldData.pages[pageIdx].conversations[convoIdx]; + const updated = moveToTop + ? { ...updater(found), updatedAt: new Date().toISOString() } + : updater(found); + + // If not moving to top, or already at top of page 0, update in place + if (!moveToTop || (pageIdx === 0 && convoIdx === 0)) { + return { + ...oldData, + pages: oldData.pages.map((page, pi) => + pi === pageIdx + ? { + ...page, + conversations: page.conversations.map((c, ci) => (ci === convoIdx ? updated : c)), + } + : page, ), - })), - }; + }; + } + + // Move to top: only modify affected pages + const newPages = oldData.pages.map((page, pi) => { + if (pi === 0 && pageIdx === 0) { + // Source is page 0: remove from current position, add to front + const convos = page.conversations.filter((_, ci) => ci !== convoIdx); + return { ...page, conversations: [updated, ...convos] }; + } + if (pi === 0) { + // Add to front of page 0 + return { ...page, conversations: [updated, ...page.conversations] }; + } + if (pi === pageIdx) { + // Remove from source page + return { + ...page, + conversations: page.conversations.filter((_, ci) => ci !== convoIdx), + }; + } + return page; + }); + + return { ...oldData, pages: newPages }; }); } }