diff --git a/CHANGELOG.md b/CHANGELOG.md index e1b660202..0b7ba5c10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [8.14.5](https://github.com/GetStream/stream-chat-js/compare/v8.14.4...v8.14.5) (2024-01-09) + + +### Bug Fixes + +* deleteUsers - add pruning to options enums ([#1206](https://github.com/GetStream/stream-chat-js/issues/1206)) ([c9af1bb](https://github.com/GetStream/stream-chat-js/commit/c9af1bb7af424f542398d5d660c0c6f220c5f0c0)) + ### [8.14.4](https://github.com/GetStream/stream-chat-js/compare/v8.14.3...v8.14.4) (2023-11-29) diff --git a/package.json b/package.json index cda0d1da4..191028b53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stream-chat", - "version": "8.14.4", + "version": "8.14.5", "description": "JS SDK for the Stream Chat API", "author": "GetStream", "homepage": "https://getstream.io/chat/", diff --git a/src/channel.ts b/src/channel.ts index 16cab802a..2812c2fff 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -1241,7 +1241,6 @@ export class Channel = Record< string, - { last_read: Date; unread_messages: number; user: UserResponse; last_read_message_id?: string } + { + last_read: Date; + unread_messages: number; + user: UserResponse; + first_unread_message_id?: string; + last_read_message_id?: string; + } >; /** diff --git a/src/client.ts b/src/client.ts index a068204c1..f549dd138 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3079,16 +3079,16 @@ export class StreamChat(this.baseURL + `/users/delete`, { user_ids, ...options, diff --git a/src/events.ts b/src/events.ts index 74e984210..25f378b2c 100644 --- a/src/events.ts +++ b/src/events.ts @@ -24,6 +24,7 @@ export const EVENT_MAP = { 'notification.invite_rejected': true, 'notification.invited': true, 'notification.mark_read': true, + 'notification.mark_unread': true, 'notification.message_new': true, 'notification.mutes_updated': true, 'notification.removed_from_channel': true, diff --git a/src/types.ts b/src/types.ts index d1a9c16f6..caa70f0a3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1055,8 +1055,13 @@ export type Event; @@ -1072,9 +1077,14 @@ export type Event; received_at?: string | Date; team?: string; + // @deprecated number of all unread messages across all current user's unread channels, equals unread_count total_unread_count?: number; + // number of all current user's channels with at least one unread message including the channel in this event unread_channels?: number; + // number of all unread messages across all current user's unread channels unread_count?: number; + // number of unread messages in the channel from this event (notification.mark_unread) + unread_messages?: number; user?: UserResponse; user_id?: string; watcher_count?: number; @@ -2365,22 +2375,29 @@ export type DeleteChannelsResponse = { result: Record; } & Partial; -export type DeleteType = 'soft' | 'hard'; +export type DeleteType = 'soft' | 'hard' | 'pruning'; /* DeleteUserOptions specifies a collection of one or more `user_ids` to be deleted. - `user` soft|hard determines if the user needs to be hard- or soft-deleted, where hard-delete - implies that all related objects (messages, flags, etc) will be hard-deleted as well. - `conversations` soft|hard will delete any 1to1 channels that the user was a member of. - `messages` soft-hard will delete any messages that the user has sent. - `new_channel_owner_id` any channels owned by the hard-deleted user will be transferred to this user ID + `user`: + - soft: marks user as deleted and retains all user data + - pruning: marks user as deleted and nullifies user information + - hard: deletes user completely - this requires hard option for messages and conversation as well + `conversations`: + - soft: marks all conversation channels as deleted (same effect as Delete Channels with 'hard' option disabled) + - hard: deletes channel and all its data completely including messages (same effect as Delete Channels with 'hard' option enabled) + `messages`: + - soft: marks all user messages as deleted without removing any related message data + - pruning: marks all user messages as deleted, nullifies message information and removes some message data such as reactions and flags + - hard: deletes messages completely with all related information + `new_channel_owner_id`: any channels owned by the hard-deleted user will be transferred to this user ID */ export type DeleteUserOptions = { - user: DeleteType; - conversations?: DeleteType; + conversations?: Exclude; messages?: DeleteType; new_channel_owner_id?: string; + user?: DeleteType; }; export type SegmentType = 'channel' | 'user'; diff --git a/test/unit/channel.js b/test/unit/channel.js index 0c7f49ab7..a95ad7d09 100644 --- a/test/unit/channel.js +++ b/test/unit/channel.js @@ -10,9 +10,7 @@ import { getOrCreateChannelApi } from './test-utils/getOrCreateChannelApi'; import sinon from 'sinon'; import { mockChannelQueryResponse } from './test-utils/mockChannelQueryResponse'; -import { StreamChat } from '../../src/client'; -import { ChannelState } from '../../src'; -import exp from 'constants'; +import { ChannelState, StreamChat } from '../../src'; const expect = chai.expect; @@ -353,6 +351,72 @@ describe('Channel _handleChannelEvent', function () { expect(channel.state.messages.find((msg) => msg.id === quotingMessage.id).quoted_message.deleted_at).to.be.ok; }); + describe('notification.mark_unread', () => { + let initialCountUnread; + let initialReadState; + let notificationMarkUnreadEvent; + beforeEach(() => { + initialCountUnread = 0; + initialReadState = { + last_read: new Date().toISOString(), + last_read_message_id: '6', + user, + unread_messages: initialCountUnread, + }; + notificationMarkUnreadEvent = { + type: 'notification.mark_unread', + created_at: new Date().toISOString(), + cid: channel.cid, + channel_id: channel.id, + channel_type: channel.type, + channel: null, + user, + first_unread_message_id: '2', + last_read_at: new Date(new Date(initialReadState.last_read).getTime() - 1000).toISOString(), + last_read_message_id: '1', + unread_messages: 5, + unread_count: 6, + total_unread_count: 6, + unread_channels: 2, + }; + }); + + it('should update channel read state produced for current user', () => { + channel.state.unreadCount = initialCountUnread; + channel.state.read[user.id] = initialReadState; + const event = notificationMarkUnreadEvent; + + channel._handleChannelEvent(event); + + expect(channel.state.unreadCount).to.be.equal(event.unread_messages); + expect(new Date(channel.state.read[user.id].last_read).getTime()).to.be.equal( + new Date(event.last_read_at).getTime(), + ); + expect(channel.state.read[user.id].last_read_message_id).to.be.equal(event.last_read_message_id); + expect(channel.state.read[user.id].unread_messages).to.be.equal(event.unread_messages); + }); + + it('should not update channel read state produced for another user or user is missing', () => { + channel.state.unreadCount = initialCountUnread; + channel.state.read[user.id] = initialReadState; + const { user: excludedUser, ...eventMissingUser } = notificationMarkUnreadEvent; + const eventWithAnotherUser = { ...notificationMarkUnreadEvent, user: { id: 'another-user' } }; + + [eventWithAnotherUser, eventMissingUser].forEach((event) => { + channel._handleChannelEvent(event); + + expect(channel.state.unreadCount).to.be.equal(initialCountUnread); + expect(new Date(channel.state.read[user.id].last_read).getTime()).to.be.equal( + new Date(initialReadState.last_read).getTime(), + ); + expect(channel.state.read[user.id].last_read_message_id).to.be.equal( + initialReadState.last_read_message_id, + ); + expect(channel.state.read[user.id].unread_messages).to.be.equal(initialReadState.unread_messages); + }); + }); + }); + it('should include unread_messages for message events from another user', () => { channel.state.read['id'] = { unread_messages: 2, @@ -501,8 +565,7 @@ describe('Channel _handleChannelEvent', function () { it(`should make sure that state reload doesn't wipe out existing data`, async () => { const mock = sinon.mock(client); - const response = mockChannelQueryResponse; - mock.expects('post').returns(Promise.resolve(response)); + mock.expects('post').returns(Promise.resolve(mockChannelQueryResponse)); channel.state.members = { user: { id: 'user' }, diff --git a/test/unit/client.js b/test/unit/client.js index 687c4fae5..11694f191 100644 --- a/test/unit/client.js +++ b/test/unit/client.js @@ -328,6 +328,49 @@ describe('Detect node environment', () => { }); }); +describe('Client deleteUsers', () => { + it('should allow completely optional options', async () => { + const client = await getClientWithUser(); + + client.post = () => Promise.resolve(); + + await expect(client.deleteUsers(['_'])).to.eventually.equal(); + }); + + it('delete types - options.conversations', async () => { + const client = await getClientWithUser(); + + client.post = () => Promise.resolve(); + + await expect(client.deleteUsers(['_'], { conversations: 'hard' })).to.eventually.equal(); + await expect(client.deleteUsers(['_'], { conversations: 'soft' })).to.eventually.equal(); + await expect(client.deleteUsers(['_'], { conversations: 'pruning' })).to.be.rejectedWith(); + await expect(client.deleteUsers(['_'], { conversations: '' })).to.be.rejectedWith(); + }); + + it('delete types - options.messages', async () => { + const client = await getClientWithUser(); + + client.post = () => Promise.resolve(); + + await expect(client.deleteUsers(['_'], { messages: 'hard' })).to.eventually.equal(); + await expect(client.deleteUsers(['_'], { messages: 'soft' })).to.eventually.equal(); + await expect(client.deleteUsers(['_'], { messages: 'pruning' })).to.eventually.equal(); + await expect(client.deleteUsers(['_'], { messages: '' })).to.be.rejectedWith(); + }); + + it('delete types - options.user', async () => { + const client = await getClientWithUser(); + + client.post = () => Promise.resolve(); + + await expect(client.deleteUsers(['_'], { user: 'hard' })).to.eventually.equal(); + await expect(client.deleteUsers(['_'], { user: 'soft' })).to.eventually.equal(); + await expect(client.deleteUsers(['_'], { user: 'pruning' })).to.eventually.equal(); + await expect(client.deleteUsers(['_'], { user: '' })).to.be.rejectedWith(); + }); +}); + describe('updateMessage should ensure sanity of `mentioned_users`', () => { it('should convert mentioned_users from array of user objects to array of userIds', async () => { const client = await getClientWithUser();