Skip to content

Commit 99dcf8c

Browse files
chore: prevent federated room from being handled by read receipt (#37721)
Co-authored-by: Aleksander Nicacio da Silva <[email protected]>
1 parent c48eb68 commit 99dcf8c

File tree

12 files changed

+137
-19
lines changed

12 files changed

+137
-19
lines changed

.changeset/cold-chefs-rhyme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@rocket.chat/meteor": patch
3+
---
4+
5+
Disables read receipts indicators in federated rooms. This feature will be re-enabled when fully compatible with federation.

apps/meteor/app/api/server/v1/subscriptions.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Subscriptions } from '@rocket.chat/models';
1+
import { Rooms, Subscriptions } from '@rocket.chat/models';
22
import {
33
isSubscriptionsGetProps,
44
isSubscriptionsGetOneProps,
@@ -85,7 +85,12 @@ API.v1.addRoute(
8585
const { readThreads = false } = this.bodyParams;
8686
const roomId = 'rid' in this.bodyParams ? this.bodyParams.rid : this.bodyParams.roomId;
8787

88-
await readMessages(roomId, this.userId, readThreads);
88+
const room = await Rooms.findOneById(roomId);
89+
if (!room) {
90+
throw new Error('error-invalid-subscription');
91+
}
92+
93+
await readMessages(room, this.userId, readThreads);
8994

9095
return API.v1.success();
9196
},

apps/meteor/app/threads/server/methods/getThreadMessages.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ Meteor.methods<ServerMethods>({
5555
...(limit && { limit }),
5656
sort: { ts: -1 },
5757
}).toArray();
58-
callbacks.runAsync('afterReadMessages', room._id, { uid: user._id, tmid });
58+
59+
callbacks.runAsync('afterReadMessages', room, { uid: user._id, tmid });
5960

6061
return [thread, ...result];
6162
},

apps/meteor/client/components/message/list/MessageListContext.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export type MessageListContextValue = {
4545
messageListRef?: RefCallback<HTMLElement | undefined>;
4646
};
4747

48-
export const MessageListContext = createContext<MessageListContextValue>({
48+
export const messageListContextDefaultValue: MessageListContextValue = {
4949
autoTranslate: {
5050
showAutoTranslate: () => false,
5151
autoTranslateLanguage: undefined,
@@ -74,7 +74,9 @@ export const MessageListContext = createContext<MessageListContextValue>({
7474
formatTime: () => '',
7575
formatDate: () => '',
7676
messageListRef: undefined,
77-
});
77+
};
78+
79+
export const MessageListContext = createContext<MessageListContextValue>(messageListContextDefaultValue);
7880

7981
export const useShowTranslated: MessageListContextValue['autoTranslate']['showAutoTranslate'] = (...args) =>
8082
useContext(MessageListContext).autoTranslate.showAutoTranslate(...args);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { mockAppRoot } from '@rocket.chat/mock-providers';
2+
import { renderHook } from '@testing-library/react';
3+
4+
import { useReadReceiptsDetailsAction } from './useReadReceiptsDetailsAction';
5+
import { createFakeMessage } from '../../../../tests/mocks/data';
6+
import { useMessageListReadReceipts } from '../list/MessageListContext';
7+
8+
jest.mock('../list/MessageListContext', () => ({
9+
useMessageListReadReceipts: jest.fn(),
10+
}));
11+
12+
const useMessageListReadReceiptsMocked = jest.mocked(useMessageListReadReceipts);
13+
14+
describe('useReadReceiptsDetailsAction', () => {
15+
const message = createFakeMessage({ _id: 'messageId' });
16+
17+
afterEach(() => {
18+
jest.clearAllMocks();
19+
});
20+
21+
it('should return null if read receipts are not enabled', () => {
22+
useMessageListReadReceiptsMocked.mockReturnValue({ enabled: false, storeUsers: true });
23+
24+
const { result } = renderHook(() => useReadReceiptsDetailsAction(message), { wrapper: mockAppRoot().build() });
25+
26+
expect(result.current).toBeNull();
27+
});
28+
29+
it('should return null if read receipts store users is not enabled', () => {
30+
useMessageListReadReceiptsMocked.mockReturnValue({ enabled: true, storeUsers: false });
31+
32+
const { result } = renderHook(() => useReadReceiptsDetailsAction(message), { wrapper: mockAppRoot().build() });
33+
34+
expect(result.current).toBeNull();
35+
});
36+
37+
it('should return a message action config', () => {
38+
useMessageListReadReceiptsMocked.mockReturnValue({ enabled: true, storeUsers: true });
39+
40+
const { result } = renderHook(() => useReadReceiptsDetailsAction(message), { wrapper: mockAppRoot().build() });
41+
42+
expect(result.current).toEqual(
43+
expect.objectContaining({
44+
id: 'receipt-detail',
45+
icon: 'check-double',
46+
label: 'Read_Receipts',
47+
}),
48+
);
49+
});
50+
});

apps/meteor/client/components/message/variants/RoomMessage.spec.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { mockAppRoot } from '@rocket.chat/mock-providers';
33
import { render, screen } from '@testing-library/react';
44

55
import RoomMessage from './RoomMessage';
6+
import { MessageListContext, messageListContextDefaultValue } from '../list/MessageListContext';
67

78
const message: IMessage = {
89
ts: new Date('2021-10-27T00:00:00.000Z'),
@@ -106,3 +107,53 @@ it('should show ignored message', () => {
106107
expect(screen.queryByText('message body')).not.toBeInTheDocument();
107108
expect(screen.getByRole('button', { name: 'Message_Ignored' })).toBeInTheDocument();
108109
});
110+
111+
it('should show read receipt', () => {
112+
render(
113+
<RoomMessage
114+
message={message}
115+
sequential={false}
116+
all={false}
117+
mention={false}
118+
unread={false}
119+
ignoredUser={false}
120+
showUserAvatar={true}
121+
/>,
122+
{
123+
wrapper: mockAppRoot()
124+
.wrap((children) => (
125+
<MessageListContext.Provider value={{ ...messageListContextDefaultValue, readReceipts: { enabled: true, storeUsers: false } }}>
126+
{children}
127+
</MessageListContext.Provider>
128+
))
129+
.build(),
130+
},
131+
);
132+
133+
expect(screen.getByRole('status', { name: 'Message_viewed' })).toBeInTheDocument();
134+
});
135+
136+
it('should not show read receipt if receipt is disabled', () => {
137+
render(
138+
<RoomMessage
139+
message={message}
140+
sequential={false}
141+
all={false}
142+
mention={false}
143+
unread={false}
144+
ignoredUser={false}
145+
showUserAvatar={true}
146+
/>,
147+
{
148+
wrapper: mockAppRoot()
149+
.wrap((children) => (
150+
<MessageListContext.Provider value={{ ...messageListContextDefaultValue, readReceipts: { enabled: false, storeUsers: false } }}>
151+
{children}
152+
</MessageListContext.Provider>
153+
))
154+
.build(),
155+
},
156+
);
157+
158+
expect(screen.queryByRole('status', { name: 'Message_viewed' })).not.toBeInTheDocument();
159+
});

apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isThreadMainMessage } from '@rocket.chat/core-typings';
1+
import { isRoomFederated, isThreadMainMessage } from '@rocket.chat/core-typings';
22
import { useLayout, useUser, useUserPreference, useSetting, useEndpoint, useSearchParameter } from '@rocket.chat/ui-contexts';
33
import type { ReactNode, RefCallback } from 'react';
44
import { useMemo, memo } from 'react';
@@ -40,7 +40,7 @@ const MessageListProvider = ({ children, messageListRef, attachmentDimension }:
4040
const { isMobile } = useLayout();
4141

4242
const autoLinkDomains = useSetting('Message_CustomDomain_AutoLink', '');
43-
const readReceiptsEnabled = useSetting('Message_Read_Receipt_Enabled', false);
43+
const readReceiptsEnabled = useSetting('Message_Read_Receipt_Enabled', false) && !isRoomFederated(room);
4444
const readReceiptsStoreUsers = useSetting('Message_Read_Receipt_Store_Users', false);
4545
const apiEmbedEnabled = useSetting('API_Embed', false);
4646
const showRealName = useSetting('UI_Use_Real_Name', false);

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import { MessageReads } from '@rocket.chat/core-services';
2-
import type { IUser, IRoom, IMessage } from '@rocket.chat/core-typings';
2+
import { type IUser, type IRoom, type IMessage, isRoomFederated } from '@rocket.chat/core-typings';
33

44
import { settings } from '../../../../../app/settings/server';
55
import { callbacks } from '../../../../../lib/callbacks';
66
import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadReceipt';
77

88
callbacks.add(
99
'afterReadMessages',
10-
async (rid: IRoom['_id'], params: { uid: IUser['_id']; lastSeen?: Date; tmid?: IMessage['_id'] }) => {
10+
async (room: IRoom, params: { uid: IUser['_id']; lastSeen?: Date; tmid?: IMessage['_id'] }) => {
1111
if (!settings.get('Message_Read_Receipt_Enabled')) {
1212
return;
1313
}
14+
// Rooms federated are not supported yet
15+
if (isRoomFederated(room)) {
16+
return;
17+
}
1418
const { uid, lastSeen, tmid } = params;
1519

1620
if (tmid) {
1721
await MessageReads.readThread(uid, tmid);
1822
} else if (lastSeen) {
19-
await ReadReceipt.markMessagesAsRead(rid, uid, lastSeen);
23+
await ReadReceipt.markMessagesAsRead(room._id, uid, lastSeen);
2024
}
2125
},
2226
callbacks.priority.MEDIUM,

apps/meteor/lib/callbacks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ interface EventLikeCallbackSignatures {
4343
'afterDeleteMessage': (message: IMessage, params: { room: IRoom; user: IUser }) => void;
4444
'workspaceLicenseChanged': (license: string) => void;
4545
'workspaceLicenseRemoved': () => void;
46-
'afterReadMessages': (rid: IRoom['_id'], params: { uid: IUser['_id']; lastSeen?: Date; tmid?: IMessage['_id'] }) => void;
46+
'afterReadMessages': (room: IRoom, params: { uid: IUser['_id']; lastSeen?: Date; tmid?: IMessage['_id'] }) => void;
4747
'beforeReadMessages': (rid: IRoom['_id'], uid: IUser['_id']) => void;
4848
'afterDeleteUser': (user: IUser) => void;
4949
'afterFileUpload': (params: { user: IUser; room: IRoom; message: IMessage }) => void;

apps/meteor/server/lib/readMessages.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,25 @@ import { NotificationQueue, Subscriptions } from '@rocket.chat/models';
44
import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/server/lib/notifyListener';
55
import { callbacks } from '../../lib/callbacks';
66

7-
export async function readMessages(rid: IRoom['_id'], uid: IUser['_id'], readThreads: boolean): Promise<void> {
8-
await callbacks.run('beforeReadMessages', rid, uid);
7+
export async function readMessages(room: IRoom, uid: IUser['_id'], readThreads: boolean): Promise<void> {
8+
await callbacks.run('beforeReadMessages', room._id, uid);
99

1010
const projection = { ls: 1, tunread: 1, alert: 1, ts: 1 };
11-
const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, uid, { projection });
11+
const sub = await Subscriptions.findOneByRoomIdAndUserId(room._id, uid, { projection });
1212
if (!sub) {
1313
throw new Error('error-invalid-subscription');
1414
}
1515

1616
// do not mark room as read if there are still unread threads
1717
const alert = !!(sub.alert && !readThreads && sub.tunread && sub.tunread.length > 0);
1818

19-
const setAsReadResponse = await Subscriptions.setAsReadByRoomIdAndUserId(rid, uid, readThreads, alert);
19+
const setAsReadResponse = await Subscriptions.setAsReadByRoomIdAndUserId(room._id, uid, readThreads, alert);
2020
if (setAsReadResponse.modifiedCount) {
21-
void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid);
21+
void notifyOnSubscriptionChangedByRoomIdAndUserId(room._id, uid);
2222
}
2323

2424
await NotificationQueue.clearQueueByUserId(uid);
2525

2626
const lastSeen = sub.ls || sub.ts;
27-
callbacks.runAsync('afterReadMessages', rid, { uid, lastSeen });
27+
callbacks.runAsync('afterReadMessages', room, { uid, lastSeen });
2828
}

0 commit comments

Comments
 (0)