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
5 changes: 5 additions & 0 deletions .changeset/cold-chefs-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Disables read receipts indicators in federated rooms. This feature will be re-enabled when fully compatible with federation.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export type MessageListContextValue = {
messageListRef?: RefCallback<HTMLElement | undefined>;
};

export const MessageListContext = createContext<MessageListContextValue>({
export const messageListContextDefaultValue: MessageListContextValue = {
autoTranslate: {
showAutoTranslate: () => false,
autoTranslateLanguage: undefined,
Expand Down Expand Up @@ -74,7 +74,9 @@ export const MessageListContext = createContext<MessageListContextValue>({
formatTime: () => '',
formatDate: () => '',
messageListRef: undefined,
});
};

export const MessageListContext = createContext<MessageListContextValue>(messageListContextDefaultValue);

export const useShowTranslated: MessageListContextValue['autoTranslate']['showAutoTranslate'] = (...args) =>
useContext(MessageListContext).autoTranslate.showAutoTranslate(...args);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { renderHook } from '@testing-library/react';

import { useReadReceiptsDetailsAction } from './useReadReceiptsDetailsAction';
import { createFakeMessage } from '../../../../tests/mocks/data';
import { useMessageListReadReceipts } from '../list/MessageListContext';

jest.mock('../list/MessageListContext', () => ({
useMessageListReadReceipts: jest.fn(),
}));

const useMessageListReadReceiptsMocked = jest.mocked(useMessageListReadReceipts);

describe('useReadReceiptsDetailsAction', () => {
const message = createFakeMessage({ _id: 'messageId' });

afterEach(() => {
jest.clearAllMocks();
});

it('should return null if read receipts are not enabled', () => {
useMessageListReadReceiptsMocked.mockReturnValue({ enabled: false, storeUsers: true });

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

expect(result.current).toBeNull();
});

it('should return null if read receipts store users is not enabled', () => {
useMessageListReadReceiptsMocked.mockReturnValue({ enabled: true, storeUsers: false });

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

expect(result.current).toBeNull();
});

it('should return a message action config', () => {
useMessageListReadReceiptsMocked.mockReturnValue({ enabled: true, storeUsers: true });

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

expect(result.current).toEqual(
expect.objectContaining({
id: 'receipt-detail',
icon: 'check-double',
label: 'Read_Receipts',
}),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { mockAppRoot } from '@rocket.chat/mock-providers';
import { render, screen } from '@testing-library/react';

import RoomMessage from './RoomMessage';
import { MessageListContext, messageListContextDefaultValue } from '../list/MessageListContext';

const message: IMessage = {
ts: new Date('2021-10-27T00:00:00.000Z'),
Expand Down Expand Up @@ -106,3 +107,53 @@ it('should show ignored message', () => {
expect(screen.queryByText('message body')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Message_Ignored' })).toBeInTheDocument();
});

it('should show read receipt', () => {
render(
<RoomMessage
message={message}
sequential={false}
all={false}
mention={false}
unread={false}
ignoredUser={false}
showUserAvatar={true}
/>,
{
wrapper: mockAppRoot()
.wrap((children) => (
<MessageListContext.Provider value={{ ...messageListContextDefaultValue, readReceipts: { enabled: true, storeUsers: false } }}>
{children}
</MessageListContext.Provider>
))
.build(),
},
);

expect(screen.getByRole('status', { name: 'Message_viewed' })).toBeInTheDocument();
});

it('should not show read receipt if receipt is disabled', () => {
render(
<RoomMessage
message={message}
sequential={false}
all={false}
mention={false}
unread={false}
ignoredUser={false}
showUserAvatar={true}
/>,
{
wrapper: mockAppRoot()
.wrap((children) => (
<MessageListContext.Provider value={{ ...messageListContextDefaultValue, readReceipts: { enabled: false, storeUsers: false } }}>
{children}
</MessageListContext.Provider>
))
.build(),
},
);

expect(screen.queryByRole('status', { name: 'Message_viewed' })).not.toBeInTheDocument();
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isThreadMainMessage } from '@rocket.chat/core-typings';
import { isRoomFederated, isThreadMainMessage } from '@rocket.chat/core-typings';
import { useLayout, useUser, useUserPreference, useSetting, useEndpoint, useSearchParameter } from '@rocket.chat/ui-contexts';
import type { ReactNode, RefCallback } from 'react';
import { useMemo, memo } from 'react';
Expand Down Expand Up @@ -40,7 +40,7 @@ const MessageListProvider = ({ children, messageListRef, attachmentDimension }:
const { isMobile } = useLayout();

const autoLinkDomains = useSetting('Message_CustomDomain_AutoLink', '');
const readReceiptsEnabled = useSetting('Message_Read_Receipt_Enabled', false);
const readReceiptsEnabled = useSetting('Message_Read_Receipt_Enabled', false) && !isRoomFederated(room);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for read receipt processing in server-side code
rg -n "read.*receipt|receipt.*read" --type=ts --glob='**/server/**' -A 5 -B 2 | head -150

Repository: RocketChat/Rocket.Chat

Length of output: 9652


🏁 Script executed:

# Search for federated room checks in read receipt context
rg -n "readReceipt|read_receipt" --type=ts --glob='**/server/**' | head -100

Repository: RocketChat/Rocket.Chat

Length of output: 1056


🏁 Script executed:

# Search for isRoomFederated usage in server code
rg -n "isRoomFederated" --type=ts --glob='**/server/**' | head -50

Repository: RocketChat/Rocket.Chat

Length of output: 3075


🏁 Script executed:

# Look for read receipt hooks or handlers
rg -n "beforeSetUserStatus|beforeUpdateUserStatusAndRoom|Meteor.methods.*receipt" --type=ts --glob='**/server/**' -A 3 | head -100

Repository: RocketChat/Rocket.Chat

Length of output: 48


🏁 Script executed:

cat -n apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts

Repository: RocketChat/Rocket.Chat

Length of output: 1056


🏁 Script executed:

cat -n apps/meteor/ee/app/message-read-receipt/server/hooks/afterSaveMessage.ts

Repository: RocketChat/Rocket.Chat

Length of output: 804


🏁 Script executed:

cat -n apps/meteor/ee/server/lib/message-read-receipt/ReadReceipt.ts | head -200

Repository: RocketChat/Rocket.Chat

Length of output: 6406


Backend read receipt processing must also be blocked for federated rooms.

The UI change correctly disables read receipt indicators in federated rooms, but backend processing continues unaddressed. The read receipt hooks (afterReadMessages and afterSaveMessage) and the ReadReceipt class methods (markMessagesAsRead, markMessageAsReadBySender, storeThreadMessagesReadReceipts) lack any federated room checks. This causes read receipt data to be stored in the database for federated rooms despite being hidden in the UI, creating data inconsistencies.

Add isRoomFederated checks to these backend methods to prevent unnecessary read receipt storage and processing for federated rooms.

🤖 Prompt for AI Agents
In apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx
around line 43 and in the backend read-receipt code (the afterReadMessages and
afterSaveMessage hooks and the ReadReceipt class methods markMessagesAsRead,
markMessageAsReadBySender, and storeThreadMessagesReadReceipts), the backend
still processes and stores read receipts for federated rooms; update each
backend entrypoint to call isRoomFederated(room) early and skip processing
(return/do nothing) when it returns true, ensuring you import or reference the
same isRoomFederated utility and pass the proper room object so no read receipt
DB writes or further processing occur for federated rooms.

const readReceiptsStoreUsers = useSetting('Message_Read_Receipt_Store_Users', false);
const apiEmbedEnabled = useSetting('API_Embed', false);
const showRealName = useSetting('UI_Use_Real_Name', false);
Expand Down
Loading