diff --git a/.changeset/heavy-boats-mix.md b/.changeset/heavy-boats-mix.md new file mode 100644 index 0000000000000..1477fce4c3564 --- /dev/null +++ b/.changeset/heavy-boats-mix.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/models': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue when sending a message on an omnichannel room would cause the web client to fetch the room data again. diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index a62c7dfea46be..11140feb08a10 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -175,7 +175,7 @@ export const RoutingManager: Routing = { const { servedBy } = room; if (shouldQueue) { - const queuedInquiry = await LivechatInquiry.queueInquiry(inquiry._id); + const queuedInquiry = await LivechatInquiry.queueInquiry(inquiry._id, room.lastMessage); if (queuedInquiry) { inquiry = queuedInquiry; void notifyOnLivechatInquiryChanged(inquiry, 'updated', { diff --git a/apps/meteor/app/livechat/server/lib/rooms.ts b/apps/meteor/app/livechat/server/lib/rooms.ts index 1d8cc1c043569..f6609fe1fc2b8 100644 --- a/apps/meteor/app/livechat/server/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/lib/rooms.ts @@ -226,6 +226,12 @@ export async function returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: return false; } + // update inquiry's last message with room's last message to correctly display in the queue + // because we stop updating the inquiry when it's been taken + if (room.lastMessage) { + await LivechatInquiry.setLastMessageById(inquiry._id, room.lastMessage); + } + const transferredBy = normalizeTransferredByData(user, room); livechatLogger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`); const transferData = { roomId: room._id, scope: 'queue', departmentId, transferredBy, ...overrideTransferData }; diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 1679319d8804c..4949ba2e002f9 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -40,6 +40,7 @@ import { makeAgentUnavailable, sendAgentMessage, fetchInquiry, + takeInquiry, } from '../../../data/livechat/rooms'; import { saveTags } from '../../../data/livechat/tags'; import { createMonitor, createUnit, deleteUnit } from '../../../data/livechat/units'; @@ -1170,6 +1171,67 @@ describe('LIVECHAT - rooms', () => { await deleteDepartment(forwardToOfflineDepartment._id); }); + (IS_EE ? it : it.skip)( + 'should update inquiry last message when manager forward to offline department and the inquiry returns to queued', + async () => { + await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); + const { department: initialDepartment, agent } = await createDepartmentWithAnOnlineAgent(); + const { department: forwardToOfflineDepartment, agent: offlineAgent } = await createDepartmentWithAnOfflineAgent({ + allowReceiveForwardOffline: true, + }); + + const newVisitor = await createVisitor(initialDepartment._id); + const newRoom = await createLivechatRoom(newVisitor.token); + + const inq = await fetchInquiry(newRoom._id); + await takeInquiry(inq._id, agent.credentials); + + const msgText = `return to queue ${Date.now()}`; + await request.post(api('livechat/message')).send({ token: newVisitor.token, rid: newRoom._id, msg: msgText }).expect(200); + + await makeAgentUnavailable(offlineAgent.credentials); + + const manager = await createUser(); + const managerCredentials = await login(manager.username, password); + await createManager(manager.username); + + await request.post(api('livechat/room.forward')).set(managerCredentials).send({ + roomId: newRoom._id, + departmentId: forwardToOfflineDepartment._id, + clientAction: true, + comment: 'test comment', + }); + + await request + .get(api(`livechat/queue`)) + .set(credentials) + .query({ + count: 1, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.queue).to.be.an('array'); + expect(res.body.queue[0].chats).not.to.undefined; + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + }); + + const inquiry = await fetchInquiry(newRoom._id); + + expect(inquiry).to.have.property('_id', inquiry._id); + expect(inquiry).to.have.property('rid', newRoom._id); + expect(inquiry).to.have.property('lastMessage'); + expect(inquiry.lastMessage).to.have.property('msg', ''); + expect(inquiry.lastMessage).to.have.property('t', 'livechat_transfer_history'); + + await deleteDepartment(initialDepartment._id); + await deleteDepartment(forwardToOfflineDepartment._id); + }, + ); + let roomId: string; let visitorToken: string; (IS_EE ? it : it.skip)('should return a success message when transferring to a fallback department', async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts b/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts index 05702deaaf709..b504de6da0be6 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts @@ -11,6 +11,7 @@ import { createDepartment, createLivechatRoom, createVisitor, + deleteVisitor, fetchInquiry, getLivechatRoomInfo, makeAgentAvailable, @@ -456,4 +457,88 @@ describe('LIVECHAT - inquiries', () => { expect(depInq.length).to.be.equal(1); }); }); + + describe('keep inquiry last message updated', () => { + let room: any; + let visitor: any; + let agent: any; + + before(async () => { + agent = await createAgent(); + visitor = await createVisitor(); + + await makeAgentAvailable(); + room = await createLivechatRoom(visitor.token); + }); + + after(async () => { + await deleteVisitor(visitor.token); + }); + + it('should update inquiry last message', async () => { + const msgText = `update inquiry ${Date.now()}`; + + await request.post(api('livechat/message')).send({ token: visitor.token, rid: room._id, msg: msgText }).expect(200); + + const inquiry = await fetchInquiry(room._id); + + expect(inquiry).to.have.property('_id', inquiry._id); + expect(inquiry).to.have.property('rid', room._id); + expect(inquiry).to.have.property('lastMessage'); + expect(inquiry.lastMessage).to.have.property('msg', msgText); + }); + + it('should update room last message after inquiry is taken', async () => { + const msgText = `update room ${Date.now()}`; + + const inquiry = await fetchInquiry(room._id); + + await request + .post(api('livechat/inquiries.take')) + .set(credentials) + .send({ + inquiryId: inquiry._id, + userId: agent._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + await request.post(api('livechat/message')).send({ token: visitor.token, rid: room._id, msg: msgText }).expect(200); + + // check room + const roomInfo = await getLivechatRoomInfo(room._id); + expect(roomInfo).to.have.property('lastMessage'); + expect(roomInfo.lastMessage).to.have.property('msg', msgText); + }); + + it('should have the correct last message when room is returned to queue', async () => { + const msgText = `return to queue ${Date.now()}`; + + await request.post(api('livechat/message')).send({ token: visitor.token, rid: room._id, msg: msgText }).expect(200); + + await request + .post(methodCall('livechat:returnAsInquiry')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:returnAsInquiry', + params: [room._id], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200); + + const inquiry = await fetchInquiry(room._id); + + expect(inquiry).to.have.property('_id', inquiry._id); + expect(inquiry).to.have.property('rid', room._id); + expect(inquiry).to.have.property('lastMessage'); + expect(inquiry.lastMessage).to.have.property('msg', msgText); + }); + }); }); diff --git a/packages/model-typings/src/models/ILivechatInquiryModel.ts b/packages/model-typings/src/models/ILivechatInquiryModel.ts index 19b52f3f90e9c..5b5283aec610c 100644 --- a/packages/model-typings/src/models/ILivechatInquiryModel.ts +++ b/packages/model-typings/src/models/ILivechatInquiryModel.ts @@ -16,6 +16,7 @@ export interface ILivechatInquiryModel extends IBaseModel; setDepartmentByInquiryId(inquiryId: string, department: string): Promise; setLastMessageByRoomId(rid: ILivechatInquiryRecord['rid'], message: IMessage): Promise; + setLastMessageById(inquiryId: string, lastMessage: IMessage): Promise; findNextAndLock( queueSortBy: FindOptions['sort'], department: string | null, @@ -31,7 +32,7 @@ export interface ILivechatInquiryModel extends IBaseModel): FindCursor; takeInquiry(inquiryId: string): Promise; openInquiry(inquiryId: string): Promise; - queueInquiry(inquiryId: string): Promise; + queueInquiry(inquiryId: string, lastMessage?: IMessage): Promise; queueInquiryAndRemoveDefaultAgent(inquiryId: string): Promise; readyInquiry(inquiryId: string): Promise; changeDepartmentIdByRoomId(rid: string, department: string): Promise; diff --git a/packages/models/src/models/LivechatInquiry.ts b/packages/models/src/models/LivechatInquiry.ts index d1361f265494c..79d581ce888ff 100644 --- a/packages/models/src/models/LivechatInquiry.ts +++ b/packages/models/src/models/LivechatInquiry.ts @@ -153,8 +153,19 @@ export class LivechatInquiryRaw extends BaseRaw implemen return this.findOneAndUpdate({ _id: inquiryId }, { $set: { department } }, { returnDocument: 'after' }); } + /** + * Updates the `lastMessage` of inquiries that are not taken yet, after they're taken we only need to update room's `lastMessage` + */ async setLastMessageByRoomId(rid: ILivechatInquiryRecord['rid'], message: IMessage): Promise { - return this.findOneAndUpdate({ rid }, { $set: { lastMessage: message } }, { returnDocument: 'after' }); + return this.findOneAndUpdate( + { rid, status: { $ne: LivechatInquiryStatus.TAKEN } }, + { $set: { lastMessage: message } }, + { returnDocument: 'after' }, + ); + } + + async setLastMessageById(inquiryId: string, lastMessage: IMessage): Promise { + return this.updateOne({ _id: inquiryId }, { $set: { lastMessage } }); } async findNextAndLock( @@ -316,13 +327,17 @@ export class LivechatInquiryRaw extends BaseRaw implemen ); } - async queueInquiry(inquiryId: string): Promise { + async queueInquiry(inquiryId: string, lastMessage?: IMessage): Promise { return this.findOneAndUpdate( { _id: inquiryId, }, { - $set: { status: LivechatInquiryStatus.QUEUED, queuedAt: new Date() }, + $set: { + status: LivechatInquiryStatus.QUEUED, + queuedAt: new Date(), + ...(lastMessage && { lastMessage }), + }, $unset: { takenAt: 1 }, }, { returnDocument: 'after' },