Skip to content

Commit 20b4022

Browse files
authored
fix: unify what is considered a deleted message across the components (#3117)
1 parent 5f9ee0d commit 20b4022

9 files changed

Lines changed: 102 additions & 23 deletions

File tree

src/components/ChannelListItem/__tests__/utils.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ describe('ChannelPreview utils', () => {
4646
const channelWithDeletedMessage = generateChannel({
4747
messages: [generateMessage({ deleted_at: new Date().toISOString() })],
4848
});
49+
const channelWithDeletedTypeMessage = generateChannel({
50+
messages: [generateMessage({ type: 'deleted' })],
51+
});
52+
const channelWithDeletedForMeMessage = generateChannel({
53+
messages: [generateMessage({ deleted_for_me: true })],
54+
});
4955
const channelWithLocationMessage = generateChannel({
5056
messages: [
5157
generateMessage({
@@ -85,6 +91,12 @@ describe('ChannelPreview utils', () => {
8591
it.each([
8692
['Nothing yet...', 'channelWithEmptyMessage', channelWithEmptyMessage],
8793
['Message deleted', 'channelWithDeletedMessage', channelWithDeletedMessage],
94+
['Message deleted', 'channelWithDeletedTypeMessage', channelWithDeletedTypeMessage],
95+
[
96+
'Message deleted',
97+
'channelWithDeletedForMeMessage',
98+
channelWithDeletedForMeMessage,
99+
],
88100
['🏙 Attachment...', 'channelWithAttachmentMessage', channelWithAttachmentMessage],
89101
['📍Shared location', 'channelWithLocationMessage', channelWithLocationMessage],
90102
[

src/components/ChannelListItem/utils.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getTranslatedMessageText } from '../../context/MessageTranslationViewCo
88
import type { TranslationContextValue } from '../../context/TranslationContext';
99
import type { PluggableList } from 'unified';
1010
import { htmlToTextPlugin, imageToLink, plusPlusToEmphasis } from '../Message';
11+
import { isMessageDeleted } from '../Message/utils';
1112
import remarkGfm from 'remark-gfm';
1213

1314
const remarkPlugins: PluggableList = [
@@ -54,7 +55,7 @@ export const getLatestMessagePreview = (
5455
return t('Nothing yet...');
5556
}
5657

57-
if (latestMessage.deleted_at) {
58+
if (isMessageDeleted(latestMessage)) {
5859
return t('Message deleted');
5960
}
6061

src/components/Message/MessageUI.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
countEmojis,
2121
isMessageBlocked,
2222
isMessageBounced,
23+
isMessageDeleted,
2324
isMessageEdited,
2425
isMessageErrorRetryable,
2526
messageHasAttachments,
@@ -95,8 +96,7 @@ const MessageUIWithContext = ({
9596
() => isMessageAIGenerated?.(message),
9697
[isMessageAIGenerated, message],
9798
);
98-
const isDeleted =
99-
!!message.deleted_at || message.type === 'deleted' || message.deleted_for_me;
99+
const isDeleted = isMessageDeleted(message);
100100

101101
const finalAttachments = useMemo(
102102
() =>

src/components/Message/utils.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export const getMessageActions = (
102102
if (actions && typeof actions === 'boolean') {
103103
// If value of actions is true, then populate all the possible values
104104
messageActions = Object.keys(MESSAGE_ACTIONS);
105-
} else if (actions && actions.length > 0) {
105+
} else if (actions && Array.isArray(actions) && actions.length > 0) {
106106
messageActions = [...actions];
107107
} else {
108108
return [];
@@ -405,5 +405,8 @@ export const isMessageBlocked = (
405405
(message.moderation_details?.action === 'MESSAGE_RESPONSE_ACTION_REMOVE' ||
406406
message.moderation?.action === 'remove');
407407

408+
export const isMessageDeleted = (message: LocalMessage): boolean =>
409+
Boolean(message.deleted_at || message.type === 'deleted' || message.deleted_for_me);
410+
408411
export const isMessageEdited = (message: Pick<LocalMessage, 'message_text_updated_at'>) =>
409412
!!message.message_text_updated_at;

src/components/MessageActions/MessageActions.defaults.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
IconUnpin,
2626
IconUserCheck,
2727
} from '../Icons';
28-
import { isUserMuted } from '../Message/utils';
28+
import { isMessageDeleted, isUserMuted } from '../Message/utils';
2929
import { useMessageComposerController } from '../MessageComposer/hooks/useMessageComposerController';
3030
import { savePreEditSnapshot } from '../MessageComposer/preEditSnapshot';
3131
import { useNotificationApi } from '../Notifications';
@@ -500,6 +500,8 @@ const DefaultMessageActionComponents = {
500500
const { t } = useTranslationContext();
501501
const [openModal, setOpenModal] = useState(false);
502502

503+
if (isMessageDeleted(message)) return null;
504+
503505
return (
504506
<>
505507
<ContextMenuButton

src/components/MessageActions/__tests__/MessageActions.test.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,54 @@ describe('<MessageActions />', () => {
277277
expect(handleDelete).toHaveBeenCalledTimes(1);
278278
});
279279

280+
it('should not show Delete when the message is already deleted', async () => {
281+
const message = generateMessage({
282+
deleted_at: new Date().toISOString(),
283+
user: alice,
284+
});
285+
await renderMessageActions({
286+
channelStateOpts: {
287+
channelCapabilities: { 'delete-own-message': true },
288+
},
289+
customMessageContext: { message },
290+
});
291+
await toggleOpenMessageActions();
292+
293+
expect(screen.queryByText('Delete message')).not.toBeInTheDocument();
294+
});
295+
296+
it('should not show Delete when the message type is deleted', async () => {
297+
const message = generateMessage({
298+
type: 'deleted',
299+
user: alice,
300+
});
301+
await renderMessageActions({
302+
channelStateOpts: {
303+
channelCapabilities: { 'delete-own-message': true },
304+
},
305+
customMessageContext: { message },
306+
});
307+
await toggleOpenMessageActions();
308+
309+
expect(screen.queryByText('Delete message')).not.toBeInTheDocument();
310+
});
311+
312+
it('should not show Delete when the message is deleted for me', async () => {
313+
const message = generateMessage({
314+
deleted_for_me: true,
315+
user: alice,
316+
});
317+
await renderMessageActions({
318+
channelStateOpts: {
319+
channelCapabilities: { 'delete-own-message': true },
320+
},
321+
customMessageContext: { message },
322+
});
323+
await toggleOpenMessageActions();
324+
325+
expect(screen.queryByText('Delete message')).not.toBeInTheDocument();
326+
});
327+
280328
it('should include Edit in dropdown actions when user has edit capability', async () => {
281329
const message = generateMessage({ user: alice });
282330
const { container } = await renderMessageActions({

src/components/MessageActions/hooks/useBaseMessageActionSetFilter.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useUserRole } from '../../Message/hooks';
55
import {
66
ACTIONS_NOT_WORKING_IN_THREAD,
77
isMessageBounced,
8+
isMessageDeleted,
89
isMessageErrorRetryable,
910
isNetworkSendFailure,
1011
} from '../../Message/utils';
@@ -15,14 +16,15 @@ import type { MessageActionSetItem } from '../MessageActions';
1516
* Base filter hook which covers actions of type `delete`, `edit`,
1617
* `flag`, `markUnread`, `mute`, `quote`, `react` and `reply`, whether
1718
* the rendered message is a reply (replies are limited to certain actions) and
18-
* whether the message has appropriate type and status.
19+
* whether the message has appropriate type and status (including soft-deleted).
1920
*/
2021
export const useBaseMessageActionSetFilter = (
2122
messageActionSet: MessageActionSetItem[],
2223
disable = false,
2324
) => {
2425
const { initialMessage: isInitialMessage, message } = useMessageContext();
2526
const { channelConfig } = useChannelStateContext();
27+
const messageIsDeleted = isMessageDeleted(message);
2628
const {
2729
canBlockUser,
2830
canDelete,
@@ -68,15 +70,17 @@ export const useBaseMessageActionSetFilter = (
6870
return (
6971
(type === 'resendMessage' && canSendMessage && (allowRetry || isBounced)) ||
7072
(type === 'edit' && ((isBounced && canEdit) || hasNetworkSendFailure)) ||
71-
(type === 'delete' && ((isBounced && canDelete) || hasNetworkSendFailure))
73+
(type === 'delete' &&
74+
!messageIsDeleted &&
75+
((isBounced && canDelete) || hasNetworkSendFailure))
7276
);
7377
}
7478

7579
if (
7680
type === 'resendMessage' ||
7781
(type === 'blockUser' && !canBlockUser) ||
7882
(type === 'copyMessageText' && !message.text) ||
79-
(type === 'delete' && !canDelete) ||
83+
(type === 'delete' && (!canDelete || messageIsDeleted)) ||
8084
(type === 'edit' && !canEdit) ||
8185
(type === 'flag' && !canFlag) ||
8286
(type === 'markUnread' && !canMarkUnread) ||
@@ -106,6 +110,7 @@ export const useBaseMessageActionSetFilter = (
106110
channelConfig,
107111
isBounced,
108112
isInitialMessage,
113+
messageIsDeleted,
109114
isMessageThreadReply,
110115
message.error,
111116
message.status,

src/components/SummarizedMessagePreview/__tests__/useLatestMessagePreview.test.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -155,20 +155,27 @@ describe('useLatestMessagePreview', () => {
155155
});
156156

157157
describe('deleted message', () => {
158-
it('returns deleted type with delivery status and sender name', () => {
159-
const message = generateMessage({
160-
deleted_at: new Date().toISOString(),
161-
user: ownUser,
162-
});
163-
const { result } = renderPreviewHook({
164-
latestMessage: message,
165-
messageDeliveryStatus: MessageDeliveryStatus.DELIVERED,
166-
});
167-
expect(result.current.type).toBe('deleted');
168-
expect(result.current.text).toBe('Message deleted');
169-
expect(result.current.deliveryStatus).toBe('delivered');
170-
expect(result.current.senderName).toBe('You');
171-
});
158+
it.each([
159+
['deleted_at timestamp', { deleted_at: new Date().toISOString() }],
160+
['deleted type', { type: 'deleted' as const }],
161+
['deleted for current user', { deleted_for_me: true }],
162+
])(
163+
'returns deleted type with delivery status and sender name for %s',
164+
(_label, messageOverrides) => {
165+
const message = generateMessage({
166+
...messageOverrides,
167+
user: ownUser,
168+
});
169+
const { result } = renderPreviewHook({
170+
latestMessage: message,
171+
messageDeliveryStatus: MessageDeliveryStatus.DELIVERED,
172+
});
173+
expect(result.current.type).toBe('deleted');
174+
expect(result.current.text).toBe('Message deleted');
175+
expect(result.current.deliveryStatus).toBe('delivered');
176+
expect(result.current.senderName).toBe('You');
177+
},
178+
);
172179
});
173180

174181
describe('poll message', () => {

src/components/SummarizedMessagePreview/hooks/useLatestMessagePreview.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
useChatContext,
1616
useTranslationContext,
1717
} from '../../../context';
18+
import { isMessageDeleted } from '../../Message/utils';
1819

1920
import type { MessageDeliveryStatus } from '../../ChannelListItem';
2021

@@ -161,7 +162,7 @@ export const useLatestMessagePreview = ({
161162
senderName = latestMessage.user?.name || latestMessage.user?.id;
162163
}
163164

164-
if (latestMessage.deleted_at) {
165+
if (isMessageDeleted(latestMessage)) {
165166
return {
166167
deliveryStatus,
167168
senderName,

0 commit comments

Comments
 (0)