Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
})();
Expand Down
11 changes: 9 additions & 2 deletions src/components/ChannelPreview/ChannelPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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],
);
Expand All @@ -145,17 +150,19 @@ 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);

return () => {
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;

Expand Down
126 changes: 126 additions & 0 deletions src/components/ChannelPreview/__tests__/ChannelPreview.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
dispatchMessageUpdatedEvent,
dispatchNotificationMarkRead,
dispatchNotificationMarkUnread,
dispatchUserMessagesDeletedEvent,
dispatchUserUpdatedEvent,
generateChannel,
generateMember,
Expand Down Expand Up @@ -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(
<ChatContext.Provider
value={{
channel: c0,
client,
setActiveChannel: () => jest.fn(),
}}
>
<ChannelPreview channel={c0} />
<ChannelPreview channel={c1} />
</ChatContext.Provider>,
);

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(
<ChatContext.Provider
value={{
channel: c0,
client,
setActiveChannel: () => jest.fn(),
}}
>
<ChannelPreview channel={c0} />
<ChannelPreview channel={c1} />
</ChatContext.Provider>,
);

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);
Expand Down
1 change: 1 addition & 0 deletions src/mock-builders/event/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
38 changes: 38 additions & 0 deletions src/mock-builders/event/userMessagesDeleted.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
};
15 changes: 6 additions & 9 deletions src/mock-builders/generator/channel.ts
Original file line number Diff line number Diff line change
@@ -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<ChannelResponse>;
config?: Partial<ChannelConfigWithInfo>;
} = { channel: {}, config: {} },
) => {
const { channel: optionsChannel, ...optionsBesidesChannel } = options;
export const generateChannel = (options?: DeepPartial<ChannelAPIResponse>) => {
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
Expand Down Expand Up @@ -68,6 +65,6 @@ export const generateChannel = (
type,
updated_at: '2020-04-28T11:20:48.578147Z',
...restOptionsChannel,
} as ChannelResponse,
},
};
};
4 changes: 4 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,7 @@ export type PartialSelected<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T,
export type DeepRequired<T> = {
[K in keyof T]-?: T[K] extends object ? DeepRequired<T[K]> : T[K];
};

export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
15 changes: 5 additions & 10 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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:
Expand Down