diff --git a/package.json b/package.json
index e62f69dc23..e5dfe11dfd 100644
--- a/package.json
+++ b/package.json
@@ -142,7 +142,7 @@
"emoji-mart": "^5.4.0",
"react": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.14.0",
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.14.0",
- "stream-chat": "^9.15.0"
+ "stream-chat": "^9.17.0"
},
"peerDependenciesMeta": {
"@breezystack/lamejs": {
@@ -236,7 +236,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"semantic-release": "^24.2.3",
- "stream-chat": "^9.15.0",
+ "stream-chat": "^9.17.0",
"ts-jest": "^29.2.5",
"typescript": "^5.4.5",
"typescript-eslint": "^8.17.0"
diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx
index 8746dda216..8a0dc6e34c 100644
--- a/src/components/Channel/Channel.tsx
+++ b/src/components/Channel/Channel.tsx
@@ -425,6 +425,11 @@ const ChannelInner = (
});
}
+ // ignore the event if it is not targeted at the current channel.
+ // Event targeted at this channel or globally targeted event should lead to state refresh
+ if (event.type === 'user.messages.deleted' && event.cid && event.cid !== channel.cid)
+ return;
+
if (event.type === 'user.watching.start' || event.type === 'user.watching.stop')
return;
@@ -568,6 +573,7 @@ const ChannelInner = (
client.on('connection.recovered', handleEvent);
client.on('user.updated', handleEvent);
client.on('user.deleted', handleEvent);
+ client.on('user.messages.deleted', handleEvent);
channel.on(handleEvent);
}
})();
diff --git a/src/components/ChannelPreview/ChannelPreview.tsx b/src/components/ChannelPreview/ChannelPreview.tsx
index 937e76b325..154d061646 100644
--- a/src/components/ChannelPreview/ChannelPreview.tsx
+++ b/src/components/ChannelPreview/ChannelPreview.tsx
@@ -135,7 +135,12 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {
useEffect(() => {
refreshUnreadCount();
- const handleEvent = () => {
+ const handleEvent = (event: Event) => {
+ const deletedMessagesInAnotherChannel =
+ event.type === 'user.messages.deleted' && event.cid && event.cid !== channel.cid;
+
+ if (deletedMessagesInAnotherChannel) return;
+
setLastMessage(
channel.state.latestMessages[channel.state.latestMessages.length - 1],
);
@@ -145,6 +150,7 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {
channel.on('message.new', handleEvent);
channel.on('message.updated', handleEvent);
channel.on('message.deleted', handleEvent);
+ client.on('user.messages.deleted', handleEvent);
channel.on('message.undeleted', handleEvent);
channel.on('channel.truncated', handleEvent);
@@ -152,10 +158,11 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {
channel.off('message.new', handleEvent);
channel.off('message.updated', handleEvent);
channel.off('message.deleted', handleEvent);
+ client.off('user.messages.deleted', handleEvent);
channel.off('message.undeleted', handleEvent);
channel.off('channel.truncated', handleEvent);
};
- }, [channel, refreshUnreadCount, channelUpdateCount]);
+ }, [channel, client, refreshUnreadCount, channelUpdateCount]);
if (!Preview) return null;
diff --git a/src/components/ChannelPreview/__tests__/ChannelPreview.test.js b/src/components/ChannelPreview/__tests__/ChannelPreview.test.js
index db9540daf4..eff538705c 100644
--- a/src/components/ChannelPreview/__tests__/ChannelPreview.test.js
+++ b/src/components/ChannelPreview/__tests__/ChannelPreview.test.js
@@ -15,6 +15,7 @@ import {
dispatchMessageUpdatedEvent,
dispatchNotificationMarkRead,
dispatchNotificationMarkUnread,
+ dispatchUserMessagesDeletedEvent,
dispatchUserUpdatedEvent,
generateChannel,
generateMember,
@@ -363,6 +364,131 @@ describe('ChannelPreview', () => {
},
);
+ describe('user.messages.deleted', () => {
+ const deletedMessageText = 'Message deleted';
+ const user = { id: 'banned-user' };
+
+ it('should update latest message preview for all ChannelPreviews if global ban', async () => {
+ const {
+ channels: [c0, c1],
+ client,
+ } = await initClientWithChannels({
+ channelsData: [
+ generateChannel({
+ messages: [
+ generateMessage({
+ created_at: '1970-01-01T00:00:00.000Z',
+ user: { id: 'other-user' },
+ }),
+ generateMessage({ created_at: '1970-01-02T00:00:00.000Z', user }),
+ ],
+ }),
+ generateChannel({
+ messages: [
+ generateMessage({
+ created_at: '1971-01-01T00:00:00.000Z',
+ user: { id: 'other-user' },
+ }),
+ generateMessage({ created_at: '1971-01-02T00:00:00.000Z', user }),
+ ],
+ }),
+ ],
+ customUser: user,
+ });
+
+ const { container } = render(
+ jest.fn(),
+ }}
+ >
+
+
+ ,
+ );
+
+ await act(() => {
+ dispatchUserMessagesDeletedEvent({
+ client,
+ user,
+ });
+ });
+
+ await waitFor(() => {
+ const lastMessagePreviews = container.querySelectorAll(
+ '.str-chat__channel-preview-messenger--last-message',
+ );
+ expect(lastMessagePreviews.length).toBe(2);
+ lastMessagePreviews.forEach((preview) => {
+ expect(preview).toHaveTextContent(deletedMessageText);
+ });
+ });
+ });
+
+ it('should update latest message preview if the channel is the target', async () => {
+ const {
+ channels: [c0, c1],
+ client,
+ } = await initClientWithChannels({
+ channelsData: [
+ generateChannel({
+ messages: [
+ generateMessage({
+ created_at: '1970-01-01T00:00:00.000Z',
+ user: { id: 'other-user' },
+ }),
+ generateMessage({ created_at: '1970-01-02T00:00:00.000Z', user }),
+ ],
+ }),
+ generateChannel({
+ messages: [
+ generateMessage({
+ created_at: '1971-01-01T00:00:00.000Z',
+ user: { id: 'other-user' },
+ }),
+ generateMessage({ created_at: '1971-01-02T00:00:00.000Z', user }),
+ ],
+ }),
+ ],
+ customUser: user,
+ });
+
+ const { container } = render(
+ jest.fn(),
+ }}
+ >
+
+
+ ,
+ );
+
+ await act(() => {
+ dispatchUserMessagesDeletedEvent({
+ channel: c0, // target
+ client,
+ user,
+ });
+ });
+
+ await waitFor(() => {
+ const lastMessagePreviews = container.querySelectorAll(
+ '.str-chat__channel-preview-messenger--last-message',
+ );
+ expect(lastMessagePreviews.length).toBe(2);
+ expect(lastMessagePreviews[0]).toHaveTextContent(deletedMessageText);
+ expect(lastMessagePreviews[1]).toHaveTextContent(
+ c1.state.messages.slice(-1)[0].text,
+ );
+ });
+ });
+ });
+
describe('notification.mark_read', () => {
it('should set unread count to 0 for event missing CID', async () => {
const unreadCount = getRandomInt(1, 10);
diff --git a/src/mock-builders/event/index.js b/src/mock-builders/event/index.js
index f24d5b165c..f6a118519a 100644
--- a/src/mock-builders/event/index.js
+++ b/src/mock-builders/event/index.js
@@ -15,4 +15,5 @@ export { default as dispatchNotificationMarkUnread } from './notificationMarkUnr
export { default as dispatchNotificationMessageNewEvent } from './notificationMessageNew';
export { default as dispatchNotificationMutesUpdated } from './notificationMutesUpdated';
export { default as dispatchNotificationRemovedFromChannel } from './notificationRemovedFromChannel';
+export { default as dispatchUserMessagesDeletedEvent } from './userMessagesDeleted';
export { default as dispatchUserUpdatedEvent } from './userUpdated';
diff --git a/src/mock-builders/event/userMessagesDeleted.ts b/src/mock-builders/event/userMessagesDeleted.ts
new file mode 100644
index 0000000000..4c6e276483
--- /dev/null
+++ b/src/mock-builders/event/userMessagesDeleted.ts
@@ -0,0 +1,38 @@
+import type { ChannelResponse, StreamChat, UserResponse } from 'stream-chat';
+
+export default ({
+ channel,
+ client,
+ hardDelete,
+ user,
+}: {
+ channel: ChannelResponse & { name?: string }; // mock-builders are excluded in tsconfig.json
+ client: StreamChat;
+ hardDelete?: boolean;
+ user: UserResponse;
+}) => {
+ if (channel) {
+ const [channel_id, channel_type] = channel.cid.split(':');
+ client.dispatchEvent({
+ channel_custom: {
+ // archived: false,
+ name: channel.name,
+ },
+ channel_id,
+ channel_member_count: 2,
+ channel_type,
+ cid: channel.cid,
+ created_at: new Date().toISOString(),
+ hard_delete: !!hardDelete,
+ type: 'user.messages.deleted',
+ user,
+ });
+ } else {
+ client.dispatchEvent({
+ created_at: new Date().toISOString(),
+ hard_delete: !!hardDelete,
+ type: 'user.messages.deleted',
+ user,
+ });
+ }
+};
diff --git a/src/mock-builders/generator/channel.ts b/src/mock-builders/generator/channel.ts
index 49cd09d8d6..efac3dedbd 100644
--- a/src/mock-builders/generator/channel.ts
+++ b/src/mock-builders/generator/channel.ts
@@ -1,13 +1,10 @@
import { nanoid } from 'nanoid';
-import type { ChannelConfigWithInfo, ChannelResponse } from 'stream-chat';
+import type { ChannelAPIResponse, ChannelConfigWithInfo } from 'stream-chat';
+import type { DeepPartial } from '../../types/types';
-export const generateChannel = (
- options: {
- channel?: Partial;
- config?: Partial;
- } = { channel: {}, config: {} },
-) => {
- const { channel: optionsChannel, ...optionsBesidesChannel } = options;
+export const generateChannel = (options?: DeepPartial) => {
+ const { channel: optionsChannel, ...optionsBesidesChannel } =
+ options ?? ({} as ChannelAPIResponse);
const id = optionsChannel?.id ?? nanoid();
const type = optionsChannel?.type ?? 'messaging';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -68,6 +65,6 @@ export const generateChannel = (
type,
updated_at: '2020-04-28T11:20:48.578147Z',
...restOptionsChannel,
- } as ChannelResponse,
+ },
};
};
diff --git a/src/types/types.ts b/src/types/types.ts
index b20af7cf90..ad3d2cc891 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -78,3 +78,7 @@ export type PartialSelected = Omit & Partial = {
[K in keyof T]-?: T[K] extends object ? DeepRequired : T[K];
};
+
+export type DeepPartial = {
+ [P in keyof T]?: T[P] extends object ? DeepPartial : T[P];
+};
diff --git a/yarn.lock b/yarn.lock
index db581f4c7e..a786684c4d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8599,11 +8599,6 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
-linkifyjs@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.2.0.tgz#9dd30222b9cbabec9c950e725ec00031c7fa3f08"
- integrity sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==
-
linkifyjs@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.2.tgz#d97eb45419aabf97ceb4b05a7adeb7b8c8ade2b1"
@@ -12047,10 +12042,10 @@ statuses@2.0.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
-stream-chat@^9.15.0:
- version "9.15.0"
- resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.15.0.tgz#97df48dd1c11b6c251bace9a43519f98841660b8"
- integrity sha512-Y2azwTbfyN8dMAovpxvjZTDTotpmRKucj/dYpVeiDgog9BZzxaaQVhKOYzDFdxA1I6PKicMAXLvgAf+py0CWEg==
+stream-chat@^9.17.0:
+ version "9.17.0"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.17.0.tgz#540cf1ea03b08a394d6140696aae8528e9ba9ce2"
+ integrity sha512-ys6K73wIVWs5+qsfPJ9wumEUtgbMXYVbH1dhmAZ1oYtQ01dY/avsvt25PYDakVjKeyrnT+y8T/xEzfeF/WDJsg==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
@@ -12059,7 +12054,7 @@ stream-chat@^9.15.0:
form-data "^4.0.4"
isomorphic-ws "^5.0.0"
jsonwebtoken "^9.0.2"
- linkifyjs "^4.2.0"
+ linkifyjs "^4.3.2"
ws "^8.18.1"
stream-combiner2@~1.1.1: