Skip to content

Commit f1bf6fa

Browse files
authored
fix: do not increase count of prepended VirtualizedMessageList messages of status "sending" or "failed" (#1972)
* fix: do not increase count of prepended VirtualizedMessageList messages of status "sending" * fix: do not increase count of prepended VirtualizedMessageList messages of status "failed" * fix: keep messages without status in the prepend count * test: ignore messages of status "sending" & "failed" from prepend count
1 parent 5c6a58f commit f1bf6fa

File tree

2 files changed

+65
-14
lines changed

2 files changed

+65
-14
lines changed

src/components/MessageList/__tests__/VirtualizedMessageList.test.js

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -110,29 +110,59 @@ describe('usePrependedMessagesCount', () => {
110110
return <div>{prependCount}</div>;
111111
};
112112

113-
it('calculates the prepended messages using the id prop', async () => {
113+
const expectPrependCount = (count, root) => {
114+
expect(root.findByType('div').props.children).toStrictEqual(count);
115+
};
116+
117+
it('determines 0 prepended messages for empty message list', async () => {
114118
const render = await renderer.create(<TestCase messages={[]} />);
115-
const expectPrependCount = (count) => {
116-
expect(render.root.findByType('div').props.children).toStrictEqual(count);
117-
};
118119

119-
expectPrependCount(0);
120+
expectPrependCount(0, render.root);
121+
});
122+
123+
const messageBatch = (ids, status) => ids.map((id) => ({ id, status }));
124+
const firstBatch = (status) => messageBatch(['a'], status);
125+
const secondBatch = (status) => messageBatch(['c', 'b'], status);
126+
const thirdBatch = (status) => messageBatch(['e', 'd'], status);
127+
128+
const testPrependCount = async (status, first, second, third) => {
129+
const firstMessage = firstBatch(status);
130+
const secondMsgBatch = secondBatch(status);
131+
const thirdMsgBatch = thirdBatch(status);
132+
133+
const render = await renderer.create(<TestCase messages={[]} />);
120134

121135
await renderer.act(async () => {
122-
await render.update(<TestCase messages={[{ id: 'a' }]} />);
123-
expectPrependCount(0);
136+
await render.update(<TestCase messages={[...firstMessage]} />);
137+
expectPrependCount(first, render.root);
124138
});
125139

126140
await renderer.act(async () => {
127-
await render.update(<TestCase messages={[{ id: 'c' }, { id: 'b' }, { id: 'a' }]} />);
128-
expectPrependCount(2);
141+
await render.update(<TestCase messages={[...secondMsgBatch, ...firstMessage]} />);
142+
expectPrependCount(second, render.root);
129143
});
130144

131145
await renderer.act(async () => {
132146
await render.update(
133-
<TestCase messages={[{ id: 'e' }, { id: 'd' }, { id: 'c' }, { id: 'b' }, { id: 'a' }]} />,
147+
<TestCase messages={[...thirdMsgBatch, ...secondMsgBatch, ...firstMessage]} />,
134148
);
135-
expectPrependCount(4);
149+
expectPrependCount(third, render.root);
136150
});
151+
};
152+
153+
it('calculates the prepended count for messages of status "received"', async () => {
154+
await testPrependCount('received', 0, 2, 4);
155+
});
156+
157+
it('ignores the messages of status "sending" from the prepended messages count', async () => {
158+
await testPrependCount('sending', 0, 0, 0);
159+
});
160+
161+
it('ignores the messages of status "failed" from the prepended messages count', async () => {
162+
await testPrependCount('failed', 0, 0, 0);
163+
});
164+
165+
it('calculates the prepended messages count for the messages of status undefined', async () => {
166+
await testPrependCount(undefined, 0, 2, 4);
137167
});
138168
});

src/components/MessageList/hooks/usePrependMessagesCount.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { StreamMessage } from '../../../context/ChannelStateContext';
44

55
import type { DefaultStreamChatGenerics } from '../../../types/types';
66

7+
const STATUSES_EXCLUDED_FROM_PREPEND = ['sending', 'failed'];
8+
79
export function usePrependedMessagesCount<
810
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
911
>(messages: StreamMessage<StreamChatGenerics>[], hasDateSeparator: boolean) {
@@ -30,14 +32,33 @@ export function usePrependedMessagesCount<
3032
earliestMessageId.current = currentFirstMessageId;
3133
// if new messages were prepended, find out how many
3234
// start with this number because there cannot be fewer prepended items than before
35+
let adjustPrependedMessageCount = 0;
3336
for (let i = previousNumItemsPrepended.current; i < messages.length; i += 1) {
37+
// Optimistic UI update, when sending messages, can lead to a situation, when
38+
// the order of the messages changes for a moment. This can happen, when a user
39+
// sends multiple messages withing few milliseconds. E.g. we send a message A
40+
// then message B. At first we have message array with both messages of status "sending"
41+
// then response for message A is received with a new - later - created_at timestamp
42+
// this leads to rearrangement of 1.B ("sending"), 2.A ("received"). Still firstMessageId.current
43+
// points to message A, but now this message has index 1 => previousNumItemsPrepended.current === 1
44+
// That in turn leads to incorrect index calculation in VirtualizedMessageList trying to access a message
45+
// at non-existent index. Therefore, we ignore messages of status "sending" / "failed" in order they are
46+
// not considered as prepended messages.
47+
if (
48+
messages[i]?.status &&
49+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
50+
STATUSES_EXCLUDED_FROM_PREPEND.includes(messages[i].status!) &&
51+
messages[i].id !== firstMessageId.current
52+
) {
53+
adjustPrependedMessageCount++;
54+
}
3455
if (messages[i].id === firstMessageId.current) {
35-
previousNumItemsPrepended.current = i;
36-
return i;
56+
previousNumItemsPrepended.current = i - adjustPrependedMessageCount;
57+
return previousNumItemsPrepended.current;
3758
}
3859
}
3960

40-
// if no match has found, we have jumped - reset the prepend item count.
61+
// if no match has found, we have jumped - reset the prepended item count.
4162
firstMessageId.current = currentFirstMessageId;
4263
previousNumItemsPrepended.current = 0;
4364
return 0;

0 commit comments

Comments
 (0)