Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Channel prop doDeleteMessageRequest #2152

Merged
merged 3 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docusaurus/docs/React/components/core-components/channel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,37 @@ Custom UI component for date separators.
| --------- | ------------------------------------------------------------------------------- |
| component | <GHComponentLink text='DateSeparator' path='/DateSeparator/DateSeparator.tsx'/> |

### doDeleteMessageRequest

Custom action handler to override the default `client.deleteMessage(message.id)` function.

| Type |
|---------------------------------------------------------------------------------------------------------------------------|
| `(message: StreamMessage<StreamChatGenerics>) => Promise<MessageResponse<StreamChatGenerics>>` |

The function can execute different logic for message replies compared to messages in the main message list based on the `parent_id` property of `StreamMessage` object:

```tsx
import { Channel, StreamMessage } from 'stream-chat-react';
import type { MyStreamChatGenerics } from './types';

const doDeleteMessageRequest = async (message: StreamMessage<MyStreamChatGenerics>) => {
if (message.parent_id) {
// do something before / after deleting a message reply
} else {
// do something before / after deleting a message
}
}

const App = () => (
{/* more components */}
<Channel doDeleteMessageRequest={ doDeleteMessageRequest }>
{/* more components */}
</Channel>
{/* more components */}
);
```

### doMarkReadRequest

Custom action handler to override the default `channel.markRead` request function (advanced usage only).
Expand Down
28 changes: 28 additions & 0 deletions src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {
PropsWithChildren,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
Expand Down Expand Up @@ -118,6 +119,10 @@ export type ChannelProps<
CooldownTimer?: ComponentContextValue<StreamChatGenerics>['CooldownTimer'];
/** Custom UI component for date separators, defaults to and accepts same props as: [DateSeparator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/DateSeparator.tsx) */
DateSeparator?: ComponentContextValue<StreamChatGenerics>['DateSeparator'];
/** Custom action handler to override the default `client.deleteMessage(message.id)` function */
doDeleteMessageRequest?: (
message: StreamMessage<StreamChatGenerics>,
) => Promise<MessageResponse<StreamChatGenerics>>;
/** Custom action handler to override the default `channel.markRead` request function (advanced usage only) */
doMarkReadRequest?: (
channel: StreamChannel<StreamChatGenerics>,
Expand Down Expand Up @@ -313,6 +318,7 @@ const ChannelInner = <
activeUnreadHandler,
channel,
children,
doDeleteMessageRequest,
doMarkReadRequest,
doSendMessageRequest,
doUpdateMessageRequest,
Expand Down Expand Up @@ -679,6 +685,26 @@ const ChannelInner = <
});
};

const deleteMessage = useCallback(
async (
message: StreamMessage<StreamChatGenerics>,
): Promise<MessageResponse<StreamChatGenerics>> => {
if (!message?.id) {
throw new Error('Cannot delete a message - missing message ID.');
}
let deletedMessage;
if (doDeleteMessageRequest) {
deletedMessage = await doDeleteMessageRequest(message);
} else {
const result = await client.deleteMessage(message.id);
deletedMessage = result.message;
}

return deletedMessage;
},
[client, doDeleteMessageRequest],
);

const updateMessage = (
updatedMessage: MessageToSend<StreamChatGenerics> | StreamMessage<StreamChatGenerics>,
) => {
Expand Down Expand Up @@ -925,6 +951,7 @@ const ChannelInner = <
() => ({
addNotification,
closeThread,
deleteMessage,
dispatch,
editMessage,
jumpToLatestMessage,
Expand All @@ -944,6 +971,7 @@ const ChannelInner = <
}),
[
channel.cid,
deleteMessage,
enrichURLForPreviewConfig?.findURLFn,
enrichURLForPreviewConfig?.onLinkPreviewDismissed,
loadMore,
Expand Down
56 changes: 56 additions & 0 deletions src/components/Channel/__tests__/Channel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,62 @@ describe('Channel', () => {
expect(await findByText(messageResponse.text)).toBeInTheDocument();
});

it('should throw error instead of calling default client.deleteMessage() function', async () => {
const { id, ...message } = generateMessage();
const clientDeleteMessageSpy = jest.spyOn(chatClient, 'deleteMessage');
let deleteMessageHandler;
await act(() => {
renderComponent({}, ({ deleteMessage }) => {
deleteMessageHandler = deleteMessage;
});
});

await expect(() => deleteMessageHandler(message)).rejects.toThrow(
'Cannot delete a message - missing message ID.',
);
expect(clientDeleteMessageSpy).not.toHaveBeenCalled();
});

it('should call the default client.deleteMessage() function', async () => {
const message = generateMessage();
const clientDeleteMessageSpy = jest.spyOn(chatClient, 'deleteMessage');
renderComponent({}, ({ deleteMessage }) => {
deleteMessage(message);
});
await waitFor(() => expect(clientDeleteMessageSpy).toHaveBeenCalledWith(message.id));
});

it('should throw error instead of calling custom doDeleteMessageRequest function', async () => {
const { id, ...message } = generateMessage();
const clientDeleteMessageSpy = jest.spyOn(chatClient, 'deleteMessage');
const doDeleteMessageRequest = jest.fn();
let deleteMessageHandler;
await act(() => {
renderComponent({ doDeleteMessageRequest }, ({ deleteMessage }) => {
deleteMessageHandler = deleteMessage;
});
});

await expect(() => deleteMessageHandler(message)).rejects.toThrow(
'Cannot delete a message - missing message ID.',
);
expect(clientDeleteMessageSpy).not.toHaveBeenCalled();
expect(doDeleteMessageRequest).not.toHaveBeenCalled();
});

it('should call the custom doDeleteMessageRequest instead of client.deleteMessage()', async () => {
const message = generateMessage();
const doDeleteMessageRequest = jest.fn();
const clientDeleteMessageSpy = jest.spyOn(chatClient, 'deleteMessage');
renderComponent({ doDeleteMessageRequest }, ({ deleteMessage }) => {
deleteMessage(message);
});
await waitFor(() => {
expect(clientDeleteMessageSpy).not.toHaveBeenCalled();
expect(doDeleteMessageRequest).toHaveBeenCalledWith(message);
});
});

it('should enable editing messages', async () => {
const newText = 'something entirely different';
const updatedMessage = { ...messages[0], text: newText };
Expand Down
80 changes: 55 additions & 25 deletions src/components/Message/hooks/__tests__/useDeleteHandler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,66 @@ import React from 'react';
import { renderHook } from '@testing-library/react-hooks';

import { useDeleteHandler } from '../useDeleteHandler';
import {
ChannelActionProvider,
useChannelActionContext,
} from '../../../../context/ChannelActionContext';
import {
generateChannel,
generateMessage,
generateUser,
getOrCreateChannelApi,
getTestClientWithUser,
useMockedApis,
} from '../../../../mock-builders';
import { Channel } from '../../../Channel';
import { Chat } from '../../../Chat';
import { act } from '@testing-library/react';

import { ChatProvider } from '../../../../context/ChatContext';
import { ChannelActionProvider } from '../../../../context/ChannelActionContext';
import { ChannelStateProvider } from '../../../../context/ChannelStateContext';
import { generateChannel, generateMessage, getTestClient } from '../../../../mock-builders';

const deleteMessage = jest.fn(() => Promise.resolve(generateMessage()));
let channel;
let client;
const message = generateMessage();
const deleteMessage = jest.fn(() => Promise.resolve(message));
const updateMessage = jest.fn();
const mouseEventMock = {
preventDefault: jest.fn(() => {}),
};

async function renderUseDeleteHandler(message = generateMessage()) {
const client = await getTestClient();
client.deleteMessage = deleteMessage;
const channel = generateChannel({
updateMessage,
});
const ChannelActionContextOverrider = ({ children }) => {
const context = useChannelActionContext();
return (
<ChannelActionProvider value={{ ...context, deleteMessage, updateMessage }}>
{children}
</ChannelActionProvider>
);
};

async function renderUseDeleteHandler(message = message) {
const wrapper = ({ children }) => (
<ChatProvider value={{ client }}>
<ChannelStateProvider value={{ channel }}>
<ChannelActionProvider value={{ updateMessage }}>{children}</ChannelActionProvider>
</ChannelStateProvider>
</ChatProvider>
<Chat client={client}>
<Channel channel={channel}>
<ChannelActionContextOverrider>{children}</ChannelActionContextOverrider>
</Channel>
</Chat>
);
const { result } = renderHook(() => useDeleteHandler(message), { wrapper });
return result.current;
let rendered;
await act(async () => {
rendered = await renderHook(() => useDeleteHandler(message), { wrapper });
});

return rendered.result.current;
}

describe('useDeleteHandler custom hook', () => {
beforeAll(async () => {
client = await getTestClientWithUser(generateUser());
const channelData = generateChannel();
useMockedApis(client, [getOrCreateChannelApi(channelData)]);
channel = client.channel('messaging', channelData.channel.id);
});

afterEach(jest.clearAllMocks);

it('should generate function that handles message deletion', async () => {
const handleDelete = await renderUseDeleteHandler();
expect(typeof handleDelete).toBe('function');
Expand All @@ -48,15 +77,16 @@ describe('useDeleteHandler custom hook', () => {
const message = generateMessage();
const handleDelete = await renderUseDeleteHandler(message);
await handleDelete(mouseEventMock);
expect(deleteMessage).toHaveBeenCalledWith(message.id);
expect(deleteMessage).toHaveBeenCalledWith(message);
});

it('should update the message with the result of deletion', async () => {
const message = generateMessage();
const deletedMessage = generateMessage();
deleteMessage.mockImplementationOnce(() => Promise.resolve({ message: deletedMessage }));
const deleteMessageResponse = generateMessage();
deleteMessage.mockImplementationOnce(() => Promise.resolve(deleteMessageResponse));
const handleDelete = await renderUseDeleteHandler(message);
await handleDelete(mouseEventMock);
expect(updateMessage).toHaveBeenCalledWith(deletedMessage);
await act(async () => {
await handleDelete(mouseEventMock);
});
expect(updateMessage).toHaveBeenCalledWith(deleteMessageResponse);
});
});
8 changes: 5 additions & 3 deletions src/components/Message/hooks/useDeleteHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export const useDeleteHandler = <
): ReactEventHandler => {
const { getErrorNotification, notify } = notifications;

const { updateMessage } = useChannelActionContext<StreamChatGenerics>('useDeleteHandler');
const { deleteMessage, updateMessage } = useChannelActionContext<StreamChatGenerics>(
'useDeleteHandler',
);
const { client } = useChatContext<StreamChatGenerics>('useDeleteHandler');
const { t } = useTranslationContext('useDeleteHandler');

Expand All @@ -36,8 +38,8 @@ export const useDeleteHandler = <
}

try {
const data = await client.deleteMessage(message.id);
updateMessage(data.message);
const deletedMessage = await deleteMessage(message);
updateMessage(deletedMessage);
} catch (e) {
const errorMessage =
getErrorNotification && validateAndGetMessage(getErrorNotification, [message]);
Expand Down
4 changes: 4 additions & 0 deletions src/context/ChannelActionContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
Attachment,
ErrorFromResponse,
Message,
MessageResponse,
UpdatedMessage,
UpdateMessageAPIResponse,
UserResponse,
Expand Down Expand Up @@ -49,6 +50,9 @@ export type ChannelActionContextValue<
> = {
addNotification: (text: string, type: 'success' | 'error') => void;
closeThread: (event?: React.BaseSyntheticEvent) => void;
deleteMessage: (
message: StreamMessage<StreamChatGenerics>,
) => Promise<MessageResponse<StreamChatGenerics>>;
dispatch: React.Dispatch<ChannelStateReducerAction<StreamChatGenerics>>;
editMessage: (
message: UpdatedMessage<StreamChatGenerics>,
Expand Down
Loading