Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
15 changes: 11 additions & 4 deletions src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
ChannelMemberResponse,
ChannelQueryOptions,
ChannelState,
DeleteMessageOptions,
ErrorFromResponse,
Event,
EventAPIResponse,
Expand Down Expand Up @@ -176,7 +177,10 @@ export type ChannelProps = ChannelPropsForwardedToComponentContext & {
*/
channelQueryOptions?: ChannelQueryOptions;
/** Custom action handler to override the default `client.deleteMessage(message.id)` function */
doDeleteMessageRequest?: (message: LocalMessage) => Promise<MessageResponse>;
doDeleteMessageRequest?: (
message: LocalMessage,
options?: DeleteMessageOptions,
) => Promise<MessageResponse>;
/** Custom action handler to override the default `channel.markRead` request function (advanced usage only) */
doMarkReadRequest?: (
channel: StreamChannel,
Expand Down Expand Up @@ -910,15 +914,18 @@ const ChannelInner = (
);

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

Expand Down
18 changes: 12 additions & 6 deletions src/components/Channel/__tests__/Channel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1607,15 +1607,18 @@ describe('Channel', () => {

it('should call the default client.deleteMessage() function', async () => {
const message = generateMessage();

const deleteMessageOptions = { deleteForMe: true, hard: false };
const clientDeleteMessageSpy = jest
.spyOn(chatClient, 'deleteMessage')
.mockImplementationOnce(() => Promise.resolve({ message }));
await renderComponent({ channel, chatClient }, ({ deleteMessage }) => {
deleteMessage(message);
deleteMessage(message, deleteMessageOptions);
});
await waitFor(() =>
expect(clientDeleteMessageSpy).toHaveBeenCalledWith(message.id),
expect(clientDeleteMessageSpy).toHaveBeenCalledWith(
message.id,
deleteMessageOptions,
),
);
});

Expand Down Expand Up @@ -1644,7 +1647,7 @@ describe('Channel', () => {

it('should call the custom doDeleteMessageRequest instead of client.deleteMessage()', async () => {
const message = generateMessage();

const deleteMessageOptions = { deleteForMe: true, hard: false };
const doDeleteMessageRequest = jest.fn();
const clientDeleteMessageSpy = jest
.spyOn(chatClient, 'deleteMessage')
Expand All @@ -1653,13 +1656,16 @@ describe('Channel', () => {
await renderComponent(
{ channel, chatClient, doDeleteMessageRequest },
({ deleteMessage }) => {
deleteMessage(message);
deleteMessage(message, deleteMessageOptions);
},
);

await waitFor(() => {
expect(clientDeleteMessageSpy).not.toHaveBeenCalled();
expect(doDeleteMessageRequest).toHaveBeenCalledWith(message);
expect(doDeleteMessageRequest).toHaveBeenCalledWith(
message,
deleteMessageOptions,
);
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export const useCreateChannelStateContext = (
channelCapabilities[capability] = true;
});

// FIXME: this is crazy - I could not find out why the messages were not getting updated when only message properties that are not part
// of this serialization has been changed. A great example of memoization gone wrong.
const memoizedMessageData = skipMessageDataMemoization
? messages
: messages
Expand All @@ -69,10 +71,11 @@ export const useCreateChannelStateContext = (
pinned,
reply_count,
status,
type,
updated_at,
user,
}) =>
`${deleted_at}${
`${type}${deleted_at}${
latest_reactions ? latest_reactions.map(({ type }) => type).join() : ''
}${pinned}${reply_count}${status}${
updated_at && (isDayOrMoment(updated_at) || isDate(updated_at))
Expand Down
57 changes: 32 additions & 25 deletions src/components/Message/__tests__/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
MESSAGE_ACTIONS,
messageHasAttachments,
messageHasReactions,
OPTIONAL_MESSAGE_ACTIONS,
validateAndGetMessage,
} from '../utils';

Expand Down Expand Up @@ -90,6 +91,7 @@ describe('Message utils', () => {
canReply: true,
};
const actions = Object.values(MESSAGE_ACTIONS);
const optionalActions = Object.values(OPTIONAL_MESSAGE_ACTIONS);

it.each([
['empty', []],
Expand Down Expand Up @@ -131,31 +133,36 @@ describe('Message utils', () => {
});

it.each([
['allow', 'edit', 'canEdit', true],
['not allow', 'edit', 'canEdit', false],
['allow', 'delete', 'canDelete', true],
['not allow', 'delete', 'canDelete', false],
['allow', 'flag', 'canFlag', true],
['not allow', 'flag', 'canFlag', false],
['allow', 'markUnread', 'canMarkUnread', true],
['not allow', 'markUnread', 'canMarkUnread', false],
['allow', 'mute', 'canMute', true],
['not allow', 'mute', 'canMute', false],
['allow', 'pin', 'canPin', true],
['not allow', 'pin', 'canPin', false],
['allow', 'quote', 'canQuote', true],
['not allow', 'quote', 'canQuote', false],
])('it should %s %s when %s is %s', (_, action, capabilityKey, capabilityValue) => {
const capabilities = {
[capabilityKey]: capabilityValue,
};
const result = getMessageActions(actions, capabilities);
if (capabilityValue) {
expect(result).toStrictEqual([action]);
} else {
expect(result).not.toStrictEqual([action]);
}
});
['allow', 'edit', 'canEdit', true, actions],
['not allow', 'edit', 'canEdit', false, actions],
['allow', 'delete', 'canDelete', true, actions],
['not allow', 'delete', 'canDelete', false, actions],
['allow', 'deleteForMe', 'canDelete', true, optionalActions],
['not allow', 'deleteForMe', 'canDelete', false, optionalActions],
['allow', 'flag', 'canFlag', true, actions],
['not allow', 'flag', 'canFlag', false, actions],
['allow', 'markUnread', 'canMarkUnread', true, actions],
['not allow', 'markUnread', 'canMarkUnread', false, actions],
['allow', 'mute', 'canMute', true, actions],
['not allow', 'mute', 'canMute', false, actions],
['allow', 'pin', 'canPin', true, actions],
['not allow', 'pin', 'canPin', false, actions],
['allow', 'quote', 'canQuote', true, actions],
['not allow', 'quote', 'canQuote', false, actions],
])(
'it should %s %s when %s is %s',
(_, action, capabilityKey, capabilityValue, actionsToUse) => {
const capabilities = {
[capabilityKey]: capabilityValue,
};
const result = getMessageActions(actionsToUse, capabilities);
if (capabilityValue) {
expect(result).toStrictEqual([action]);
} else {
expect(result).not.toStrictEqual([action]);
}
},
);
});

describe('shouldMessageComponentUpdate', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@ describe('useDeleteHandler custom hook', () => {

it('should delete a message by its id', async () => {
const message = generateMessage();
const deleteMessageOptions = { deleteForMe: true, hard: false };
const handleDelete = await renderUseDeleteHandler(message);
await handleDelete(mouseEventMock);
expect(deleteMessage).toHaveBeenCalledWith(message);
await handleDelete(mouseEventMock, deleteMessageOptions);
expect(deleteMessage).toHaveBeenCalledWith(message, deleteMessageOptions);
});

it('should update the message with the result of deletion', async () => {
Expand Down
6 changes: 3 additions & 3 deletions src/components/Message/hooks/useDeleteHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useChannelActionContext } from '../../../context/ChannelActionContext';
import { useChatContext } from '../../../context/ChatContext';
import { useTranslationContext } from '../../../context/TranslationContext';

import type { LocalMessage } from 'stream-chat';
import type { DeleteMessageOptions, LocalMessage } from 'stream-chat';
import type { ReactEventHandler } from '../types';

export type DeleteMessageNotifications = {
Expand All @@ -22,14 +22,14 @@ export const useDeleteHandler = (
const { client } = useChatContext('useDeleteHandler');
const { t } = useTranslationContext('useDeleteHandler');

return async (event) => {
return async (event, options?: DeleteMessageOptions) => {
event.preventDefault();
if (!message?.id || !client || !updateMessage) {
return;
}

try {
const deletedMessage = await deleteMessage(message);
const deletedMessage = await deleteMessage(message, options);
updateMessage(deletedMessage);
} catch (e) {
const errorMessage =
Expand Down
10 changes: 9 additions & 1 deletion src/components/Message/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export const isUserMuted = (message: LocalMessage, mutes?: Mute[]) => {
return !!userMuted.length;
};

export const OPTIONAL_MESSAGE_ACTIONS = {
deleteForMe: 'deleteForMe',
};

export const MESSAGE_ACTIONS = {
delete: 'delete',
edit: 'edit',
Expand All @@ -67,7 +71,7 @@ export const MESSAGE_ACTIONS = {
};

export type MessageActionsArray<T extends string = string> = Array<
keyof typeof MESSAGE_ACTIONS | T
keyof typeof MESSAGE_ACTIONS | keyof typeof OPTIONAL_MESSAGE_ACTIONS | T
>;

// @deprecated in favor of `channelCapabilities` - TODO: remove in next major release
Expand Down Expand Up @@ -172,6 +176,10 @@ export const getMessageActions = (
messageActionsAfterPermission.push(MESSAGE_ACTIONS.delete);
}

if (canDelete && messageActions.indexOf(OPTIONAL_MESSAGE_ACTIONS.deleteForMe) > -1) {
messageActionsAfterPermission.push(OPTIONAL_MESSAGE_ACTIONS.deleteForMe);
}

if (canEdit && messageActions.indexOf(MESSAGE_ACTIONS.edit) > -1) {
messageActionsAfterPermission.push(MESSAGE_ACTIONS.edit);
}
Expand Down
13 changes: 12 additions & 1 deletion src/components/MessageActions/MessageActionsBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ComponentProps } from 'react';
import React from 'react';
import { CustomMessageActionsList as DefaultCustomMessageActionsList } from './CustomMessageActionsList';
import { RemindMeActionButton } from './RemindMeSubmenu';
import { useMessageReminder } from '../Message';
import { OPTIONAL_MESSAGE_ACTIONS, useMessageReminder } from '../Message';
import { useMessageComposer } from '../MessageInput';
import {
useChatContext,
Expand Down Expand Up @@ -162,6 +162,17 @@ const UnMemoizedMessageActionsBox = (props: MessageActionsBoxProps) => {
{t('Delete')}
</button>
)}
{messageActions.indexOf(OPTIONAL_MESSAGE_ACTIONS.deleteForMe) > -1 &&
!message.deleted_for_me && (
<button
aria-selected='false'
className={buttonClassName}
onClick={(e) => handleDelete(e, { deleteForMe: true })}
role='option'
>
{t('Delete for me')}
</button>
)}
{messageActions.indexOf(MESSAGE_ACTIONS.remindMe) > -1 && (
<RemindMeActionButton className={buttonClassName} isMine={mine} />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,20 @@ describe('MessageActionsBox', () => {
expect(results).toHaveNoViolations();
});

it('should call the handleDelete prop if the deleteForMe button is clicked', async () => {
getMessageActionsMock.mockImplementationOnce(() => ['deleteForMe']);
const handleDelete = jest.fn();
const {
result: { container, getByText },
} = await renderComponent({ handleDelete, message: generateMessage() });
await act(async () => {
await fireEvent.click(getByText('Delete for me'));
});
expect(handleDelete).toHaveBeenCalledTimes(1);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

it('should call the handlePin prop if the pin button is clicked', async () => {
getMessageActionsMock.mockImplementationOnce(() => ['pin']);
const handlePin = jest.fn();
Expand Down
6 changes: 5 additions & 1 deletion src/context/ChannelActionContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { PropsWithChildren } from 'react';
import React, { useContext } from 'react';

import type {
DeleteMessageOptions,
LocalMessage,
Message,
MessageResponse,
Expand Down Expand Up @@ -29,7 +30,10 @@ export type RetrySendMessage = (message: LocalMessage) => Promise<void>;
export type ChannelActionContextValue = {
addNotification: (text: string, type: 'success' | 'error') => void;
closeThread: (event?: React.BaseSyntheticEvent) => void;
deleteMessage: (message: LocalMessage) => Promise<MessageResponse>;
deleteMessage: (
message: LocalMessage,
options?: DeleteMessageOptions,
) => Promise<MessageResponse>;
dispatch: React.Dispatch<ChannelStateReducerAction>;
editMessage: (
message: LocalMessage | MessageResponse,
Expand Down
8 changes: 6 additions & 2 deletions src/context/MessageContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { PropsWithChildren, ReactNode } from 'react';
import type { BaseSyntheticEvent, PropsWithChildren, ReactNode } from 'react';
import React, { useContext } from 'react';

import type {
DeleteMessageOptions,
LocalMessage,
Mute,
ReactionResponse,
Expand Down Expand Up @@ -48,7 +49,10 @@ export type MessageContextValue = {
/** Function to send an action in a Channel */
handleAction: ActionHandlerReturnType;
/** Function to delete a message in a Channel */
handleDelete: ReactEventHandler;
handleDelete: (
event: BaseSyntheticEvent,
options?: DeleteMessageOptions,
) => Promise<void> | void;
/** Function to edit a message in a Channel */
handleEdit: ReactEventHandler;
/** Function to fetch the message reactions */
Expand Down
1 change: 1 addition & 0 deletions src/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"Create poll": "Umfrage erstellen",
"Current location": "Aktueller Standort",
"Delete": "Löschen",
"Delete for me": "Für mich löschen",
"Delivered": "Zugestellt",
"Download attachment {{ name }}": "Anhang {{ name }} herunterladen",
"Drag your files here": "Ziehen Sie Ihre Dateien hierher",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"Create poll": "Create poll",
"Current location": "Current location",
"Delete": "Delete",
"Delete for me": "Delete for me",
"Delivered": "Delivered",
"Download attachment {{ name }}": "Download attachment {{ name }}",
"Drag your files here": "Drag your files here",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"Create poll": "Crear encuesta",
"Current location": "Ubicación actual",
"Delete": "Borrar",
"Delete for me": "Eliminar para mí",
"Delivered": "Entregado",
"Download attachment {{ name }}": "Descargar adjunto {{ name }}",
"Drag your files here": "Arrastra tus archivos aquí",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"Create poll": "Créer un sondage",
"Current location": "Emplacement actuel",
"Delete": "Supprimer",
"Delete for me": "Supprimer pour moi",
"Delivered": "Publié",
"Download attachment {{ name }}": "Télécharger la pièce jointe {{ name }}",
"Drag your files here": "Glissez vos fichiers ici",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"Create poll": "मतदान बनाएँ",
"Current location": "वर्तमान स्थान",
"Delete": "डिलीट",
"Delete for me": "मेरे लिए डिलीट करें",
"Delivered": "पहुंच गया",
"Download attachment {{ name }}": "अनुलग्नक {{ name }} डाउनलोड करें",
"Drag your files here": "अपनी फ़ाइलें यहाँ खींचें",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"Create poll": "Crea sondaggio",
"Current location": "Posizione attuale",
"Delete": "Elimina",
"Delete for me": "Elimina per me",
"Delivered": "Consegnato",
"Download attachment {{ name }}": "Scarica l'allegato {{ name }}",
"Drag your files here": "Trascina i tuoi file qui",
Expand Down
Loading