Skip to content

Commit 74263c5

Browse files
authored
feat: reflect user.messages.deleted WS event (#2799)
1 parent 68b5d26 commit 74263c5

File tree

9 files changed

+197
-23
lines changed

9 files changed

+197
-23
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@
142142
"emoji-mart": "^5.4.0",
143143
"react": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.14.0",
144144
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.14.0",
145-
"stream-chat": "^9.15.0"
145+
"stream-chat": "^9.17.0"
146146
},
147147
"peerDependenciesMeta": {
148148
"@breezystack/lamejs": {
@@ -236,7 +236,7 @@
236236
"react": "^19.0.0",
237237
"react-dom": "^19.0.0",
238238
"semantic-release": "^24.2.3",
239-
"stream-chat": "^9.15.0",
239+
"stream-chat": "^9.17.0",
240240
"ts-jest": "^29.2.5",
241241
"typescript": "^5.4.5",
242242
"typescript-eslint": "^8.17.0"

src/components/Channel/Channel.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,11 @@ const ChannelInner = (
425425
});
426426
}
427427

428+
// ignore the event if it is not targeted at the current channel.
429+
// Event targeted at this channel or globally targeted event should lead to state refresh
430+
if (event.type === 'user.messages.deleted' && event.cid && event.cid !== channel.cid)
431+
return;
432+
428433
if (event.type === 'user.watching.start' || event.type === 'user.watching.stop')
429434
return;
430435

@@ -568,6 +573,7 @@ const ChannelInner = (
568573
client.on('connection.recovered', handleEvent);
569574
client.on('user.updated', handleEvent);
570575
client.on('user.deleted', handleEvent);
576+
client.on('user.messages.deleted', handleEvent);
571577
channel.on(handleEvent);
572578
}
573579
})();

src/components/ChannelPreview/ChannelPreview.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,12 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {
135135
useEffect(() => {
136136
refreshUnreadCount();
137137

138-
const handleEvent = () => {
138+
const handleEvent = (event: Event) => {
139+
const deletedMessagesInAnotherChannel =
140+
event.type === 'user.messages.deleted' && event.cid && event.cid !== channel.cid;
141+
142+
if (deletedMessagesInAnotherChannel) return;
143+
139144
setLastMessage(
140145
channel.state.latestMessages[channel.state.latestMessages.length - 1],
141146
);
@@ -145,17 +150,19 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {
145150
channel.on('message.new', handleEvent);
146151
channel.on('message.updated', handleEvent);
147152
channel.on('message.deleted', handleEvent);
153+
client.on('user.messages.deleted', handleEvent);
148154
channel.on('message.undeleted', handleEvent);
149155
channel.on('channel.truncated', handleEvent);
150156

151157
return () => {
152158
channel.off('message.new', handleEvent);
153159
channel.off('message.updated', handleEvent);
154160
channel.off('message.deleted', handleEvent);
161+
client.off('user.messages.deleted', handleEvent);
155162
channel.off('message.undeleted', handleEvent);
156163
channel.off('channel.truncated', handleEvent);
157164
};
158-
}, [channel, refreshUnreadCount, channelUpdateCount]);
165+
}, [channel, client, refreshUnreadCount, channelUpdateCount]);
159166

160167
if (!Preview) return null;
161168

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

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
dispatchMessageUpdatedEvent,
1616
dispatchNotificationMarkRead,
1717
dispatchNotificationMarkUnread,
18+
dispatchUserMessagesDeletedEvent,
1819
dispatchUserUpdatedEvent,
1920
generateChannel,
2021
generateMember,
@@ -363,6 +364,131 @@ describe('ChannelPreview', () => {
363364
},
364365
);
365366

367+
describe('user.messages.deleted', () => {
368+
const deletedMessageText = 'Message deleted';
369+
const user = { id: 'banned-user' };
370+
371+
it('should update latest message preview for all ChannelPreviews if global ban', async () => {
372+
const {
373+
channels: [c0, c1],
374+
client,
375+
} = await initClientWithChannels({
376+
channelsData: [
377+
generateChannel({
378+
messages: [
379+
generateMessage({
380+
created_at: '1970-01-01T00:00:00.000Z',
381+
user: { id: 'other-user' },
382+
}),
383+
generateMessage({ created_at: '1970-01-02T00:00:00.000Z', user }),
384+
],
385+
}),
386+
generateChannel({
387+
messages: [
388+
generateMessage({
389+
created_at: '1971-01-01T00:00:00.000Z',
390+
user: { id: 'other-user' },
391+
}),
392+
generateMessage({ created_at: '1971-01-02T00:00:00.000Z', user }),
393+
],
394+
}),
395+
],
396+
customUser: user,
397+
});
398+
399+
const { container } = render(
400+
<ChatContext.Provider
401+
value={{
402+
channel: c0,
403+
client,
404+
setActiveChannel: () => jest.fn(),
405+
}}
406+
>
407+
<ChannelPreview channel={c0} />
408+
<ChannelPreview channel={c1} />
409+
</ChatContext.Provider>,
410+
);
411+
412+
await act(() => {
413+
dispatchUserMessagesDeletedEvent({
414+
client,
415+
user,
416+
});
417+
});
418+
419+
await waitFor(() => {
420+
const lastMessagePreviews = container.querySelectorAll(
421+
'.str-chat__channel-preview-messenger--last-message',
422+
);
423+
expect(lastMessagePreviews.length).toBe(2);
424+
lastMessagePreviews.forEach((preview) => {
425+
expect(preview).toHaveTextContent(deletedMessageText);
426+
});
427+
});
428+
});
429+
430+
it('should update latest message preview if the channel is the target', async () => {
431+
const {
432+
channels: [c0, c1],
433+
client,
434+
} = await initClientWithChannels({
435+
channelsData: [
436+
generateChannel({
437+
messages: [
438+
generateMessage({
439+
created_at: '1970-01-01T00:00:00.000Z',
440+
user: { id: 'other-user' },
441+
}),
442+
generateMessage({ created_at: '1970-01-02T00:00:00.000Z', user }),
443+
],
444+
}),
445+
generateChannel({
446+
messages: [
447+
generateMessage({
448+
created_at: '1971-01-01T00:00:00.000Z',
449+
user: { id: 'other-user' },
450+
}),
451+
generateMessage({ created_at: '1971-01-02T00:00:00.000Z', user }),
452+
],
453+
}),
454+
],
455+
customUser: user,
456+
});
457+
458+
const { container } = render(
459+
<ChatContext.Provider
460+
value={{
461+
channel: c0,
462+
client,
463+
setActiveChannel: () => jest.fn(),
464+
}}
465+
>
466+
<ChannelPreview channel={c0} />
467+
<ChannelPreview channel={c1} />
468+
</ChatContext.Provider>,
469+
);
470+
471+
await act(() => {
472+
dispatchUserMessagesDeletedEvent({
473+
channel: c0, // target
474+
client,
475+
user,
476+
});
477+
});
478+
479+
await waitFor(() => {
480+
const lastMessagePreviews = container.querySelectorAll(
481+
'.str-chat__channel-preview-messenger--last-message',
482+
);
483+
expect(lastMessagePreviews.length).toBe(2);
484+
expect(lastMessagePreviews[0]).toHaveTextContent(deletedMessageText);
485+
expect(lastMessagePreviews[1]).toHaveTextContent(
486+
c1.state.messages.slice(-1)[0].text,
487+
);
488+
});
489+
});
490+
});
491+
366492
describe('notification.mark_read', () => {
367493
it('should set unread count to 0 for event missing CID', async () => {
368494
const unreadCount = getRandomInt(1, 10);

src/mock-builders/event/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ export { default as dispatchNotificationMarkUnread } from './notificationMarkUnr
1515
export { default as dispatchNotificationMessageNewEvent } from './notificationMessageNew';
1616
export { default as dispatchNotificationMutesUpdated } from './notificationMutesUpdated';
1717
export { default as dispatchNotificationRemovedFromChannel } from './notificationRemovedFromChannel';
18+
export { default as dispatchUserMessagesDeletedEvent } from './userMessagesDeleted';
1819
export { default as dispatchUserUpdatedEvent } from './userUpdated';
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { ChannelResponse, StreamChat, UserResponse } from 'stream-chat';
2+
3+
export default ({
4+
channel,
5+
client,
6+
hardDelete,
7+
user,
8+
}: {
9+
channel: ChannelResponse & { name?: string }; // mock-builders are excluded in tsconfig.json
10+
client: StreamChat;
11+
hardDelete?: boolean;
12+
user: UserResponse;
13+
}) => {
14+
if (channel) {
15+
const [channel_id, channel_type] = channel.cid.split(':');
16+
client.dispatchEvent({
17+
channel_custom: {
18+
// archived: false,
19+
name: channel.name,
20+
},
21+
channel_id,
22+
channel_member_count: 2,
23+
channel_type,
24+
cid: channel.cid,
25+
created_at: new Date().toISOString(),
26+
hard_delete: !!hardDelete,
27+
type: 'user.messages.deleted',
28+
user,
29+
});
30+
} else {
31+
client.dispatchEvent({
32+
created_at: new Date().toISOString(),
33+
hard_delete: !!hardDelete,
34+
type: 'user.messages.deleted',
35+
user,
36+
});
37+
}
38+
};

src/mock-builders/generator/channel.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import { nanoid } from 'nanoid';
2-
import type { ChannelConfigWithInfo, ChannelResponse } from 'stream-chat';
2+
import type { ChannelAPIResponse, ChannelConfigWithInfo } from 'stream-chat';
3+
import type { DeepPartial } from '../../types/types';
34

4-
export const generateChannel = (
5-
options: {
6-
channel?: Partial<ChannelResponse>;
7-
config?: Partial<ChannelConfigWithInfo>;
8-
} = { channel: {}, config: {} },
9-
) => {
10-
const { channel: optionsChannel, ...optionsBesidesChannel } = options;
5+
export const generateChannel = (options?: DeepPartial<ChannelAPIResponse>) => {
6+
const { channel: optionsChannel, ...optionsBesidesChannel } =
7+
options ?? ({} as ChannelAPIResponse);
118
const id = optionsChannel?.id ?? nanoid();
129
const type = optionsChannel?.type ?? 'messaging';
1310
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -68,6 +65,6 @@ export const generateChannel = (
6865
type,
6966
updated_at: '2020-04-28T11:20:48.578147Z',
7067
...restOptionsChannel,
71-
} as ChannelResponse,
68+
},
7269
};
7370
};

src/types/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,7 @@ export type PartialSelected<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T,
7878
export type DeepRequired<T> = {
7979
[K in keyof T]-?: T[K] extends object ? DeepRequired<T[K]> : T[K];
8080
};
81+
82+
export type DeepPartial<T> = {
83+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
84+
};

yarn.lock

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8599,11 +8599,6 @@ lines-and-columns@^1.1.6:
85998599
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
86008600
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
86018601

8602-
linkifyjs@^4.2.0:
8603-
version "4.2.0"
8604-
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.2.0.tgz#9dd30222b9cbabec9c950e725ec00031c7fa3f08"
8605-
integrity sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==
8606-
86078602
linkifyjs@^4.3.2:
86088603
version "4.3.2"
86098604
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.2.tgz#d97eb45419aabf97ceb4b05a7adeb7b8c8ade2b1"
@@ -12047,10 +12042,10 @@ [email protected]:
1204712042
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
1204812043
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
1204912044

12050-
stream-chat@^9.15.0:
12051-
version "9.15.0"
12052-
resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.15.0.tgz#97df48dd1c11b6c251bace9a43519f98841660b8"
12053-
integrity sha512-Y2azwTbfyN8dMAovpxvjZTDTotpmRKucj/dYpVeiDgog9BZzxaaQVhKOYzDFdxA1I6PKicMAXLvgAf+py0CWEg==
12045+
stream-chat@^9.17.0:
12046+
version "9.17.0"
12047+
resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.17.0.tgz#540cf1ea03b08a394d6140696aae8528e9ba9ce2"
12048+
integrity sha512-ys6K73wIVWs5+qsfPJ9wumEUtgbMXYVbH1dhmAZ1oYtQ01dY/avsvt25PYDakVjKeyrnT+y8T/xEzfeF/WDJsg==
1205412049
dependencies:
1205512050
"@types/jsonwebtoken" "^9.0.8"
1205612051
"@types/ws" "^8.5.14"
@@ -12059,7 +12054,7 @@ stream-chat@^9.15.0:
1205912054
form-data "^4.0.4"
1206012055
isomorphic-ws "^5.0.0"
1206112056
jsonwebtoken "^9.0.2"
12062-
linkifyjs "^4.2.0"
12057+
linkifyjs "^4.3.2"
1206312058
ws "^8.18.1"
1206412059

1206512060
stream-combiner2@~1.1.1:

0 commit comments

Comments
 (0)