Skip to content
6 changes: 6 additions & 0 deletions .changeset/sparkly-regions-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/message-types': patch
'@rocket.chat/meteor': patch
---

Fixes an issue where deleted thread messages display as empty in thread context
50 changes: 38 additions & 12 deletions apps/meteor/client/lib/normalizeThreadMessage.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { parse } from '@rocket.chat/message-parser';
import { render } from '@testing-library/react';
import type { TFunction } from 'i18next';

import { getMarkdownParserLimit } from './getMarkdownParserLimit';
import { normalizeThreadMessage } from './normalizeThreadMessage';
Expand All @@ -14,6 +16,8 @@ const mockedGetMarkdownParserLimit = jest.mocked(getMarkdownParserLimit);
const mockedFilterMarkdown = jest.mocked(filterMarkdown);
const mockedParse = jest.mocked(parse);

const t = ((key: string) => key) as TFunction;

describe('normalizeThreadMessage', () => {
beforeEach(() => {
mockedGetMarkdownParserLimit.mockReturnValue(Infinity);
Expand All @@ -26,8 +30,8 @@ describe('normalizeThreadMessage', () => {
});

it('should parse message through filterMarkdown and parse when within limit', () => {
const message = { msg: 'Hello world', mentions: [], attachments: [] };
normalizeThreadMessage(message);
const message = { msg: 'Hello world', mentions: [], attachments: [] } as unknown as IMessage;
normalizeThreadMessage(message, t);

expect(mockedFilterMarkdown).toHaveBeenCalledWith('Hello world');
expect(mockedParse).toHaveBeenCalledWith('Hello world', { emoticons: true });
Expand All @@ -36,8 +40,8 @@ describe('normalizeThreadMessage', () => {
it('should skip filterMarkdown and parse when message exceeds limit', () => {
mockedGetMarkdownParserLimit.mockReturnValue(5);

const message = { msg: 'This message is longer than the limit', mentions: [], attachments: [] };
const result = normalizeThreadMessage(message);
const message = { msg: 'This message is longer than the limit', mentions: [], attachments: [] } as unknown as IMessage;
const result = normalizeThreadMessage(message, t);

expect(mockedFilterMarkdown).not.toHaveBeenCalled();
expect(mockedParse).not.toHaveBeenCalled();
Expand All @@ -49,21 +53,21 @@ describe('normalizeThreadMessage', () => {
});

it('should return null when msg is empty and no attachments', () => {
const message = { msg: '', mentions: [], attachments: undefined } as any;
expect(normalizeThreadMessage(message)).toBeNull();
const message = { msg: '', mentions: [], attachments: undefined } as unknown as IMessage;
expect(normalizeThreadMessage(message, t)).toBeNull();
});

it('should return attachment title when msg is empty but attachment has title', () => {
const message = { msg: '', mentions: [], attachments: [{ title: 'file.pdf' }] } as any;
const result = normalizeThreadMessage(message);
const message = { msg: '', mentions: [], attachments: [{ title: 'file.pdf' }] } as unknown as IMessage;
const result = normalizeThreadMessage(message, t);

const { container } = render(<>{result}</>);
expect(container.textContent).toBe('file.pdf');
});

it('should return attachment description when msg is empty and attachments have no title', () => {
const message = { msg: '', mentions: [], attachments: [{ description: 'desc' }] } as any;
const result = normalizeThreadMessage(message);
const message = { msg: '', mentions: [], attachments: [{ description: 'desc' }] } as unknown as IMessage;
const result = normalizeThreadMessage(message, t);

const { container } = render(<>{result}</>);
expect(container.textContent).toBe('desc');
Expand All @@ -74,7 +78,29 @@ describe('normalizeThreadMessage', () => {
throw new Error('parse error');
});

const message = { msg: 'test', mentions: [], attachments: [] };
expect(normalizeThreadMessage(message)).toBeNull();
const message = { msg: 'test', mentions: [], attachments: [] } as unknown as IMessage;
expect(normalizeThreadMessage(message, t)).toBeNull();
});

it('should return the message type text for a removed message without content', () => {
const message = {
t: 'rm',
msg: '',
editedAt: new Date(),
editedBy: { _id: 'uid', username: 'user' },
mentions: [],
attachments: [],
} as unknown as IMessage;

expect(normalizeThreadMessage(message, t)).toBe('Message_removed');
Comment thread
dougfabris marked this conversation as resolved.
expect(mockedFilterMarkdown).not.toHaveBeenCalled();
expect(mockedParse).not.toHaveBeenCalled();
});

it('should render the message content when a `t: rm` message still has content', () => {
const message = { t: 'rm', msg: 'Hello world', mentions: [], attachments: [] } as unknown as IMessage;

expect(normalizeThreadMessage(message, t)).not.toBe('Message_removed');
expect(mockedParse).toHaveBeenCalledWith('Hello world', { emoticons: true });
});
});
12 changes: 10 additions & 2 deletions apps/meteor/client/lib/normalizeThreadMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { type IMessage } from '@rocket.chat/core-typings';
import { Markup } from '@rocket.chat/gazzodown';
import { parse } from '@rocket.chat/message-parser';
import type { Root } from '@rocket.chat/message-parser';
import { MessageTypes } from '@rocket.chat/message-types';
import type { TFunction } from 'i18next';

import { getMarkdownParserLimit } from './getMarkdownParserLimit';
import { filterMarkdown } from '../../app/markdown/lib/markdown';
Expand All @@ -21,7 +23,9 @@ const tryParseWithLimit = (text: string): Root | undefined => {
}
};

export function normalizeThreadMessage({ ...message }: Readonly<Pick<IMessage, 'msg' | 'mentions' | 'attachments'>>) {
export function normalizeThreadMessage({ ...message }: Readonly<IMessage>, t: TFunction) {
const messageType = MessageTypes.getType(message);

if (message.msg) {
delete message.mentions;

Expand Down Expand Up @@ -50,5 +54,9 @@ export function normalizeThreadMessage({ ...message }: Readonly<Pick<IMessage, '
}
}

if (message.t) {
return messageType?.text(t, message, { capitalize: true });
}

return null;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IDiscussionMessage } from '@rocket.chat/core-typings';
import type { MouseEvent } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

import DiscussionsListItem from './components/DiscussionsListItem';
import { useTimeAgo } from '../../../../hooks/useTimeAgo';
Expand All @@ -13,9 +14,10 @@ export type DiscussionListRowProps = {
};

function DiscussionListRow({ discussion, showRealNames, onClick }: DiscussionListRowProps) {
const { t } = useTranslation();
const formatDate = useTimeAgo();

const msg = normalizeThreadMessage(discussion);
const msg = normalizeThreadMessage(discussion, t);

const { name = discussion.u.username } = discussion.u;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Palette } from '@rocket.chat/fuselage';
import { useSetting, useUserId } from '@rocket.chat/ui-contexts';
import type { MouseEvent } from 'react';
import { useCallback, memo } from 'react';
import { useTranslation } from 'react-i18next';

import ThreadListMessage from './ThreadListMessage';
import { useDecryptedMessage } from '../../../../../hooks/useDecryptedMessage';
Expand All @@ -18,9 +19,10 @@ export type ThreadListItemProps = {
};

const ThreadListItem = ({ thread, unread, unreadUser, unreadGroup, onClick }: ThreadListItemProps) => {
const { t } = useTranslation();
const uid = useUserId();
const decryptedMsg = useDecryptedMessage(thread);
const msg = normalizeThreadMessage({ ...thread, msg: decryptedMsg });
const msg = normalizeThreadMessage({ ...thread, msg: decryptedMsg }, t);

const { name = thread.u.username } = thread.u;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { IThreadMainMessage } from '@rocket.chat/core-typings';
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { renderHook } from '@testing-library/react';

import { useNormalizedThreadTitleHtml } from './useNormalizedThreadTitleHtml';
import { filterMarkdown } from '../../../../../../app/markdown/lib/markdown';

jest.mock('../../../../../../app/markdown/lib/markdown', () => ({
filterMarkdown: jest.fn((text: string) => text),
}));

jest.mock('../../../../../../app/emoji/client/emojiParser', () => ({
emojiParser: jest.fn((text: string) => text),
}));

const mockedFilterMarkdown = jest.mocked(filterMarkdown);

const baseMessage = {
_id: 'msg-id',
rid: 'rid',
ts: new Date(),
u: { _id: 'uid', username: 'user' },
_updatedAt: new Date(),
tcount: 1,
tlm: new Date(),
replies: [],
} as unknown as IThreadMainMessage;

describe('useNormalizedThreadTitleHtml', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('should return the "Message_removed" translation for a removed message without content', () => {
const message = {
...baseMessage,
t: 'rm',
msg: '',
editedAt: new Date(),
editedBy: { _id: 'uid', username: 'user' },
} as unknown as IThreadMainMessage;

const { result } = renderHook(() => useNormalizedThreadTitleHtml(message), {
wrapper: mockAppRoot().build(),
});

expect(result.current).toBe('Message_removed');
Comment thread
dougfabris marked this conversation as resolved.
expect(mockedFilterMarkdown).not.toHaveBeenCalled();
});

it('should return the normalized message content for a regular message', () => {
const message = { ...baseMessage, msg: 'Hello world' } as unknown as IThreadMainMessage;

const { result } = renderHook(() => useNormalizedThreadTitleHtml(message), {
wrapper: mockAppRoot().build(),
});

expect(result.current).toBe('Hello world');
expect(mockedFilterMarkdown).toHaveBeenCalledWith('Hello world');
});

it('should render the message content when a `t: rm` message still has content', () => {
const message = { ...baseMessage, t: 'rm', msg: 'Hello world' } as unknown as IThreadMainMessage;

const { result } = renderHook(() => useNormalizedThreadTitleHtml(message), {
wrapper: mockAppRoot().build(),
});

expect(result.current).not.toBe('Message_removed');
expect(result.current).toBe('Hello world');
});
});
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import type { IThreadMainMessage } from '@rocket.chat/core-typings';
import { type IThreadMainMessage } from '@rocket.chat/core-typings';
import { MessageTypes } from '@rocket.chat/message-types';
import { escapeHTML } from '@rocket.chat/string-helpers';
import { useUser, useSetting } from '@rocket.chat/ui-contexts';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';

import { emojiParser } from '../../../../../../app/emoji/client/emojiParser';
import { filterMarkdown } from '../../../../../../app/markdown/lib/markdown';
import { MentionsParser } from '../../../../../../app/mentions/lib/MentionsParser';

export const useNormalizedThreadTitleHtml = (mainMessage: IThreadMainMessage) => {
const { t } = useTranslation();
const me = useUser()?.username || '';
const pattern = useSetting('UTF8_User_Names_Validation', '[0-9a-zA-Z-_.]+');
const useRealName = useSetting('UI_Use_Real_Name', false);

return useMemo((): string => {
const message = { ...mainMessage };
const messageType = MessageTypes.getType(message);

if (message.msg) {
const filteredMessage = filterMarkdown(escapeHTML(message.msg));
Expand Down Expand Up @@ -44,6 +48,10 @@ export const useNormalizedThreadTitleHtml = (mainMessage: IThreadMainMessage) =>
}
}

if (message.t && messageType) {
return escapeHTML(messageType.text(t, message, { capitalize: true }));
}
Comment thread
dougfabris marked this conversation as resolved.

return '';
}, [mainMessage, me, pattern, useRealName]);
}, [mainMessage, me, pattern, useRealName, t]);
};
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/af.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1454,9 +1454,9 @@
"Message_deleting_blocked": "Hierdie boodskap kan nie meer uitgevee word nie",
"Message_editing": "Boodskap redigering",
"Message_info": "Boodskapinligting",
"Message_is_removed": "Boodskap verwyder",
"Message_pinning": "Boodskap vasbind",
"Message_removed": "Boodskap verwyder",
"message_removed": "boodskap verwyder",
"Message_sent_by_email": "Boodskap gestuur per e-pos",
"Message_starring": "Boodskap hoofletter",
"Message_too_long": "Boodskap te lank",
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/ar.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -2487,9 +2487,9 @@
"Message_has_been_unpinned": "تم إلغاء تثبيت الرسالة",
"Message_has_been_unstarred": "تم إلغاء تمييز الرسالة بنجمة",
"Message_info": "معلومات الرسالة",
"Message_is_removed": "تمت إزالة الرسالة",
"Message_pinning": "تثبيت الرسالة",
"Message_removed": "تمت إزالة الرسالة",
"message_removed": "تمت إزالة الرسالة",
"Message_sent_by_email": "تم إرسال الرسالة عبر البريد الإلكتروني",
"Message_starring": "تمييز الرسالة بنجمة",
"Message_too_long": "الرسالة طويلة جدًا",
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/az.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1454,9 +1454,9 @@
"Message_deleting_blocked": "Bu mesaj artıq silinə bilməz",
"Message_editing": "Mesajın redaktə edilməsi",
"Message_info": "Mesaj məlumatı",
"Message_is_removed": "Mesaj silindi",
"Message_pinning": "Mesaj pinning",
"Message_removed": "Mesaj silindi",
"message_removed": "mesaj silindi",
"Message_sent_by_email": "E-poçt ilə göndərilən mesaj",
"Message_starring": "Başlanğıc mesaj",
"Message_too_long": "Mesaj çox uzun",
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/be-BY.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1474,9 +1474,9 @@
"Message_deleting_blocked": "Гэта паведамленне не можа быць выдаленае больш",
"Message_editing": "рэдагаванне паведамлення",
"Message_info": "інфармацыя паведамленняў",
"Message_is_removed": "паведамленне удалено",
"Message_pinning": "паведамленне пиннинга",
"Message_removed": "паведамленне удалено",
"message_removed": "паведамленне удалено",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"Message_sent_by_email": "Паведамленне адпраўлена па электроннай пошце",
"Message_starring": "Паведамленне ў галоўнай ролі",
"Message_too_long": "Занадта доўгае паведамленне",
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/bg.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1451,9 +1451,9 @@
"Message_deleting_blocked": "Това съобщение вече не може да бъде изтрито",
"Message_editing": "Редактиране на съобщения",
"Message_info": "Информация за съобщението",
"Message_is_removed": "Съобщението бе премахнато",
"Message_pinning": "Писане на съобщения",
"Message_removed": "Съобщението бе премахнато",
"message_removed": "съобщението бе премахнато",
"Message_sent_by_email": "Съобщението е изпратено по имейл",
"Message_starring": "Съобщение с главна роля",
"Message_too_long": "Съобщението е твърде дълго",
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/bs.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1451,9 +1451,9 @@
"Message_deleting_blocked": "Ova poruka se više ne može izbrisati",
"Message_editing": "Uređivanje poruke",
"Message_info": "Informacije o poruci",
"Message_is_removed": "Poruka je maknuta",
"Message_pinning": "Prikvačivanje poruka",
"Message_removed": "Poruka je maknuta",
"message_removed": "poruka je maknuta",
"Message_sent_by_email": "Poruka poslana putem e-pošte",
"Message_starring": "Dodavanje zvjezdica na poruka",
"Message_too_long": "Poruka je preduga",
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/ca.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -2460,9 +2460,9 @@
"Message_has_been_unpinned": "El missatge ha estat desancorat",
"Message_has_been_unstarred": "El missatge s'ha tret de favorits",
"Message_info": "Informació del missatge",
"Message_is_removed": "Missatge eliminat",
"Message_pinning": "Fixació de missatges",
"Message_removed": "Missatge eliminat",
"message_removed": "missatge eliminat",
"Message_sent_by_email": "Missatge enviat via correu-e",
"Message_starring": "Destacar missatges",
"Message_too_long": "Missatge massa llarg",
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/cs.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -2091,9 +2091,9 @@
"Message_has_been_unpinned": "Zpráva odepnuta",
"Message_has_been_unstarred": "Hvězdička odebrána",
"Message_info": "Informace o zprávě",
"Message_is_removed": "Zpráva odstraněna",
"Message_pinning": "Připnutí zprávy",
"Message_removed": "Zpráva odstraněna",
"message_removed": "zpráva odstraněna",
"Message_sent_by_email": "Zpráva odeslaná emailem",
"Message_starring": "Ohvězdičkování zprávy",
"Message_too_long": "Zpráva je příliš dlouhá",
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/cy.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1451,9 +1451,9 @@
"Message_deleting_blocked": "Ni ellir dileu'r neges hon bellach",
"Message_editing": "Golygu negeseuon",
"Message_info": "Gwybodaeth am negeseuon",
"Message_is_removed": "Neges wedi'i dynnu",
"Message_pinning": "Pwyso negeseuon",
"Message_removed": "Neges wedi'i dynnu",
"message_removed": "neges wedi'i dynnu",
"Message_sent_by_email": "Neges a anfonwyd trwy E-bost",
"Message_starring": "Neges yn chwarae",
"Message_too_long": "Neges yn rhy hir",
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/da.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -2171,9 +2171,9 @@
"Message_has_been_unpinned": "Beskeden er frigjort",
"Message_has_been_unstarred": "Beskeden er ikke længere stjernemarkeret",
"Message_info": "Besked info",
"Message_is_removed": "Beskeden er fjernet",
"Message_pinning": "Meddelelse fastgørelse",
"Message_removed": "Beskeden er fjernet",
"message_removed": "beskeden er fjernet",
"Message_sent_by_email": "Besked sendt via e-mail",
"Message_starring": "Meddelelse med hovedrollen",
"Message_too_long": "Besked for lang",
Expand Down
Loading
Loading