Skip to content

Commit eda0844

Browse files
authored
fix: calculate message read status for the first message in a channel (#3055)
1 parent f853501 commit eda0844

File tree

8 files changed

+116
-29
lines changed

8 files changed

+116
-29
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@
139139
"emoji-mart": "^5.4.0",
140140
"react": "^19.0.0 || ^18.0.0 || ^17.0.0",
141141
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0",
142-
"stream-chat": "^9.27.2"
142+
"stream-chat": "^9.38.0"
143143
},
144144
"peerDependenciesMeta": {
145145
"@breezystack/lamejs": {
@@ -234,7 +234,7 @@
234234
"react": "^19.0.0",
235235
"react-dom": "^19.0.0",
236236
"semantic-release": "^25.0.2",
237-
"stream-chat": "^9.27.2",
237+
"stream-chat": "^9.38.0",
238238
"ts-jest": "^29.2.5",
239239
"typescript": "^5.4.5",
240240
"typescript-eslint": "^8.17.0"

src/components/ChannelPreview/__tests__/ChannelPreview.test.js

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -500,11 +500,11 @@ describe('ChannelPreview', () => {
500500
},
501501
render,
502502
);
503-
expectUnreadCountToBe(screen.getByTestId, unreadCount);
503+
await expectUnreadCountToBe(screen.getByTestId, unreadCount);
504504
await act(() => {
505505
dispatchNotificationMarkRead({ client });
506506
});
507-
expectUnreadCountToBe(screen.getByTestId, 0);
507+
await expectUnreadCountToBe(screen.getByTestId, 0);
508508
});
509509

510510
it('should set unread count to 0 for current channel', async () => {
@@ -518,11 +518,11 @@ describe('ChannelPreview', () => {
518518
},
519519
render,
520520
);
521-
expectUnreadCountToBe(screen.getByTestId, unreadCount);
521+
await expectUnreadCountToBe(screen.getByTestId, unreadCount);
522522
await act(() => {
523523
dispatchNotificationMarkRead({ channel: channelInPreview, client });
524524
});
525-
expectUnreadCountToBe(screen.getByTestId, 0);
525+
await expectUnreadCountToBe(screen.getByTestId, 0);
526526
});
527527

528528
it('should be ignored if not targeted for the current channel', async () => {
@@ -537,11 +537,11 @@ describe('ChannelPreview', () => {
537537
},
538538
render,
539539
);
540-
expectUnreadCountToBe(screen.getByTestId, unreadCount);
540+
await expectUnreadCountToBe(screen.getByTestId, unreadCount);
541541
await act(() => {
542542
dispatchNotificationMarkRead({ channel: activeChannel, client });
543543
});
544-
expectUnreadCountToBe(screen.getByTestId, unreadCount);
544+
await expectUnreadCountToBe(screen.getByTestId, unreadCount);
545545
});
546546
});
547547

@@ -558,7 +558,7 @@ describe('ChannelPreview', () => {
558558
},
559559
render,
560560
);
561-
expectUnreadCountToBe(screen.getByTestId, unreadCount);
561+
await expectUnreadCountToBe(screen.getByTestId, unreadCount);
562562
await act(() => {
563563
dispatchNotificationMarkUnread({
564564
channel: channelInPreview,
@@ -567,7 +567,7 @@ describe('ChannelPreview', () => {
567567
user: otherUser,
568568
});
569569
});
570-
expectUnreadCountToBe(screen.getByTestId, unreadCount);
570+
await expectUnreadCountToBe(screen.getByTestId, unreadCount);
571571
});
572572

573573
it('should be ignored if not targeted for the current channel', async () => {
@@ -582,7 +582,7 @@ describe('ChannelPreview', () => {
582582
},
583583
render,
584584
);
585-
expectUnreadCountToBe(screen.getByTestId, unreadCount);
585+
await expectUnreadCountToBe(screen.getByTestId, unreadCount);
586586
await act(() => {
587587
dispatchNotificationMarkUnread({
588588
channel: activeChannel,
@@ -591,23 +591,26 @@ describe('ChannelPreview', () => {
591591
user,
592592
});
593593
});
594-
expectUnreadCountToBe(screen.getByTestId, unreadCount);
594+
await expectUnreadCountToBe(screen.getByTestId, unreadCount);
595595
});
596596

597597
it("should set unread count from client's unread count state for active channel", async () => {
598598
const unreadCount = 0;
599+
const nextUnreadCount = 5;
599600
const activeChannel = c1;
600-
activeChannel.countUnread = () => unreadCount;
601+
jest
602+
.spyOn(activeChannel, 'countUnread')
603+
.mockReturnValueOnce(unreadCount)
604+
.mockReturnValueOnce(nextUnreadCount);
601605
renderComponent(
602606
{
603607
activeChannel,
604608
channel: activeChannel,
605609
},
606610
render,
607611
);
608-
expectUnreadCountToBe(screen.getByTestId, unreadCount);
612+
await expectUnreadCountToBe(screen.getByTestId, unreadCount);
609613

610-
const eventPayload = { unread_channels: 2, unread_messages: 5 };
611614
await act(() => {
612615
dispatchNotificationMarkUnread({
613616
channel: activeChannel,
@@ -616,24 +619,27 @@ describe('ChannelPreview', () => {
616619
user,
617620
});
618621
});
619-
expectUnreadCountToBe(screen.getByTestId, eventPayload.unread_messages);
622+
await expectUnreadCountToBe(screen.getByTestId, nextUnreadCount);
620623
});
621624

622625
it("should set unread count from client's unread count state for non-active channel", async () => {
623626
const unreadCount = 0;
627+
const nextUnreadCount = 5;
624628
const channelInPreview = c0;
625629
const activeChannel = c1;
626-
channelInPreview.countUnread = () => unreadCount;
630+
jest
631+
.spyOn(channelInPreview, 'countUnread')
632+
.mockReturnValueOnce(unreadCount)
633+
.mockReturnValueOnce(nextUnreadCount);
627634
renderComponent(
628635
{
629636
activeChannel,
630637
channel: channelInPreview,
631638
},
632639
render,
633640
);
634-
expectUnreadCountToBe(screen.getByTestId, unreadCount);
641+
await expectUnreadCountToBe(screen.getByTestId, unreadCount);
635642

636-
const eventPayload = { unread_channels: 2, unread_messages: 5 };
637643
await act(() => {
638644
dispatchNotificationMarkUnread({
639645
channel: channelInPreview,
@@ -642,7 +648,7 @@ describe('ChannelPreview', () => {
642648
user,
643649
});
644650
});
645-
expectUnreadCountToBe(screen.getByTestId, eventPayload.unread_messages);
651+
await expectUnreadCountToBe(screen.getByTestId, nextUnreadCount);
646652
});
647653
});
648654

src/components/MessageList/CustomNotification.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { PropsWithChildren } from 'react';
22
import React from 'react';
33
import clsx from 'clsx';
4+
import type { NotificationSeverity } from 'stream-chat';
45

56
export type CustomNotificationProps = {
6-
type: string;
7+
type?: NotificationSeverity | string;
78
active?: boolean;
89
className?: string;
910
};
@@ -19,9 +20,10 @@ const UnMemoizedCustomNotification = (
1920
<div
2021
aria-live='polite'
2122
className={clsx(
22-
`str-chat__custom-notification notification-${type}`,
23+
`str-chat__custom-notification`,
2324
`str-chat__notification`,
2425
`str-chat-react__notification`,
26+
{ [`notification-${type}`]: type },
2527
className,
2628
)}
2729
data-testid='custom-notification'

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('CustomNotification', () => {
2323
<div>
2424
<div
2525
aria-live="polite"
26-
class="str-chat__custom-notification notification-undefined str-chat__notification str-chat-react__notification"
26+
class="str-chat__custom-notification str-chat__notification str-chat-react__notification"
2727
data-testid="custom-notification"
2828
>
2929
children
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { renderHook } from '@testing-library/react';
2+
3+
import { generateMessage, generateUser } from '../../../../mock-builders';
4+
import { useLastOwnMessage } from '../useLastOwnMessage';
5+
6+
describe('useLastOwnMessage', () => {
7+
it('returns undefined when there are no messages', () => {
8+
const ownUser = generateUser();
9+
10+
const { result } = renderHook(() =>
11+
useLastOwnMessage({ messages: undefined, ownUserId: ownUser.id }),
12+
);
13+
14+
expect(result.current).toBeUndefined();
15+
});
16+
17+
it('returns undefined when there are no own messages', () => {
18+
const ownUser = generateUser();
19+
const otherUser = generateUser();
20+
const messages = [generateMessage({ user: otherUser })];
21+
22+
const { result } = renderHook(() =>
23+
useLastOwnMessage({ messages, ownUserId: ownUser.id }),
24+
);
25+
26+
expect(result.current).toBeUndefined();
27+
});
28+
29+
it('returns the first message when it is the only own message', () => {
30+
const ownUser = generateUser();
31+
const otherUser = generateUser();
32+
const ownMessage = generateMessage({ user: ownUser });
33+
const messages = [ownMessage, generateMessage({ user: otherUser })];
34+
35+
const { result } = renderHook(() =>
36+
useLastOwnMessage({ messages, ownUserId: ownUser.id }),
37+
);
38+
39+
expect(result.current).toBe(ownMessage);
40+
});
41+
42+
it('returns the latest own message', () => {
43+
const ownUser = generateUser();
44+
const otherUser = generateUser();
45+
const firstOwnMessage = generateMessage({ user: ownUser });
46+
const lastOwnMessage = generateMessage({ user: ownUser });
47+
const messages = [
48+
firstOwnMessage,
49+
generateMessage({ user: otherUser }),
50+
lastOwnMessage,
51+
generateMessage({ user: otherUser }),
52+
];
53+
54+
const { result } = renderHook(() =>
55+
useLastOwnMessage({ messages, ownUserId: ownUser.id }),
56+
);
57+
58+
expect(result.current).toBe(lastOwnMessage);
59+
});
60+
});

src/components/Poll/__tests__/PollCreationDialog.test.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ describe('PollCreationDialog', () => {
278278

279279
it('registers max vote count field error and prevents submission', async () => {
280280
const text = 'Abc';
281-
await renderComponent();
281+
const { channel } = await renderComponent();
282282
const nameInput = getNameInput();
283283
await act(async () => {
284284
await fireEvent.change(nameInput, { target: { value: text } });
@@ -296,6 +296,25 @@ describe('PollCreationDialog', () => {
296296
await fireEvent.change(maxVoteCountInput, { target: { value: '11' } });
297297
});
298298

299+
// stream-chat clamps max_votes_allowed on field change, so force a blur-validation
300+
// pass with raw out-of-range value through the state middleware.
301+
await act(async () => {
302+
const pollComposer = channel.messageComposer.pollComposer;
303+
const latestState = pollComposer.state.getLatestValue();
304+
const result = await pollComposer.stateMiddlewareExecutor.execute({
305+
eventName: 'handleFieldBlur',
306+
initialValue: {
307+
nextState: { ...latestState },
308+
previousState: { ...latestState },
309+
targetFields: { max_votes_allowed: '11' },
310+
},
311+
});
312+
313+
if (result.status !== 'discard') {
314+
pollComposer.state.next(result.state.nextState);
315+
}
316+
});
317+
299318
const maxVoteCountErrors = screen.getAllByTestId(
300319
MAX_VOTE_COUT_INPUT_FIELD_ERROR_TEST_ID,
301320
);

src/utils/findReverse.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const findReverse = <T>(
33
items: T[],
44
matches: (items: T) => boolean,
55
): T | undefined => {
6-
for (let i = items.length - 1; i > 0; i -= 1) {
6+
for (let i = items.length - 1; i >= 0; i -= 1) {
77
if (matches(items[i])) {
88
return items[i];
99
}

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11936,10 +11936,10 @@ stdin-discarder@^0.2.2:
1193611936
resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz#390037f44c4ae1a1ae535c5fe38dc3aba8d997be"
1193711937
integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==
1193811938

11939-
stream-chat@^9.27.2:
11940-
version "9.27.2"
11941-
resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.27.2.tgz#5b41173e513f3606c47c93f391693b589e663968"
11942-
integrity sha512-OdALDzg8lO8CAdl8deydJ1+O4wJ7mM9dPLeCwDppq/OQ4aFIS9X38P+IdXPcOCsgSS97UoVUuxD2/excC5PEeg==
11939+
stream-chat@^9.38.0:
11940+
version "9.38.0"
11941+
resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.38.0.tgz#5c13eb8bbc2fa4adb774687b0c9c51f129d1b458"
11942+
integrity sha512-nyTFKHnhGfk1Op/xuZzPKzM9uNTy4TBma69+ApwGj/UtrK2pT6rSaU0Qy/oAqub+Bh7jR2/5vlV/8FWJ2BObFg==
1194311943
dependencies:
1194411944
"@types/jsonwebtoken" "^9.0.8"
1194511945
"@types/ws" "^8.5.14"

0 commit comments

Comments
 (0)