Skip to content

Commit

Permalink
Fix/unread count mutes (#498)
Browse files Browse the repository at this point in the history
* add userMuteStatus

* refactor generateMsg to test utils

* skip muted users in unreadCounts

* message.new updates unreadCount correctly

* reversing the unread count condition
  • Loading branch information
Amin Mahboubi authored Nov 17, 2020
1 parent 3527d20 commit 5f44dd1
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 68 deletions.
76 changes: 33 additions & 43 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,20 @@ export class Channel<
}
}

_countMessageAsUnread(message: {
shadowed?: boolean;
silent?: boolean;
user?: { id?: string } | null;
}) {
if (message.shadowed) return false;
if (message.silent) return false;
if (message.user?.id === this.getClient().userID) return false;
if (message.user?.id && this.getClient().userMuteStatus(message.user.id))
return false;

return true;
}

/**
* countUnread - Count of unread messages
*
Expand All @@ -1135,27 +1149,12 @@ export class Channel<
* @return {number} Unread count
*/
countUnread(lastRead?: Date | Immutable.ImmutableDate | null) {
if (!lastRead) {
return this.state.unreadCount;
}
if (!lastRead) return this.state.unreadCount;

let count = 0;
for (const m of this.state.messages.asMutable()) {
const message = m.asMutable({ deep: true });
if (this.getClient().userID === message.user?.id) {
continue;
}
if (m.shadowed) {
continue;
}
if (m.silent) {
continue;
}
if (lastRead == null) {
count++;
continue;
}
if (m.created_at > lastRead) {
for (let i = 0; i < this.state.messages.length; i += 1) {
const message = this.state.messages[i];
if (message.created_at > lastRead && this._countMessageAsUnread(message)) {
count++;
}
}
Expand All @@ -1169,27 +1168,17 @@ export class Channel<
*/
countUnreadMentions() {
const lastRead = this.lastRead();
const userID = this.getClient().userID;

let count = 0;
for (const m of this.state.messages.asMutable()) {
const message = m.asMutable({ deep: true });
if (this.getClient().userID === message.user?.id) {
continue;
}
if (m.shadowed) {
continue;
}
if (m.silent) {
continue;
}
if (lastRead == null) {
for (let i = 0; i < this.state.messages.length; i += 1) {
const message = this.state.messages[i];
if (
this._countMessageAsUnread(message) &&
(!lastRead || message.created_at > lastRead) &&
message.mentioned_users?.find((u) => u.id === userID)
) {
count++;
continue;
}
if (m.created_at > lastRead) {
const userID = this.getClient().userID;
if (m.mentioned_users?.findIndex((u) => u.id === userID) !== -1) {
count++;
}
}
}
return count;
Expand Down Expand Up @@ -1555,13 +1544,14 @@ export class Channel<
}
break;
case 'message.new':
if (event.user?.id === this.getClient().user?.id) {
s.unreadCount = 0;
} else {
if (!event.message?.shadowed) s.unreadCount = s.unreadCount + 1;
}
if (event.message) {
s.addMessageSorted(event.message);

if (event.user?.id === this.getClient().user?.id) {
s.unreadCount = 0;
} else if (this._countMessageAsUnread(event.message)) {
s.unreadCount = s.unreadCount + 1;
}
}
break;
case 'message.updated':
Expand Down
23 changes: 23 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
Message,
MessageFilters,
MessageResponse,
Mute,
MuteUserOptions,
MuteUserResponse,
PartialUserUpdate,
Expand Down Expand Up @@ -135,6 +136,7 @@ export class StreamChat<
};
logger: Logger;
mutedChannels: ChannelMute<ChannelType, CommandType, UserType>[];
mutedUsers: Mute<UserType>[];
node: boolean;
options: StreamChatOptions;
secret?: string;
Expand Down Expand Up @@ -179,6 +181,7 @@ export class StreamChat<
this.state = new ClientState<UserType>();
// a list of channels to hide ws events from
this.mutedChannels = [];
this.mutedUsers = [];

// set the secret
if (secretOrOptions && isString(secretOrOptions)) {
Expand Down Expand Up @@ -926,6 +929,7 @@ export class StreamChat<
client.user = event.me;
client.state.updateUser(event.me);
client.mutedChannels = event.me.channel_mutes;
client.mutedUsers = event.me.mutes;
}

if (event.channel && event.type === 'notification.message_new') {
Expand All @@ -935,6 +939,10 @@ export class StreamChat<
if (event.type === 'notification.channel_mutes_updated' && event.me?.channel_mutes) {
this.mutedChannels = event.me.channel_mutes;
}

if (event.type === 'notification.mutes_updated' && event.me?.mutes) {
this.mutedUsers = event.me.mutes;
}
}

_muteStatus(cid: string) {
Expand Down Expand Up @@ -1701,6 +1709,21 @@ export class StreamChat<
});
}

/** userMuteStatus - check if a user is muted or not, can be used after setUser() is called
*
* @param {string} targetID
* @returns {boolean}
*/
userMuteStatus(targetID: string) {
if (!this.user || !this.wsPromise)
throw new Error('Make sure to await setUser() first.');

for (let i = 0; i < this.mutedUsers.length; i += 1) {
if (this.mutedUsers[i].target.id === targetID) return true;
}
return false;
}

/**
* flagMessage - flag a message
* @param {string} targetMessageID
Expand Down
161 changes: 161 additions & 0 deletions test/unit/channel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import chai from 'chai';
import { Channel } from '../../src/channel';
import { generateMsg } from './utils';

const expect = chai.expect;

describe('Channel count unread', function () {
const user = { id: 'user' };
const lastRead = new Date('2020-01-01T00:00:00');

const channel = new Channel({}, 'messaging', 'id');
channel.lastRead = () => lastRead;
channel.getClient = () => ({
user,
userID: 'user',
userMuteStatus: (targetId) => targetId.startsWith('mute'),
});

const ignoredMessages = [
generateMsg({ date: '2018-01-01T00:00:00', mentioned_users: [user] }),
generateMsg({ date: '2019-01-01T00:00:00' }),
generateMsg({ date: '2020-01-01T00:00:00' }),
generateMsg({
date: '2023-01-01T00:00:00',
shadowed: true,
mentioned_users: [user],
}),
generateMsg({
date: '2024-01-01T00:00:00',
silent: true,
mentioned_users: [user],
}),
generateMsg({
date: '2025-01-01T00:00:00',
user: { id: 'mute1' },
mentioned_users: [user],
}),
];
channel.state.addMessagesSorted(ignoredMessages);

it('_countMessageAsUnread should return false shadowed or silent messages', function () {
expect(channel._countMessageAsUnread({ shadowed: true })).not.to.be.ok;
expect(channel._countMessageAsUnread({ silent: true })).not.to.be.ok;
});

it('_countMessageAsUnread should return false for current user messages', function () {
expect(channel._countMessageAsUnread({ user })).not.to.be.ok;
});

it('_countMessageAsUnread should return false for muted user', function () {
expect(channel._countMessageAsUnread({ user: { id: 'mute1' } })).not.to.be.ok;
});

it('_countMessageAsUnread should return true for unmuted user', function () {
expect(channel._countMessageAsUnread({ user: { id: 'unmute' } })).to.be.ok;
});

it('_countMessageAsUnread should return true for other messages', function () {
expect(
channel._countMessageAsUnread({
shadowed: false,
silent: false,
user: { id: 'random' },
}),
).to.be.ok;
});

it('countUnread should return state.unreadCount without lastRead', function () {
expect(channel.countUnread()).to.be.equal(channel.state.unreadCount);
channel.state.unreadCount = 10;
expect(channel.countUnread()).to.be.equal(10);
channel.state.unreadCount = 0;
});

it('countUnread should return correct count', function () {
expect(channel.countUnread(lastRead)).to.be.equal(0);
channel.state.addMessagesSorted([
generateMsg({ date: '2021-01-01T00:00:00' }),
generateMsg({ date: '2022-01-01T00:00:00' }),
]);
expect(channel.countUnread(lastRead)).to.be.equal(2);
});

it('countUnreadMentions should return correct count', function () {
expect(channel.countUnreadMentions()).to.be.equal(0);
channel.state.addMessageSorted(
generateMsg({
date: '2021-01-01T00:00:00',
mentioned_users: [user, { id: 'random' }],
}),
generateMsg({
date: '2022-01-01T00:00:00',
mentioned_users: [{ id: 'random' }],
}),
);
expect(channel.countUnreadMentions()).to.be.equal(1);
});
});

describe('Channel _handleChannelEvent', function () {
const user = { id: 'user' };
const channel = new Channel(
{
logger: () => null,
user,
userID: user.id,
userMuteStatus: (targetId) => targetId.startsWith('mute'),
},
'messaging',
'id',
);

it('message.new reset the unreadCount for current user messages', function () {
channel.state.unreadCount = 100;
channel._handleChannelEvent({
type: 'message.new',
user,
message: generateMsg(),
});

expect(channel.state.unreadCount).to.be.equal(0);
});

it('message.new increment unreadCount properly', function () {
channel.state.unreadCount = 20;
channel._handleChannelEvent({
type: 'message.new',
user: { id: 'id' },
message: generateMsg({ user: { id: 'id' } }),
});
expect(channel.state.unreadCount).to.be.equal(21);
channel._handleChannelEvent({
type: 'message.new',
user: { id: 'id2' },
message: generateMsg({ user: { id: 'id2' } }),
});
expect(channel.state.unreadCount).to.be.equal(22);
});

it('message.new skip increment for silent/shadowed/muted messages', function () {
channel.state.unreadCount = 30;
channel._handleChannelEvent({
type: 'message.new',
user: { id: 'id' },
message: generateMsg({ silent: true }),
});
expect(channel.state.unreadCount).to.be.equal(30);
channel._handleChannelEvent({
type: 'message.new',
user: { id: 'id2' },
message: generateMsg({ shadowed: true }),
});
expect(channel.state.unreadCount).to.be.equal(30);
channel._handleChannelEvent({
type: 'message.new',
user: { id: 'mute1' },
message: generateMsg({ user: { id: 'mute1' } }),
});
expect(channel.state.unreadCount).to.be.equal(30);
});
});
26 changes: 1 addition & 25 deletions test/unit/channel_state.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,10 @@
import Immutable from 'seamless-immutable';
import chai from 'chai';
import { v4 as uuidv4 } from 'uuid';
import { ChannelState } from '../../src/channel_state';
import { generateMsg } from './utils';

const expect = chai.expect;

const generateMsg = (msg = {}) => {
const date = msg.date || new Date().toISOString();
return {
id: uuidv4(),
text: 'x',
html: '<p>x</p>\n',
type: 'regular',
user: { id: 'id' },
attachments: [],
latest_reactions: [],
own_reactions: [],
reaction_counts: null,
reaction_scores: {},
reply_count: 0,
created_at: date,
updated_at: date,
mentioned_users: [],
silent: false,
status: 'received',
__html: '<p>x</p>\n',
...msg,
};
};

describe('ChannelState addMessagesSorted', function () {
it('empty state add single messages', async function () {
const state = new ChannelState();
Expand Down
Loading

0 comments on commit 5f44dd1

Please sign in to comment.