Skip to content

Commit

Permalink
Merge branch 'master' into segment-v2
Browse files Browse the repository at this point in the history
  • Loading branch information
yaziine authored Jan 18, 2024
2 parents 7589fd3 + 4d73838 commit 2b5ad8d
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 23 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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/",
Expand Down
16 changes: 15 additions & 1 deletion src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1241,7 +1241,6 @@ export class Channel<StreamChatGenerics extends ExtendableGenerics = DefaultGene
case 'message.read':
if (event.user?.id && event.created_at) {
channelState.read[event.user.id] = {
// because in client.ts the handleEvent call that flows to this sets this `event.received_at = new Date();`
last_read: new Date(event.created_at),
last_read_message_id: event.last_read_message_id,
user: event.user,
Expand Down Expand Up @@ -1360,6 +1359,21 @@ export class Channel<StreamChatGenerics extends ExtendableGenerics = DefaultGene
delete channelState.members[event.user.id];
}
break;
case 'notification.mark_unread': {
const ownMessage = event.user?.id === this.getClient().user?.id;
if (!(ownMessage && event.user)) break;

channelState.read[event.user.id] = {
first_unread_message_id: event.first_unread_message_id,
last_read: new Date(event.last_read_at as string),
last_read_message_id: event.last_read_message_id,
user: event.user,
unread_messages: event.unread_messages ?? 0,
};

channelState.unreadCount = event.unread_messages ?? 0;
break;
}
case 'channel.updated':
if (event.channel) {
const isFrozenChanged = event.channel?.frozen !== undefined && event.channel.frozen !== channel.data?.frozen;
Expand Down
8 changes: 7 additions & 1 deletion src/channel_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ import {

type ChannelReadStatus<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = Record<
string,
{ last_read: Date; unread_messages: number; user: UserResponse<StreamChatGenerics>; last_read_message_id?: string }
{
last_read: Date;
unread_messages: number;
user: UserResponse<StreamChatGenerics>;
first_unread_message_id?: string;
last_read_message_id?: string;
}
>;

/**
Expand Down
14 changes: 7 additions & 7 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3079,16 +3079,16 @@ export class StreamChat<StreamChatGenerics extends ExtendableGenerics = DefaultG
*
* @return {TaskResponse} A task ID
*/
async deleteUsers(user_ids: string[], options: DeleteUserOptions) {
if (options?.user !== 'soft' && options?.user !== 'hard') {
throw new Error('Invalid delete user options. user must be one of [soft hard]');
async deleteUsers(user_ids: string[], options: DeleteUserOptions = {}) {
if (typeof options.user !== 'undefined' && !['soft', 'hard', 'pruning'].includes(options.user)) {
throw new Error('Invalid delete user options. user must be one of [soft hard pruning]');
}
if (options.messages !== undefined && options.messages !== 'soft' && options.messages !== 'hard') {
throw new Error('Invalid delete user options. messages must be one of [soft hard]');
}
if (options.conversations !== undefined && options.conversations !== 'soft' && options.conversations !== 'hard') {
if (typeof options.conversations !== 'undefined' && !['soft', 'hard'].includes(options.conversations)) {
throw new Error('Invalid delete user options. conversations must be one of [soft hard]');
}
if (typeof options.messages !== 'undefined' && !['soft', 'hard', 'pruning'].includes(options.messages)) {
throw new Error('Invalid delete user options. messages must be one of [soft hard pruning]');
}
return await this.post<APIResponse & TaskResponse>(this.baseURL + `/users/delete`, {
user_ids,
...options,
Expand Down
1 change: 1 addition & 0 deletions src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
33 changes: 25 additions & 8 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1055,8 +1055,13 @@ export type Event<StreamChatGenerics extends ExtendableGenerics = DefaultGeneric
cid?: string;
clear_history?: boolean;
connection_id?: string;
// event creation timestamp, format Date ISO string
created_at?: string;
// id of the message that was marked as unread - all the following messages are considered unread. (notification.mark_unread)
first_unread_message_id?: string;
hard_delete?: boolean;
// creation date of a message with last_read_message_id, formatted as Date ISO string
last_read_at?: string;
last_read_message_id?: string;
mark_messages_deleted?: boolean;
me?: OwnUserResponse<StreamChatGenerics>;
Expand All @@ -1072,9 +1077,14 @@ export type Event<StreamChatGenerics extends ExtendableGenerics = DefaultGeneric
reaction?: ReactionResponse<StreamChatGenerics>;
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<StreamChatGenerics>;
user_id?: string;
watcher_count?: number;
Expand Down Expand Up @@ -2365,22 +2375,29 @@ export type DeleteChannelsResponse = {
result: Record<string, string>;
} & Partial<TaskResponse>;

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<DeleteType, 'pruning'>;
messages?: DeleteType;
new_channel_owner_id?: string;
user?: DeleteType;
};

export type SegmentType = 'channel' | 'user';
Expand Down
73 changes: 68 additions & 5 deletions test/unit/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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' },
Expand Down
43 changes: 43 additions & 0 deletions test/unit/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 2b5ad8d

Please sign in to comment.