Skip to content

Commit

Permalink
fix: Deletion of 1st message in a thread causes notification to be up…
Browse files Browse the repository at this point in the history
… without showing why. (RocketChat#34165)
  • Loading branch information
Gustrb authored Dec 20, 2024
1 parent ff04c19 commit c84543f
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/ninety-bulldogs-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes an issue where removing the only message of a thread would keep the unread thread messages badge
32 changes: 27 additions & 5 deletions apps/meteor/app/lib/server/functions/deleteMessage.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { AppEvents, Apps } from '@rocket.chat/apps';
import { api, Message } from '@rocket.chat/core-services';
import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings';
import { Messages, Rooms, Uploads, Users, ReadReceipts } from '@rocket.chat/models';
import { isThreadMessage, type AtLeast, type IMessage, type IRoom, type IThreadMessage, type IUser } from '@rocket.chat/core-typings';
import { Messages, Rooms, Uploads, Users, ReadReceipts, Subscriptions } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { callbacks } from '../../../../lib/callbacks';
import { canDeleteMessageAsync } from '../../../authorization/server/functions/canDeleteMessage';
import { FileUpload } from '../../../file-upload/server';
import { settings } from '../../../settings/server';
import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener';
import { notifyOnRoomChangedById, notifyOnMessageChange, notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener';

export const deleteMessageValidatingPermission = async (message: AtLeast<IMessage, '_id'>, userId: IUser['_id']): Promise<void> => {
if (!message?._id) {
Expand Down Expand Up @@ -50,8 +50,8 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise<voi
}
}

if (deletedMsg?.tmid) {
await Messages.decreaseReplyCountById(deletedMsg.tmid, -1);
if (deletedMsg && isThreadMessage(deletedMsg)) {
await deleteThreadMessage(deletedMsg, user, room);
}

const files = (message.files || [message.file]).filter(Boolean); // Keep compatibility with old messages
Expand Down Expand Up @@ -107,3 +107,25 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise<voi
void bridges.getListenerBridge().messageEvent(AppEvents.IPostMessageDeleted, deletedMsg, user);
}
}

async function deleteThreadMessage(message: IThreadMessage, user: IUser, room: IRoom | null): Promise<void> {
const { value: updatedParentMessage } = await Messages.decreaseReplyCountById(message.tmid, -1);

if (room) {
const { modifiedCount } = await Subscriptions.removeUnreadThreadsByRoomId(room._id, [message.tmid]);
if (modifiedCount > 0) {
// The replies array contains the ids of all the users that are following the thread (everyone that is involved + the ones who are following)
// Technically, user._id is already in the message.replies array, but since we don't have any strong
// guarantees of it, we are adding again to make sure it is there.
const userIdsThatAreWatchingTheThread = [...new Set([user._id, ...(message.replies || [])])];
// So they can decrement the unread threads count
void notifyOnSubscriptionChangedByRoomIdAndUserIds(room._id, userIdsThatAreWatchingTheThread);
}
}

if (updatedParentMessage && updatedParentMessage.tcount === 0) {
void notifyOnMessageChange({
id: message.tmid,
});
}
}
4 changes: 2 additions & 2 deletions apps/meteor/server/models/raw/Messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1779,13 +1779,13 @@ export class MessagesRaw extends BaseRaw<IMessage> implements IMessagesModel {
return this.col.countDocuments(query);
}

decreaseReplyCountById(_id: string, inc = -1): Promise<UpdateResult> {
decreaseReplyCountById(_id: string, inc = -1): Promise<ModifyResult<IMessage>> {
const query = { _id };
const update: UpdateFilter<IMessage> = {
$inc: {
tcount: inc,
},
};
return this.updateOne(query, update);
return this.findOneAndUpdate(query, update, { returnDocument: 'after' });
}
}
11 changes: 10 additions & 1 deletion apps/meteor/tests/data/chat.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ export const sendSimpleMessage = ({
roomId,
text = 'test message',
tmid,
userCredentials = credentials,
}: {
roomId: IRoom['_id'];
text?: string;
tmid?: IMessage['_id'];
userCredentials?: Credentials;
}) => {
if (!roomId) {
throw new Error('"roomId" is required in "sendSimpleMessage" test helper');
Expand All @@ -28,7 +30,7 @@ export const sendSimpleMessage = ({
message.tmid = tmid;
}

return request.post(api('chat.sendMessage')).set(credentials).send({ message });
return request.post(api('chat.sendMessage')).set(userCredentials).send({ message });
};

export const sendMessage = ({
Expand Down Expand Up @@ -87,3 +89,10 @@ export const getMessageById = ({ msgId }: { msgId: IMessage['_id'] }) => {
});
});
};

export const followMessage = ({ msgId, requestCredentials }: { msgId: IMessage['_id']; requestCredentials?: Credentials }) => {
return request
.post(api('chat.followMessage'))
.set(requestCredentials ?? credentials)
.send({ mid: msgId });
};
37 changes: 35 additions & 2 deletions apps/meteor/tests/data/rooms.helper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IRoom } from '@rocket.chat/core-typings';
import type { IRoom, ISubscription } from '@rocket.chat/core-typings';

import { api, credentials, request } from './api-data';
import { api, credentials, methodCall, request } from './api-data';

type CreateRoomParams = {
name?: IRoom['name'];
Expand Down Expand Up @@ -108,3 +108,36 @@ export function actionRoom({ action, type, roomId, overrideCredentials = credent

export const deleteRoom = ({ type, roomId }: { type: ActionRoomParams['type']; roomId: IRoom['_id'] }) =>
actionRoom({ action: 'delete', type, roomId, overrideCredentials: credentials });

export const getSubscriptionByRoomId = (roomId: IRoom['_id'], userCredentials = credentials): Promise<ISubscription> =>
new Promise((resolve) => {
void request
.get(api('subscriptions.getOne'))
.set(userCredentials)
.query({ roomId })
.end((_err, res) => {
resolve(res.body.subscription);
});
});

export const addUserToRoom = ({
usernames,
rid,
userCredentials,
}: {
usernames: string[];
rid: IRoom['_id'];
userCredentials?: Credentials;
}) => {
return request
.post(methodCall('addUsersToRoom'))
.set(userCredentials ?? credentials)
.send({
message: JSON.stringify({
method: 'addUsersToRoom',
params: [{ rid, users: usernames }],
id: 'id',
msg: 'method',
}),
});
};
52 changes: 49 additions & 3 deletions apps/meteor/tests/end-to-end/api/chat.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IMessage, IRoom, IThreadMessage, IUser } from '@rocket.chat/core-typings';
import type { IMessage, IRoom, ISubscription, IThreadMessage, IUser } from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import { expect } from 'chai';
import { after, before, beforeEach, describe, it } from 'mocha';
import type { Response } from 'supertest';

import { getCredentials, api, request, credentials } from '../../data/api-data';
import { sendSimpleMessage, deleteMessage } from '../../data/chat.helper';
import { followMessage, sendSimpleMessage, deleteMessage } from '../../data/chat.helper';
import { imgURL } from '../../data/interactions';
import { updatePermission, updateSetting } from '../../data/permissions.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { addUserToRoom, createRoom, deleteRoom, getSubscriptionByRoomId } from '../../data/rooms.helper';
import { password } from '../../data/user';
import type { TestUser } from '../../data/users.helper';
import { createUser, deleteUser, login } from '../../data/users.helper';
Expand Down Expand Up @@ -2005,6 +2005,52 @@ describe('[Chat]', () => {
})
.end(done);
});

describe('when deleting a thread message', () => {
let otherUser: TestUser<IUser>;
let otherUserCredentials: Credentials;
let parentThreadId: IMessage['_id'];

before(async () => {
const username = `user${+new Date()}`;
otherUser = await createUser({ username });
otherUserCredentials = await login(otherUser.username, password);
parentThreadId = (await sendSimpleMessage({ roomId: testChannel._id })).body.message._id;
await addUserToRoom({ rid: testChannel._id, usernames: [otherUser.username] });
});

after(() => Promise.all([deleteUser(otherUser), deleteMessage({ msgId: parentThreadId, roomId: testChannel._id })]));

const expectNoUnreadThreadMessages = (s: ISubscription) => {
expect(s).to.have.property('tunread');
expect(s.tunread).to.be.an('array');
expect(s.tunread).to.deep.equal([]);
};

it('should reset the unread counter if the message was removed', async () => {
const { body } = await sendSimpleMessage({ roomId: testChannel._id, tmid: parentThreadId, userCredentials: otherUserCredentials });
const childrenMessageId = body.message._id;

await followMessage({ msgId: parentThreadId, requestCredentials: otherUserCredentials });
await deleteMessage({ msgId: childrenMessageId, roomId: testChannel._id });

const userWhoCreatedTheThreadSubscription = await getSubscriptionByRoomId(testChannel._id);

expectNoUnreadThreadMessages(userWhoCreatedTheThreadSubscription);
});

it('should reset the unread counter of users who followed the thread', async () => {
const { body } = await sendSimpleMessage({ roomId: testChannel._id, tmid: parentThreadId });
const childrenMessageId = body.message._id;

await followMessage({ msgId: parentThreadId, requestCredentials: otherUserCredentials });
await deleteMessage({ msgId: childrenMessageId, roomId: testChannel._id });

const userWhoWasFollowingTheThreadSubscription = await getSubscriptionByRoomId(testChannel._id, otherUserCredentials);

expectNoUnreadThreadMessages(userWhoWasFollowingTheThreadSubscription);
});
});
});

describe('/chat.search', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/model-typings/src/models/IMessagesModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export interface IMessagesModel extends IBaseModel<IMessage> {
removeThreadFollowerByThreadId(tmid: string, userId: string): Promise<UpdateResult>;

findThreadsByRoomId(rid: string, skip: number, limit: number): FindCursor<IMessage>;
decreaseReplyCountById(_id: string, inc?: number): Promise<UpdateResult>;
decreaseReplyCountById(_id: string, inc?: number): Promise<ModifyResult<IMessage>>;
countPinned(options?: CountDocumentsOptions): Promise<number>;
countStarred(options?: CountDocumentsOptions): Promise<number>;
}

0 comments on commit c84543f

Please sign in to comment.