diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts index 016c76b95b..d82b799614 100644 --- a/app/soapbox/actions/streaming.ts +++ b/app/soapbox/actions/streaming.ts @@ -5,7 +5,7 @@ import messages from 'soapbox/locales/messages'; import { normalizeChatMessage } from 'soapbox/normalizers'; import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats'; import { queryClient } from 'soapbox/queries/client'; -import { updatePageItem, appendPageItem, removePageItem, flattenPages, PaginatedResult } from 'soapbox/utils/queries'; +import { updatePageItem, appendPageItem, removePageItem, flattenPages, PaginatedResult, sortQueryData } from 'soapbox/utils/queries'; import { play, soundCache } from 'soapbox/utils/sounds'; import { connectStream } from '../stream'; @@ -56,17 +56,28 @@ interface ChatPayload extends Omit { last_message: ChatMessage | null, } +const dateComparator = (chatA: IChat, chatB: IChat): number => { + const chatADate = new Date(chatA.last_message?.created_at as string); + const chatBDate = new Date(chatB.last_message?.created_at as string); + + if (chatBDate < chatADate) return -1; + if (chatBDate > chatADate) return 1; + return 0; +}; + const updateChat = (payload: ChatPayload) => { const { id: chatId, last_message: lastMessage } = payload; - const currentChats = flattenPages(queryClient.getQueryData>>(ChatKeys.chatSearch())); - - // Update the specific Chat query data. - // queryClient.setQueryData(ChatKeys.chat(chatId), payload as any); + const currentChats = flattenPages( + queryClient.getQueryData>>(ChatKeys.chatSearch()), + ); if (currentChats?.find((chat: any) => chat.id === chatId)) { // If the chat exists in the client, let's update it. updatePageItem(ChatKeys.chatSearch(), payload as any, (o, n) => o.id === n.id); + // Now that we have the new chat loaded, let's re-sort to put + // the most recent on top. + sortQueryData(ChatKeys.chatSearch(), dateComparator); } else { // If this is a brand-new chat, let's invalid the queries. queryClient.invalidateQueries(ChatKeys.chatSearch()); diff --git a/app/soapbox/queries/chats.ts b/app/soapbox/queries/chats.ts index d98101e445..44c948c56f 100644 --- a/app/soapbox/queries/chats.ts +++ b/app/soapbox/queries/chats.ts @@ -74,7 +74,7 @@ const ChatKeys = { /** Check if item is most recent */ const isLastMessage = (chatMessageId: string): boolean => { const queryData = queryClient.getQueryData>>(ChatKeys.chatSearch()); - const items = flattenPages(queryData); + const items = flattenPages(queryData); const chat = items?.find((item) => item.last_message?.id === chatMessageId); return !!chat; diff --git a/app/soapbox/utils/__tests__/queries.test.ts b/app/soapbox/utils/__tests__/queries.test.ts new file mode 100644 index 0000000000..c21740a903 --- /dev/null +++ b/app/soapbox/utils/__tests__/queries.test.ts @@ -0,0 +1,63 @@ +import { InfiniteData } from '@tanstack/react-query'; + +import { queryClient } from 'soapbox/queries/client'; + +import { PaginatedResult, sortQueryData } from '../queries'; + +interface Item { + id: number +} + +const buildItem = (id: number): Item => ({ id }); + +const queryKey = ['test', 'query']; + +describe('sortQueryData()', () => { + describe('without cached data', () => { + it('safely returns undefined', () => { + sortQueryData(queryKey, (a, b) => b.id - a.id); + const nextQueryData = queryClient.getQueryData>>(queryKey); + expect(nextQueryData).toBeUndefined(); + }); + }); + + describe('with cached data', () => { + const cachedQueryData = { + pages: [ + { + result: [...Array(20).fill(0).map((_, idx) => buildItem(idx))], + hasMore: false, + link: undefined, + }, + { + result: [...Array(4).fill(0).map((_, idx) => buildItem(idx + 20))], + hasMore: true, + link: 'my-link', + }, + ], + pageParams: [undefined], + }; + + beforeEach(() => { + queryClient.setQueryData(queryKey, cachedQueryData); + }); + + it('sorts the cached data', () => { + const initialQueryData = queryClient.getQueryData>>(queryKey); + expect(initialQueryData?.pages[0].result[0].id === 0); // first id is 0 + sortQueryData(queryKey, (a, b) => b.id - a.id); // sort descending + const nextQueryData = queryClient.getQueryData>>(queryKey); + expect(nextQueryData?.pages[0].result[0].id === 0); // first id is now 23 + }); + + it('persists the metadata', () => { + const initialQueryData = queryClient.getQueryData>>(queryKey); + const initialMetaData = initialQueryData?.pages.map((page) => page.link); + sortQueryData(queryKey, (a, b) => b.id - a.id); + const nextQueryData = queryClient.getQueryData>>(queryKey); + const nextMetaData = nextQueryData?.pages.map((page) => page.link); + + expect(initialMetaData).toEqual(nextMetaData); + }); + }); +}); \ No newline at end of file diff --git a/app/soapbox/utils/queries.ts b/app/soapbox/utils/queries.ts index b39579de18..61df573f42 100644 --- a/app/soapbox/utils/queries.ts +++ b/app/soapbox/utils/queries.ts @@ -1,6 +1,6 @@ import { queryClient } from 'soapbox/queries/client'; -import type { InfiniteData, QueryKey, UseInfiniteQueryResult } from '@tanstack/react-query'; +import type { InfiniteData, QueryKey } from '@tanstack/react-query'; export interface PaginatedResult { result: T[], @@ -9,7 +9,7 @@ export interface PaginatedResult { } /** Flatten paginated results into a single array. */ -const flattenPages = (queryData: UseInfiniteQueryResult>['data']) => { +const flattenPages = (queryData: InfiniteData> | undefined) => { return queryData?.pages.reduce( (prev: T[], curr) => [...curr.result, ...prev], [], @@ -53,9 +53,42 @@ const removePageItem = (queryKey: QueryKey, itemToRemove: T, isItem: (item: T }); }; +const paginateQueryData = (array: T[] | undefined) => { + return array?.reduce((resultArray: any, item: any, index: any) => { + const chunkIndex = Math.floor(index / 20); + + if (!resultArray[chunkIndex]) { + resultArray[chunkIndex] = []; // start a new chunk + } + + resultArray[chunkIndex].push(item); + + return resultArray; + }, []); +}; + +const sortQueryData = (queryKey: QueryKey, comparator: (a: T, b: T) => number) => { + queryClient.setQueryData>>(queryKey, (prevResult) => { + if (prevResult) { + const nextResult = { ...prevResult }; + const flattenedQueryData = flattenPages(nextResult); + const sortedQueryData = flattenedQueryData?.sort(comparator); + const paginatedPages = paginateQueryData(sortedQueryData); + const newPages = paginatedPages.map((page: T, idx: number) => ({ + ...prevResult.pages[idx], + result: page, + })); + + nextResult.pages = newPages; + return nextResult; + } + }); +}; + export { flattenPages, updatePageItem, appendPageItem, removePageItem, + sortQueryData, };