diff --git a/.changeset/cold-chefs-rhyme.md b/.changeset/cold-chefs-rhyme.md new file mode 100644 index 0000000000000..5dc1bbdc43c81 --- /dev/null +++ b/.changeset/cold-chefs-rhyme.md @@ -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. diff --git a/apps/meteor/app/api/server/v1/subscriptions.ts b/apps/meteor/app/api/server/v1/subscriptions.ts index a3192aeb9ec14..b6e406bbfc969 100644 --- a/apps/meteor/app/api/server/v1/subscriptions.ts +++ b/apps/meteor/app/api/server/v1/subscriptions.ts @@ -1,4 +1,4 @@ -import { Subscriptions } from '@rocket.chat/models'; +import { Rooms, Subscriptions } from '@rocket.chat/models'; import { isSubscriptionsGetProps, isSubscriptionsGetOneProps, @@ -85,7 +85,12 @@ API.v1.addRoute( const { readThreads = false } = this.bodyParams; const roomId = 'rid' in this.bodyParams ? this.bodyParams.rid : this.bodyParams.roomId; - await readMessages(roomId, this.userId, readThreads); + const room = await Rooms.findOneById(roomId); + if (!room) { + throw new Error('error-invalid-subscription'); + } + + await readMessages(room, this.userId, readThreads); return API.v1.success(); }, diff --git a/apps/meteor/app/threads/server/methods/getThreadMessages.ts b/apps/meteor/app/threads/server/methods/getThreadMessages.ts index 8ae31130df1b0..6e5fa21306846 100644 --- a/apps/meteor/app/threads/server/methods/getThreadMessages.ts +++ b/apps/meteor/app/threads/server/methods/getThreadMessages.ts @@ -55,7 +55,8 @@ Meteor.methods({ ...(limit && { limit }), sort: { ts: -1 }, }).toArray(); - callbacks.runAsync('afterReadMessages', room._id, { uid: user._id, tmid }); + + callbacks.runAsync('afterReadMessages', room, { uid: user._id, tmid }); return [thread, ...result]; }, diff --git a/apps/meteor/client/components/message/list/MessageListContext.tsx b/apps/meteor/client/components/message/list/MessageListContext.tsx index 45a1e4239ff0c..1f462d2539398 100644 --- a/apps/meteor/client/components/message/list/MessageListContext.tsx +++ b/apps/meteor/client/components/message/list/MessageListContext.tsx @@ -45,7 +45,7 @@ export type MessageListContextValue = { messageListRef?: RefCallback; }; -export const MessageListContext = createContext({ +export const messageListContextDefaultValue: MessageListContextValue = { autoTranslate: { showAutoTranslate: () => false, autoTranslateLanguage: undefined, @@ -74,7 +74,9 @@ export const MessageListContext = createContext({ formatTime: () => '', formatDate: () => '', messageListRef: undefined, -}); +}; + +export const MessageListContext = createContext(messageListContextDefaultValue); export const useShowTranslated: MessageListContextValue['autoTranslate']['showAutoTranslate'] = (...args) => useContext(MessageListContext).autoTranslate.showAutoTranslate(...args); diff --git a/apps/meteor/client/components/message/toolbar/useReadReceiptsDetailsAction.spec.tsx b/apps/meteor/client/components/message/toolbar/useReadReceiptsDetailsAction.spec.tsx new file mode 100644 index 0000000000000..6ae118d3d4295 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useReadReceiptsDetailsAction.spec.tsx @@ -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', + }), + ); + }); +}); diff --git a/apps/meteor/client/components/message/variants/RoomMessage.spec.tsx b/apps/meteor/client/components/message/variants/RoomMessage.spec.tsx index 51fe1b0d97a82..fe931e262a352 100644 --- a/apps/meteor/client/components/message/variants/RoomMessage.spec.tsx +++ b/apps/meteor/client/components/message/variants/RoomMessage.spec.tsx @@ -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'), @@ -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( + , + { + wrapper: mockAppRoot() + .wrap((children) => ( + + {children} + + )) + .build(), + }, + ); + + expect(screen.getByRole('status', { name: 'Message_viewed' })).toBeInTheDocument(); +}); + +it('should not show read receipt if receipt is disabled', () => { + render( + , + { + wrapper: mockAppRoot() + .wrap((children) => ( + + {children} + + )) + .build(), + }, + ); + + expect(screen.queryByRole('status', { name: 'Message_viewed' })).not.toBeInTheDocument(); +}); diff --git a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx index 5981b8b72ab7e..af366350fad68 100644 --- a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx +++ b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx @@ -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'; @@ -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); const readReceiptsStoreUsers = useSetting('Message_Read_Receipt_Store_Users', false); const apiEmbedEnabled = useSetting('API_Embed', false); const showRealName = useSetting('UI_Use_Real_Name', false); diff --git a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts index f96a9a773fecc..d18834ac70865 100644 --- a/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts +++ b/apps/meteor/ee/app/message-read-receipt/server/hooks/afterReadMessages.ts @@ -1,5 +1,5 @@ import { MessageReads } from '@rocket.chat/core-services'; -import type { IUser, IRoom, IMessage } from '@rocket.chat/core-typings'; +import { type IUser, type IRoom, type IMessage, isRoomFederated } from '@rocket.chat/core-typings'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; @@ -7,16 +7,20 @@ import { ReadReceipt } from '../../../../server/lib/message-read-receipt/ReadRec callbacks.add( 'afterReadMessages', - async (rid: IRoom['_id'], params: { uid: IUser['_id']; lastSeen?: Date; tmid?: IMessage['_id'] }) => { + async (room: IRoom, params: { uid: IUser['_id']; lastSeen?: Date; tmid?: IMessage['_id'] }) => { if (!settings.get('Message_Read_Receipt_Enabled')) { return; } + // Rooms federated are not supported yet + if (isRoomFederated(room)) { + return; + } const { uid, lastSeen, tmid } = params; if (tmid) { await MessageReads.readThread(uid, tmid); } else if (lastSeen) { - await ReadReceipt.markMessagesAsRead(rid, uid, lastSeen); + await ReadReceipt.markMessagesAsRead(room._id, uid, lastSeen); } }, callbacks.priority.MEDIUM, diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index d55a6c64ae96f..c914e60d940a7 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -43,7 +43,7 @@ interface EventLikeCallbackSignatures { 'afterDeleteMessage': (message: IMessage, params: { room: IRoom; user: IUser }) => void; 'workspaceLicenseChanged': (license: string) => void; 'workspaceLicenseRemoved': () => void; - 'afterReadMessages': (rid: IRoom['_id'], params: { uid: IUser['_id']; lastSeen?: Date; tmid?: IMessage['_id'] }) => void; + 'afterReadMessages': (room: IRoom, params: { uid: IUser['_id']; lastSeen?: Date; tmid?: IMessage['_id'] }) => void; 'beforeReadMessages': (rid: IRoom['_id'], uid: IUser['_id']) => void; 'afterDeleteUser': (user: IUser) => void; 'afterFileUpload': (params: { user: IUser; room: IRoom; message: IMessage }) => void; diff --git a/apps/meteor/server/lib/readMessages.ts b/apps/meteor/server/lib/readMessages.ts index 3be43a875fac0..6ae43ef1ad27e 100644 --- a/apps/meteor/server/lib/readMessages.ts +++ b/apps/meteor/server/lib/readMessages.ts @@ -4,11 +4,11 @@ import { NotificationQueue, Subscriptions } from '@rocket.chat/models'; import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/server/lib/notifyListener'; import { callbacks } from '../../lib/callbacks'; -export async function readMessages(rid: IRoom['_id'], uid: IUser['_id'], readThreads: boolean): Promise { - await callbacks.run('beforeReadMessages', rid, uid); +export async function readMessages(room: IRoom, uid: IUser['_id'], readThreads: boolean): Promise { + await callbacks.run('beforeReadMessages', room._id, uid); const projection = { ls: 1, tunread: 1, alert: 1, ts: 1 }; - const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, uid, { projection }); + const sub = await Subscriptions.findOneByRoomIdAndUserId(room._id, uid, { projection }); if (!sub) { throw new Error('error-invalid-subscription'); } @@ -16,13 +16,13 @@ export async function readMessages(rid: IRoom['_id'], uid: IUser['_id'], readThr // do not mark room as read if there are still unread threads const alert = !!(sub.alert && !readThreads && sub.tunread && sub.tunread.length > 0); - const setAsReadResponse = await Subscriptions.setAsReadByRoomIdAndUserId(rid, uid, readThreads, alert); + const setAsReadResponse = await Subscriptions.setAsReadByRoomIdAndUserId(room._id, uid, readThreads, alert); if (setAsReadResponse.modifiedCount) { - void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, uid); + void notifyOnSubscriptionChangedByRoomIdAndUserId(room._id, uid); } await NotificationQueue.clearQueueByUserId(uid); const lastSeen = sub.ls || sub.ts; - callbacks.runAsync('afterReadMessages', rid, { uid, lastSeen }); + callbacks.runAsync('afterReadMessages', room, { uid, lastSeen }); } diff --git a/apps/meteor/server/methods/readMessages.ts b/apps/meteor/server/methods/readMessages.ts index 217ba948b093a..b698a440a534e 100644 --- a/apps/meteor/server/methods/readMessages.ts +++ b/apps/meteor/server/methods/readMessages.ts @@ -34,6 +34,6 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'readMessages' }); } - await readMessages(rid, userId, readThreads); + await readMessages(room, userId, readThreads); }, }); diff --git a/apps/meteor/server/methods/readThreads.ts b/apps/meteor/server/methods/readThreads.ts index 8f3652b7012cf..86a2e816a19bf 100644 --- a/apps/meteor/server/methods/readThreads.ts +++ b/apps/meteor/server/methods/readThreads.ts @@ -45,7 +45,7 @@ Meteor.methods({ await callbacks.run('beforeReadMessages', thread.rid, user?._id); if (user?._id) { await readThread({ userId: user._id, rid: thread.rid, tmid }); - callbacks.runAsync('afterReadMessages', room._id, { uid: user._id, tmid }); + callbacks.runAsync('afterReadMessages', room, { uid: user._id, tmid }); } }, });