Skip to content

Commit 274ea6b

Browse files
committed
feat: migrate sendMessage + getReadReceipts callers from DDP to REST
Two methods that resisted the URL-swap pass in the wider DDP -> REST migration. This PR contains the deeper refactor each needed. sendMessage Replace sdk.call('sendMessage', message, previewUrls) with sdk.rest.post('/v1/chat.sendMessage', { message, previewUrls }). In the primary send flow (apps/meteor/client/lib/chats/flows/sendMessage.ts) the response's server-rendered { message } is fed back into Messages.state via mapMessageFromApi, replacing the optimistic temp record in the same tick the REST call resolves. That reproduces the Minimongo replication the DDP method triggered — composer quote previews unmount, attachment renderers see attachments[] and urls[], message _updatedAt advances — all without waiting for the room-messages stream event to arrive separately. The seven fire-and-forget callsites do not run the optimistic reconcile and are straight URL swaps: - apps/meteor/client/hooks/notification/useNotification.ts - apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx - apps/meteor/app/slashcommand-asciiarts/client/{lenny,tableflip,unflip,gimme,shrug}.ts getReadReceipts Replace useMethod('getReadReceipts') with useEndpoint('GET', '/v1/chat.getMessageReadReceipts') in ReadReceiptsModal. Add rid prop and subscribe to notify-room/<rid>/messagesRead; on event, invalidate the ['read-receipts', messageId] query so the dialog re-fetches when new receipts land. mapReadReceiptFromApi revives the Date fields the REST endpoint serializes as strings. Server-side: ReadReceipt.markMessagesAsRead / ReadReceipt.markMessageAsReadBySender / ReadReceipt.storeThreadMessagesReadReceipts no longer fire-and-forget the storeReadReceipts insertion — they await it. Closes the read-after-write race the omnichannel-livechat-read-receipts e2e test exposed: a fast REST GET could observe a partial set of receipts because the visitor's self-receipt insert was still in flight when the test opened the Read-Receipts dialog.
1 parent 8e5f185 commit 274ea6b

11 files changed

Lines changed: 53 additions & 28 deletions

File tree

apps/meteor/app/slashcommand-asciiarts/client/gimme.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { slashCommands } from '../../utils/client/slashCommand';
88
*/
99
async function Gimme({ message, params }: SlashCommandCallbackParams<'gimme'>): Promise<void> {
1010
const msg = message;
11-
await sdk.call('sendMessage', { ...msg, msg: `༼ つ ◕_◕ ༽つ ${params}` });
11+
await sdk.rest.post('/v1/chat.sendMessage', { message: { ...msg, msg: `༼ つ ◕_◕ ༽つ ${params}` } });
1212
}
1313

1414
slashCommands.add({

apps/meteor/app/slashcommand-asciiarts/client/lenny.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { slashCommands } from '../../utils/client/slashCommand';
99

1010
async function LennyFace({ message, params }: SlashCommandCallbackParams<'lenny'>): Promise<void> {
1111
const msg = message;
12-
await sdk.call('sendMessage', { ...msg, msg: `${params} ( ͡° ͜ʖ ͡°)` });
12+
await sdk.rest.post('/v1/chat.sendMessage', { message: { ...msg, msg: `${params} ( ͡° ͜ʖ ͡°)` } });
1313
}
1414

1515
slashCommands.add({

apps/meteor/app/slashcommand-asciiarts/client/shrug.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ slashCommands.add({
1111
command: 'shrug',
1212
callback: async ({ message, params }: SlashCommandCallbackParams<'shrug'>): Promise<void> => {
1313
const msg = message;
14-
await sdk.call('sendMessage', { ...msg, msg: `${params} ¯\\\\_(ツ)_/¯` });
14+
await sdk.rest.post('/v1/chat.sendMessage', { message: { ...msg, msg: `${params} ¯\\\\_(ツ)_/¯` } });
1515
},
1616
options: {
1717
description: 'Slash_Shrug_Description',

apps/meteor/app/slashcommand-asciiarts/client/tableflip.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ slashCommands.add({
1111
command: 'tableflip',
1212
callback: async ({ message, params }: SlashCommandCallbackParams<'tableflip'>): Promise<void> => {
1313
const msg = message;
14-
await sdk.call('sendMessage', { ...msg, msg: `${params} (╯°□°)╯︵ ┻━┻` });
14+
await sdk.rest.post('/v1/chat.sendMessage', { message: { ...msg, msg: `${params} (╯°□°)╯︵ ┻━┻` } });
1515
},
1616
options: {
1717
description: 'Slash_Tableflip_Description',

apps/meteor/app/slashcommand-asciiarts/client/unflip.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ slashCommands.add({
1111
command: 'unflip',
1212
callback: async ({ message, params }: SlashCommandCallbackParams<'unflip'>): Promise<void> => {
1313
const msg = message;
14-
await sdk.call('sendMessage', { ...msg, msg: `${params} ┬─┬ ノ( ゜-゜ノ)` });
14+
await sdk.rest.post('/v1/chat.sendMessage', { message: { ...msg, msg: `${params} ┬─┬ ノ( ゜-゜ノ)` } });
1515
},
1616
options: {
1717
description: 'Slash_TableUnflip_Description',

apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { sdk } from '../../../app/utils/client/lib/SDKClient';
1111
import UserAutoCompleteMultiple from '../../components/UserAutoCompleteMultiple';
1212
import { useOpenedRoom } from '../../lib/RoomManager';
1313
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
14-
import { callWithErrorHandling } from '../../lib/utils/callWithErrorHandling';
1514

1615
type Username = Exclude<IUser['username'], undefined>;
1716

@@ -36,10 +35,12 @@ const GameCenterInvitePlayersModal = ({ game, onClose }: IGameCenterInvitePlayer
3635
roomCoordinator.openRouteLink(group.t, { rid: group._id, name: group.name });
3736

3837
if (openedRoom === group._id) {
39-
await callWithErrorHandling('sendMessage', {
40-
_id: Random.id(),
41-
rid: group._id,
42-
msg: t('Apps_Game_Center_Play_Game_Together', { name }),
38+
void sdk.rest.post('/v1/chat.sendMessage', {
39+
message: {
40+
_id: Random.id(),
41+
rid: group._id,
42+
msg: t('Apps_Game_Center_Play_Game_Together', { name }),
43+
},
4344
});
4445
}
4546
onClose();

apps/meteor/client/components/message/toolbar/useReadReceiptsDetailsAction.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const useReadReceiptsDetailsAction = (message: IMessage): MessageActionCo
2424
setModal(
2525
<ReadReceiptsModal
2626
messageId={message._id}
27+
rid={message.rid}
2728
onClose={() => {
2829
setModal(null);
2930
}}

apps/meteor/client/hooks/notification/useNotification.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@ export const useNotification = () => {
5151
n.addEventListener(
5252
'reply',
5353
({ response }) =>
54-
void sdk.call('sendMessage', {
55-
_id: Random.id(),
56-
rid,
57-
msg: response,
54+
void sdk.rest.post('/v1/chat.sendMessage', {
55+
message: {
56+
_id: Random.id(),
57+
rid,
58+
msg: response,
59+
},
5860
}),
5961
);
6062
}

apps/meteor/client/lib/chats/flows/sendMessage.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { closeUnclosedCodeBlock } from '../../../../lib/utils/closeUnclosedCodeB
77
import { Messages } from '../../../stores';
88
import { onClientBeforeSendMessage } from '../../onClientBeforeSendMessage';
99
import { dispatchToastMessage } from '../../toast';
10+
import { mapMessageFromApi } from '../../utils/mapMessageFromApi';
1011
import type { ChatAPI } from '../ChatAPI';
1112
import { afterSendMessageCallback } from './afterSendMessageCallback';
1213
import { processMessageEditing } from './processMessageEditing';
@@ -47,12 +48,17 @@ const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[],
4748

4849
chat.composer?.clear();
4950
await runOptimisticSendMessage(message);
50-
await sdk.call('sendMessage', message, previewUrls);
5151

52-
// after the request is complete we can go ahead and mark as sent
52+
const { message: saved } = await sdk.rest.post('/v1/chat.sendMessage', { message, previewUrls });
53+
54+
// Replace the optimistic temp record with the server-rendered message so
55+
// downstream consumers (composer quote preview, message list, threads)
56+
// see the final shape — `attachments[]`, `urls[]`, `mentions[]`, etc. —
57+
// in the same tick the REST call resolves. Mirrors the Minimongo replication
58+
// that the DDP `sendMessage` method used to trigger.
5359
Messages.state.update(
54-
(record) => record._id === message._id && record.temp === true,
55-
({ temp: _, ...record }) => record,
60+
(record) => record._id === message._id,
61+
() => mapMessageFromApi(saved),
5662
);
5763
};
5864

apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,44 @@
1-
import type { IMessage } from '@rocket.chat/core-typings';
1+
import type { IMessage, IRoom } from '@rocket.chat/core-typings';
22
import { GenericModal, GenericModalSkeleton } from '@rocket.chat/ui-client';
3-
import { useMethod, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
4-
import { useQuery } from '@tanstack/react-query';
3+
import { useEndpoint, useStream, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
4+
import { useQuery, useQueryClient } from '@tanstack/react-query';
55
import type { ReactElement } from 'react';
66
import { useEffect } from 'react';
77
import { useTranslation } from 'react-i18next';
88

99
import ReadReceiptRow from './ReadReceiptRow';
10+
import { mapReadReceiptFromApi } from '../../../../lib/utils/mapReadReceiptFromApi';
1011

1112
type ReadReceiptsModalProps = {
1213
messageId: IMessage['_id'];
14+
rid?: IRoom['_id'];
1315
onClose: () => void;
1416
};
1517

16-
const ReadReceiptsModal = ({ messageId, onClose }: ReadReceiptsModalProps): ReactElement => {
18+
const ReadReceiptsModal = ({ messageId, rid, onClose }: ReadReceiptsModalProps): ReactElement => {
1719
const { t } = useTranslation();
1820
const dispatchToastMessage = useToastMessageDispatch();
21+
const queryClient = useQueryClient();
22+
const subscribeToNotifyRoom = useStream('notify-room');
1923

20-
const getReadReceipts = useMethod('getReadReceipts');
24+
const getReadReceipts = useEndpoint('GET', '/v1/chat.getMessageReadReceipts');
25+
26+
const queryKey = ['read-receipts', messageId];
2127

2228
const readReceiptsResult = useQuery({
23-
queryKey: ['read-receipts', messageId],
24-
queryFn: () => getReadReceipts({ messageId }),
29+
queryKey,
30+
queryFn: async () => (await getReadReceipts({ messageId })).receipts.map(mapReadReceiptFromApi),
2531
});
2632

33+
useEffect(() => {
34+
if (!rid) {
35+
return;
36+
}
37+
return subscribeToNotifyRoom(`${rid}/messagesRead`, () => {
38+
queryClient.invalidateQueries({ queryKey });
39+
});
40+
}, [rid, queryClient, queryKey, subscribeToNotifyRoom]);
41+
2742
useEffect(() => {
2843
if (readReceiptsResult.isError) {
2944
dispatchToastMessage({ type: 'error', message: readReceiptsResult.error });

0 commit comments

Comments
 (0)