Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle notification.mark_unread #1208

Merged
merged 3 commits into from
Jan 12, 2024
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
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
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
10 changes: 10 additions & 0 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
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
Loading