From e7106a35b392d6187d987ae476b03bf5d5fa98b3 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 9 Nov 2022 12:24:44 -0500 Subject: [PATCH] Refactor 'createChatMessage' mutation --- .../features/chats/components/chat.tsx | 73 ++++--------------- app/soapbox/queries/__tests__/chats.test.ts | 37 ++++++++++ app/soapbox/queries/chats.ts | 56 +++++++++++++- 3 files changed, 104 insertions(+), 62 deletions(-) diff --git a/app/soapbox/features/chats/components/chat.tsx b/app/soapbox/features/chats/components/chat.tsx index ed4adc2b1..bd5a384a7 100644 --- a/app/soapbox/features/chats/components/chat.tsx +++ b/app/soapbox/features/chats/components/chat.tsx @@ -1,13 +1,10 @@ -import { useMutation } from '@tanstack/react-query'; import classNames from 'clsx'; import React, { MutableRefObject, useEffect, useState } from 'react'; import { Stack } from 'soapbox/components/ui'; // import UploadProgress from 'soapbox/components/upload-progress'; // import UploadButton from 'soapbox/features/compose/components/upload_button'; -import { useOwnAccount } from 'soapbox/hooks'; -import { ChatKeys, IChat, useChatActions } from 'soapbox/queries/chats'; -import { queryClient } from 'soapbox/queries/client'; +import { IChat, useChatActions } from 'soapbox/queries/chats'; // import { truncateFilename } from 'soapbox/utils/media'; import ChatComposer from './chat-composer'; @@ -26,8 +23,6 @@ interface ChatInterface { * Reused between floating desktop chats and fullscreen/mobile chats. */ const Chat: React.FC = ({ chat, inputRef, className }) => { - const account = useOwnAccount(); - const { createChatMessage, acceptChat } = useChatActions(chat.id); const [content, setContent] = useState(''); @@ -39,57 +34,19 @@ const Chat: React.FC = ({ chat, inputRef, className }) => { const isSubmitDisabled = content.length === 0 && !attachment; - const submitMessage = useMutation(({ chatId, content }: any) => createChatMessage(chatId, content), { - retry: false, - onMutate: async (newMessage: any) => { - // Cancel any outgoing refetches (so they don't overwrite our optimistic update) - await queryClient.cancelQueries(['chats', 'messages', chat.id]); + const submitMessage = () => { + createChatMessage.mutate({ chatId: chat.id, content }, { + onError: (_error, _variables, context: any) => { + setContent(context.prevContent as string); + setErrorSubmittingMessage(true); + }, + onSuccess: () => { + setErrorSubmittingMessage(false); + }, + }); - // Snapshot the previous value - const prevChatMessages = queryClient.getQueryData(['chats', 'messages', chat.id]); - const prevContent = content; - - // Clear state (content, attachment, etc) - clearState(); - - // Optimistically update to the new value - queryClient.setQueryData(['chats', 'messages', chat.id], (prevResult: any) => { - const newResult = { ...prevResult }; - newResult.pages = newResult.pages.map((page: any, idx: number) => { - if (idx === 0) { - return { - ...page, - result: [...page.result, { - ...newMessage, - id: String(Number(new Date())), - created_at: new Date(), - account_id: account?.id, - pending: true, - unread: true, - }], - }; - } - - return page; - }); - - return newResult; - }); - - // Return a context object with the snapshotted value - return { prevChatMessages, prevContent }; - }, - // If the mutation fails, use the context returned from onMutate to roll back - onError: (_error: any, _newData: any, context: any) => { - setContent(context.prevContent); - queryClient.setQueryData(['chats', 'messages', chat.id], context.prevChatMessages); - setErrorSubmittingMessage(true); - }, - onSuccess: () => { - setErrorSubmittingMessage(false); - queryClient.invalidateQueries(ChatKeys.chatMessages(chat.id)); - }, - }); + clearState(); + }; const clearState = () => { setContent(''); @@ -101,8 +58,8 @@ const Chat: React.FC = ({ chat, inputRef, className }) => { }; const sendMessage = () => { - if (!isSubmitDisabled && !submitMessage.isLoading) { - submitMessage.mutate({ chatId: chat.id, content }); + if (!isSubmitDisabled && !createChatMessage.isLoading) { + submitMessage(); if (!chat.accepted) { acceptChat.mutate(); diff --git a/app/soapbox/queries/__tests__/chats.test.ts b/app/soapbox/queries/__tests__/chats.test.ts index 61ad4f99d..58decdd18 100644 --- a/app/soapbox/queries/__tests__/chats.test.ts +++ b/app/soapbox/queries/__tests__/chats.test.ts @@ -295,6 +295,43 @@ describe('useChatActions', () => { }); }); + describe('createChatMessage()', () => { + beforeEach(() => { + const initialQueryData = { + pages: [ + { result: [buildChatMessage('1')], hasMore: false, link: undefined }, + ], + pageParams: [undefined], + }; + + queryClient.setQueryData(ChatKeys.chatMessages(chat.id), initialQueryData); + + __stub((mock) => { + mock + .onPost(`/api/v1/pleroma/chats/${chat.id}/messages`) + .reply(200, { hello: 'world' }); + }); + }); + + it('creates a chat message', async() => { + const { result } = renderHook(() => { + const { createChatMessage } = useChatActions(chat.id); + + useEffect(() => { + createChatMessage.mutate({ chatId: chat.id, content: 'hello' }); + }, []); + + return createChatMessage; + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.data.data).toEqual({ hello: 'world' }); + }); + }); + describe('updateChat()', () => { const nextUnreadCount = 5; diff --git a/app/soapbox/queries/chats.ts b/app/soapbox/queries/chats.ts index 9601aa41e..e51f2e4c9 100644 --- a/app/soapbox/queries/chats.ts +++ b/app/soapbox/queries/chats.ts @@ -8,7 +8,7 @@ import { getNextLink } from 'soapbox/api'; import compareId from 'soapbox/compare_id'; import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context'; import { useStatContext } from 'soapbox/contexts/stat-context'; -import { useApi, useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; +import { useApi, useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks'; import { normalizeChatMessage } from 'soapbox/normalizers'; import { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/queries'; @@ -202,8 +202,10 @@ const useChat = (chatId?: string) => { }; const useChatActions = (chatId: string) => { + const account = useOwnAccount(); const api = useApi(); const dispatch = useAppDispatch(); + const { setUnreadChatsCount } = useStatContext(); const { chat, changeScreen } = useChatContext(); @@ -230,9 +232,55 @@ const useChatActions = (chatId: string) => { .catch(() => null); }; - const createChatMessage = (chatId: string, content: string) => { - return api.post(`/api/v1/pleroma/chats/${chatId}/messages`, { content }); - }; + const createChatMessage = useMutation( + ( + { chatId, content }: { chatId: string, content: string }, + ) => api.post(`/api/v1/pleroma/chats/${chatId}/messages`, { content }), + { + retry: false, + onMutate: async (variables) => { + // Cancel any outgoing refetches (so they don't overwrite our optimistic update) + await queryClient.cancelQueries(['chats', 'messages', variables.chatId]); + + // Snapshot the previous value + const prevContent = variables.content; + const prevChatMessages = queryClient.getQueryData(['chats', 'messages', variables.chatId]); + + // Optimistically update to the new value + queryClient.setQueryData(ChatKeys.chatMessages(variables.chatId), (prevResult: any) => { + const newResult = { ...prevResult }; + newResult.pages = newResult.pages.map((page: any, idx: number) => { + if (idx === 0) { + return { + ...page, + result: [...page.result, { + content: variables.content, + id: String(Number(new Date())), + created_at: new Date(), + account_id: account?.id, + pending: true, + unread: true, + }], + }; + } + + return page; + }); + + return newResult; + }); + + return { prevChatMessages, prevContent }; + }, + // If the mutation fails, use the context returned from onMutate to roll back + onError: (_error: any, variables, context: any) => { + queryClient.setQueryData(['chats', 'messages', variables.chatId], context.prevChatMessages); + }, + onSuccess: (_data: any, variables) => { + queryClient.invalidateQueries(ChatKeys.chatMessages(variables.chatId)); + }, + }, + ); const updateChat = useMutation((data: UpdateChatVariables) => api.patch(`/api/v1/pleroma/chats/${chatId}`, data), { onMutate: async (data) => {