Skip to content

Commit 8208223

Browse files
authored
fix: prevent overwriting sent message on slow network (#1996)
1 parent 52f9c30 commit 8208223

File tree

2 files changed

+102
-3
lines changed

2 files changed

+102
-3
lines changed

src/components/Channel/Channel.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -611,8 +611,26 @@ const ChannelInner = <
611611
messageResponse = await channel.sendMessage(messageData);
612612
}
613613

614-
// replace it after send is completed
615-
if (messageResponse?.message) {
614+
let existingMessage;
615+
for (let i = channel.state.messages.length - 1; i >= 0; i--) {
616+
const msg = channel.state.messages[i];
617+
if (msg.id === messageData.id) {
618+
existingMessage = msg;
619+
break;
620+
}
621+
}
622+
623+
const responseTimestamp = new Date(messageResponse?.message?.updated_at || 0).getTime();
624+
const existingMessageTimestamp = existingMessage?.updated_at?.getTime() || 0;
625+
const responseIsTheNewest = responseTimestamp > existingMessageTimestamp;
626+
627+
// Replace the message payload after send is completed
628+
// We need to check for the newest message payload, because on slow network, the response can arrive later than WS events message.new, message.updated.
629+
// Always override existing message in status "sending"
630+
if (
631+
messageResponse?.message &&
632+
(responseIsTheNewest || existingMessage?.status === 'sending')
633+
) {
616634
updateMessage({
617635
...messageResponse.message,
618636
status: 'received',

src/components/Channel/__tests__/Channel.test.js

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,87 @@ describe('Channel', () => {
826826
expect(await findByText(message.text)).toBeInTheDocument();
827827
});
828828

829+
it('should not overwrite the message with send response, if already updated by WS events', async () => {
830+
let oldText;
831+
const newText = 'new text';
832+
const creationDate = new Date();
833+
const created_at = creationDate.toISOString();
834+
const updated_at = new Date(creationDate.getTime() + 1).toISOString();
835+
let hasSent = false;
836+
837+
jest.spyOn(channel, 'sendMessage').mockImplementationOnce((message) => {
838+
oldText = message.text;
839+
const finalMessage = { ...message, created_at, updated_at: created_at };
840+
useMockedApis(chatClient, [sendMessageApi(finalMessage)]);
841+
// both effects have to be emitted, otherwise the original message in status "sending" will not be filtered out (done when message.new is emitted) => and the message.updated event would add the updated message as a new message.
842+
createChannelEventDispatcher({
843+
created_at,
844+
message: {
845+
...finalMessage,
846+
text: newText,
847+
},
848+
user,
849+
})();
850+
createChannelEventDispatcher({
851+
created_at: updated_at,
852+
message: {
853+
...finalMessage,
854+
text: newText,
855+
updated_at,
856+
user,
857+
},
858+
type: 'message.updated',
859+
})();
860+
return channel.sendMessage(message);
861+
});
862+
863+
const { queryByText } = renderComponent(
864+
{ children: <MockMessageList /> },
865+
({ sendMessage }) => {
866+
if (!hasSent) {
867+
sendMessage(generateMessage());
868+
hasSent = true;
869+
}
870+
},
871+
);
872+
873+
await waitFor(async () => {
874+
expect(await queryByText(oldText, undefined, { timeout: 100 })).not.toBeInTheDocument();
875+
expect(await queryByText(newText, undefined, { timeout: 100 })).toBeInTheDocument();
876+
});
877+
});
878+
879+
it('should overwrite the message of status "sending" regardless of updated_at timestamp', async () => {
880+
let oldText;
881+
const newText = 'new text';
882+
const creationDate = new Date();
883+
const created_at = creationDate.toISOString();
884+
const updated_at = new Date(creationDate.getTime() - 1).toISOString();
885+
let hasSent = false;
886+
887+
jest.spyOn(channel, 'sendMessage').mockImplementationOnce((message) => {
888+
oldText = message.text;
889+
const finalMessage = { ...message, created_at, text: newText, updated_at };
890+
useMockedApis(chatClient, [sendMessageApi(finalMessage)]);
891+
return channel.sendMessage(message);
892+
});
893+
894+
const { queryByText } = renderComponent(
895+
{ children: <MockMessageList /> },
896+
({ sendMessage }) => {
897+
if (!hasSent) {
898+
sendMessage(generateMessage());
899+
hasSent = true;
900+
}
901+
},
902+
);
903+
904+
await waitFor(async () => {
905+
expect(await queryByText(oldText, undefined, { timeout: 100 })).not.toBeInTheDocument();
906+
expect(await queryByText(newText, undefined, { timeout: 100 })).toBeInTheDocument();
907+
});
908+
});
909+
829910
it('should mark the channel as read if a new message from another user comes in and the user is looking at the page', async () => {
830911
const markReadSpy = jest.spyOn(channel, 'markRead');
831912

@@ -862,7 +943,7 @@ describe('Channel', () => {
862943
const updatedThreadMessage = { ...threadMessage, text: newText };
863944
const dispatchUpdateMessageEvent = createChannelEventDispatcher(
864945
{ message: updatedThreadMessage },
865-
'message.update',
946+
'message.updated',
866947
);
867948
let threadStarterHasUpdatedText = false;
868949
renderComponent({}, ({ openThread, thread }) => {

0 commit comments

Comments
 (0)