From bbbbdacc51149c3f6651a1cf61bba9d60773fbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Mon, 16 Oct 2023 14:25:30 -0300 Subject: [PATCH 1/7] feat: add tooltip to badge mentions (#30590) --- .../RoomList/SideBarItemTemplateWithData.tsx | 28 ++++++++++++++++++- .../rocketchat-i18n/i18n/en.i18n.json | 8 ++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx index b96c54d4c955d..f275ff2800d82 100644 --- a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx @@ -33,6 +33,30 @@ const getMessage = (room: IRoom, lastMessage: IMessage | undefined, t: ReturnTyp return `${lastMessage.u.name || lastMessage.u.username}: ${normalizeSidebarMessage(lastMessage, t)}`; }; +const getBadgeTitle = ( + userMentions: number, + threadUnread: number, + groupMentions: number, + unread: number, + t: ReturnType, +) => { + const title = [] as string[]; + if (userMentions) { + title.push(t('mentions_counter', { count: userMentions })); + } + if (threadUnread) { + title.push(t('threads_counter', { count: threadUnread })); + } + if (groupMentions) { + title.push(t('group_mentions_counter', { count: groupMentions })); + } + const count = unread - userMentions - groupMentions; + if (count > 0) { + title.push(t('unread_messages_counter', { count })); + } + return title.join(', '); +}; + type RoomListRowProps = { extended: boolean; t: ReturnType; @@ -137,10 +161,12 @@ function SideBarItemTemplateWithData({ const isUnread = unread > 0 || threadUnread; const showBadge = !hideUnreadStatus || (!hideMentionStatus && (Boolean(userMentions) || tunreadUser.length > 0)); + const badgeTitle = getBadgeTitle(userMentions, tunread.length, groupMentions, unread, t); + const badges = ( {showBadge && isUnread && ( - + {unread + tunread?.length} )} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 80ad18ad67436..001cdf080f7b5 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -6086,6 +6086,14 @@ "Filter_by_room": "Filter by room type", "Filter_by_visibility": "Filter by visibility", "Theme_Appearence": "Theme Appearence", + "mentions_counter": "{{count}} mention", + "mentions_counter_plural": "{{count}} mentions", + "threads_counter": "{{count}} unread threaded message", + "threads_counter_plural": "{{count}} unread threaded messages", + "group_mentions_counter": "{{count}} group mention", + "group_mentions_counter_plural": "{{count}} group mentions", + "unread_messages_counter": "{{count}} unread message", + "unread_messages_counter_plural": "{{count}} unread messages", "Premium": "Premium", "Premium_capability": "Premium capability" } \ No newline at end of file From b22da641303e3b22571519125de68311756ebf17 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 16 Oct 2023 11:50:45 -0600 Subject: [PATCH 2/7] refactor: Livechat functions to Typescript (#30631) --- .../app/apps/server/bridges/livechat.ts | 2 +- .../imports/server/rest/departments.ts | 10 +- .../meteor/app/livechat/server/api/v1/room.ts | 13 +- .../app/livechat/server/api/v1/videoCall.ts | 2 +- .../app/livechat/server/api/v1/visitor.ts | 2 +- .../server/hooks/saveContactLastChat.ts | 2 +- .../app/livechat/server/lib/Livechat.js | 125 +----------------- .../app/livechat/server/lib/LivechatTyped.ts | 111 +++++++++++++++- apps/meteor/lib/callbacks.ts | 3 +- .../server/services/omnichannel/service.ts | 2 +- packages/core-services/src/Events.ts | 4 +- packages/core-typings/src/ILivechatVisitor.ts | 2 +- .../src/models/ILivechatDepartmentModel.ts | 2 + 13 files changed, 137 insertions(+), 143 deletions(-) diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 76a0545c8801e..0ace08bb84466 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -288,7 +288,7 @@ export class AppLivechatBridge extends LivechatBridge { throw new Error('Could not get the message converter to process livechat room messages'); } - const livechatMessages = await Livechat.getRoomMessages({ rid: roomId }); + const livechatMessages = await LivechatTyped.getRoomMessages({ rid: roomId }); return Promise.all(livechatMessages.map((message) => messageConverter.convertMessage(message) as Promise)); } diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 6540b67d79aaf..095baefaa2942 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -17,6 +17,7 @@ import { } from '../../../server/api/lib/departments'; import { DepartmentHelper } from '../../../server/lib/Departments'; import { Livechat } from '../../../server/lib/Livechat'; +import { Livechat as LivechatTs } from '../../../server/lib/LivechatTyped'; API.v1.addRoute( 'livechat/department', @@ -192,7 +193,7 @@ API.v1.addRoute( }, { async post() { - await Livechat.archiveDepartment(this.urlParams._id); + await LivechatTs.archiveDepartment(this.urlParams._id); return API.v1.success(); }, @@ -207,11 +208,8 @@ API.v1.addRoute( }, { async post() { - if (await Livechat.unarchiveDepartment(this.urlParams._id)) { - return API.v1.success(); - } - - return API.v1.failure(); + await LivechatTs.unarchiveDepartment(this.urlParams._id); + return API.v1.success(); }, }, ); diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 86629e636bf84..8f61517974631 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -18,7 +18,7 @@ import { callbacks } from '../../../../../lib/callbacks'; import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; import { isWidget } from '../../../../api/server/helpers/isWidget'; -import { canAccessRoomAsync } from '../../../../authorization/server'; +import { canAccessRoomAsync, roomAccessAttributes } from '../../../../authorization/server'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { addUserToRoom } from '../../../../lib/server/functions/addUserToRoom'; import { settings as rcSettings } from '../../../../settings/server'; @@ -352,7 +352,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/room.visitor', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isPUTLivechatRoomVisitorParams, deprecationVersion: '7.0.0' }, + { + authRequired: true, + permissionsRequired: ['change-livechat-room-visitor'], + validateParams: isPUTLivechatRoomVisitorParams, + deprecationVersion: '7.0.0', + }, { async put() { // This endpoint is deprecated and will be removed in future versions. @@ -363,7 +368,7 @@ API.v1.addRoute( throw new Error('invalid-visitor'); } - const room = await LivechatRooms.findOneById(rid, { _id: 1, v: 1 }); // TODO: check _id + const room = await LivechatRooms.findOneById(rid, { projection: { ...roomAccessAttributes, _id: 1, t: 1, v: 1 } }); // TODO: check _id if (!room) { throw new Error('invalid-room'); } @@ -373,7 +378,7 @@ API.v1.addRoute( throw new Error('invalid-room-visitor'); } - const roomAfterChange = await Livechat.changeRoomVisitor(this.userId, rid, visitor); + const roomAfterChange = await LivechatTyped.changeRoomVisitor(this.userId, room, visitor); if (!roomAfterChange) { return API.v1.failure(); diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.ts b/apps/meteor/app/livechat/server/api/v1/videoCall.ts index 5ce0ddc4ca379..52cd8738bec99 100644 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.ts +++ b/apps/meteor/app/livechat/server/api/v1/videoCall.ts @@ -6,7 +6,7 @@ import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; import { canSendMessageAsync } from '../../../../authorization/server/functions/canSendMessage'; import { settings as rcSettings } from '../../../../settings/server'; -import { Livechat } from '../../lib/Livechat'; +import { Livechat } from '../../lib/LivechatTyped'; import { settings } from '../lib/livechat'; API.v1.addRoute( diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index ae9d1ea4fd837..84f7b96e155d3 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -174,7 +174,7 @@ API.v1.addRoute('livechat/visitor.callStatus', { if (!guest) { throw new Meteor.Error('invalid-token'); } - await Livechat.updateCallStatus(callId, rid, callStatus, guest); + await LivechatTyped.updateCallStatus(callId, rid, callStatus, guest); return API.v1.success({ token, callStatus }); }, }); diff --git a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts index 7b4d9b89f14c2..6f42a910417d6 100644 --- a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts +++ b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts @@ -1,7 +1,7 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { callbacks } from '../../../../lib/callbacks'; -import { Livechat } from '../lib/Livechat'; +import { Livechat } from '../lib/LivechatTyped'; callbacks.add( 'livechat.newRoom', diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index c560f3dd7aa76..e1d6626c7ddba 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -4,7 +4,7 @@ import dns from 'dns'; import util from 'util'; -import { Message, VideoConf, api } from '@rocket.chat/core-services'; +import { Message } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { LivechatVisitors, @@ -30,7 +30,6 @@ import { trim } from '../../../../lib/utils/stringUtils'; import { i18n } from '../../../../server/lib/i18n'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; -import { canAccessRoomAsync, roomAccessAttributes } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; @@ -718,40 +717,6 @@ export const Livechat = { return ret; }, - async unarchiveDepartment(_id) { - check(_id, String); - - const department = await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found', 'Department not found', { - method: 'livechat:removeDepartment', - }); - } - - // TODO: these kind of actions should be on events instead of here - await LivechatDepartmentAgents.enableAgentsByDepartmentId(_id); - return LivechatDepartmentRaw.unarchiveDepartment(_id); - }, - - async archiveDepartment(_id) { - check(_id, String); - - const department = await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found', 'Department not found', { - method: 'livechat:removeDepartment', - }); - } - - await LivechatDepartmentAgents.disableAgentsByDepartmentId(_id); - await LivechatDepartmentRaw.archiveDepartment(_id); - - this.logger.debug({ msg: 'Running livechat.afterDepartmentArchived callback for department:', departmentId: _id }); - await callbacks.run('livechat.afterDepartmentArchived', department); - }, - showConnecting() { const { showConnecting } = RoutingManager.getConfig(); return showConnecting; @@ -767,28 +732,6 @@ export const Livechat = { }); }, - async getRoomMessages({ rid }) { - check(rid, String); - - const room = await Rooms.findOneById(rid, { projection: { t: 1 } }); - if (room?.t !== 'l') { - throw new Meteor.Error('invalid-room'); - } - - const ignoredMessageTypes = [ - 'livechat_navigation_history', - 'livechat_transcript_history', - 'command', - 'livechat-close', - 'livechat-started', - 'livechat_video_call', - ]; - - return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { - sort: { ts: 1 }, - }).toArray(); - }, - async requestTranscript({ rid, email, subject, user }) { check(rid, String); check(email, String); @@ -892,20 +835,6 @@ export const Livechat = { }); }, - async notifyAgentStatusChanged(userId, status) { - callbacks.runAsync('livechat.agentStatusChanged', { userId, status }); - if (!settings.get('Livechat_show_agent_info')) { - return; - } - - await LivechatRooms.findOpenByAgent(userId).forEach((room) => { - void api.broadcast('omnichannel.room', room._id, { - type: 'agentStatus', - status, - }); - }); - }, - async allowAgentChangeServiceStatus(statusLivechat, agentId) { if (statusLivechat !== 'available') { return true; @@ -913,56 +842,4 @@ export const Livechat = { return businessHourManager.allowAgentChangeServiceStatus(agentId); }, - - notifyRoomVisitorChange(roomId, visitor) { - void api.broadcast('omnichannel.room', roomId, { - type: 'visitorData', - visitor, - }); - }, - - async changeRoomVisitor(userId, roomId, visitor) { - const user = await Users.findOneById(userId); - if (!user) { - throw new Error('error-user-not-found'); - } - - if (!(await hasPermissionAsync(userId, 'change-livechat-room-visitor'))) { - throw new Error('error-not-authorized'); - } - - const room = await LivechatRooms.findOneById(roomId, { ...roomAccessAttributes, _id: 1, t: 1 }); - - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - if (!(await canAccessRoomAsync(room, user))) { - throw new Error('error-not-allowed'); - } - - await LivechatRooms.changeVisitorByRoomId(room._id, visitor); - - Livechat.notifyRoomVisitorChange(room._id, visitor); - - return LivechatRooms.findOneById(roomId); - }, - async updateLastChat(contactId, lastChat) { - const updateUser = { - $set: { - lastChat, - }, - }; - await LivechatVisitors.updateById(contactId, updateUser); - }, - async updateCallStatus(callId, rid, status, user) { - await Rooms.setCallStatus(rid, status); - if (status === 'ended' || status === 'declined') { - if (await VideoConf.declineLivechatCall(callId)) { - return; - } - - return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date() }, user); - } - }, }; diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index c443bc7873c70..afb6494883005 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,4 +1,4 @@ -import { Message } from '@rocket.chat/core-services'; +import { Message, VideoConf, api } from '@rocket.chat/core-services'; import type { IOmnichannelRoom, IOmnichannelRoomClosingInfo, @@ -23,6 +23,7 @@ import { Users, LivechatDepartmentAgents, ReadReceipts, + Rooms, } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; @@ -32,8 +33,10 @@ import type { FindCursor, UpdateFilter } from 'mongodb'; import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; +import { canAccessRoomAsync } from '../../../authorization/server'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; +import { updateMessage } from '../../../lib/server/functions/updateMessage'; import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; @@ -808,6 +811,112 @@ class LivechatClass { return true; } + + async updateCallStatus(callId: string, rid: string, status: 'ended' | 'declined', user: IUser | ILivechatVisitor) { + await Rooms.setCallStatus(rid, status); + if (status === 'ended' || status === 'declined') { + if (await VideoConf.declineLivechatCall(callId)) { + return; + } + + return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date(), rid }, user as unknown as IUser); + } + } + + async updateLastChat(contactId: string, lastChat: Required) { + const updateUser = { + $set: { + lastChat, + }, + }; + await LivechatVisitors.updateById(contactId, updateUser); + } + + notifyRoomVisitorChange(roomId: string, visitor: ILivechatVisitor) { + void api.broadcast('omnichannel.room', roomId, { + type: 'visitorData', + visitor, + }); + } + + async changeRoomVisitor(userId: string, room: IOmnichannelRoom, visitor: ILivechatVisitor) { + const user = await Users.findOneById(userId, { projection: { _id: 1 } }); + if (!user) { + throw new Error('error-user-not-found'); + } + + if (!(await canAccessRoomAsync(room, user))) { + throw new Error('error-not-allowed'); + } + + await LivechatRooms.changeVisitorByRoomId(room._id, visitor); + + this.notifyRoomVisitorChange(room._id, visitor); + + return LivechatRooms.findOneById(room._id); + } + + async notifyAgentStatusChanged(userId: string, status?: UserStatus) { + if (!status) { + return; + } + + void callbacks.runAsync('livechat.agentStatusChanged', { userId, status }); + if (!settings.get('Livechat_show_agent_info')) { + return; + } + + await LivechatRooms.findOpenByAgent(userId).forEach((room) => { + void api.broadcast('omnichannel.room', room._id, { + type: 'agentStatus', + status, + }); + }); + } + + async getRoomMessages({ rid }: { rid: string }) { + const room = await Rooms.findOneById(rid, { projection: { t: 1 } }); + if (room?.t !== 'l') { + throw new Meteor.Error('invalid-room'); + } + + const ignoredMessageTypes: MessageTypesValues[] = [ + 'livechat_navigation_history', + 'livechat_transcript_history', + 'command', + 'livechat-close', + 'livechat-started', + 'livechat_video_call', + ]; + + return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { + sort: { ts: 1 }, + }).toArray(); + } + + async archiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Error('department-not-found'); + } + + await Promise.all([LivechatDepartmentAgents.disableAgentsByDepartmentId(_id), LivechatDepartment.archiveDepartment(_id)]); + + await callbacks.run('livechat.afterDepartmentArchived', department); + } + + async unarchiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Meteor.Error('department-not-found'); + } + + // TODO: these kind of actions should be on events instead of here + await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); + return true; + } } export const Livechat = new LivechatClass(); diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 169144cc27881..4d59f52e9cd6e 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -18,6 +18,7 @@ import type { ILivechatTagRecord, TransferData, AtLeast, + UserStatus, } from '@rocket.chat/core-typings'; import type { FilterOperators } from 'mongodb'; @@ -52,7 +53,7 @@ interface EventLikeCallbackSignatures { 'livechat.saveRoom': (room: IRoom) => void; 'livechat:afterReturnRoomAsInquiry': (params: { room: IRoom }) => void; 'livechat.setUserStatusLivechat': (params: { userId: IUser['_id']; status: OmnichannelAgentStatus }) => void; - 'livechat.agentStatusChanged': (params: { userId: IUser['_id']; status: OmnichannelAgentStatus }) => void; + 'livechat.agentStatusChanged': (params: { userId: IUser['_id']; status: UserStatus }) => void; 'livechat.onNewAgentCreated': (agentId: string) => void; 'livechat.afterTakeInquiry': (inq: InquiryWithAgentInfo, agent: { agentId: string; username: string }) => void; 'livechat.afterAgentRemoved': (params: { agent: Pick }) => void; diff --git a/apps/meteor/server/services/omnichannel/service.ts b/apps/meteor/server/services/omnichannel/service.ts index 7f35de104e1cd..61c22505ca98d 100644 --- a/apps/meteor/server/services/omnichannel/service.ts +++ b/apps/meteor/server/services/omnichannel/service.ts @@ -2,7 +2,7 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { IOmnichannelService } from '@rocket.chat/core-services'; import type { IOmnichannelQueue } from '@rocket.chat/core-typings'; -import { Livechat } from '../../../app/livechat/server'; +import { Livechat } from '../../../app/livechat/server/lib/LivechatTyped'; import { RoutingManager } from '../../../app/livechat/server/lib/RoutingManager'; import { settings } from '../../../app/settings/server'; import { OmnichannelQueue } from './queue'; diff --git a/packages/core-services/src/Events.ts b/packages/core-services/src/Events.ts index 88ca1034b9c98..e2a7f624d8df3 100644 --- a/packages/core-services/src/Events.ts +++ b/packages/core-services/src/Events.ts @@ -32,6 +32,7 @@ import type { ILivechatInquiryRecord, ILivechatAgent, IBanner, + ILivechatVisitor, } from '@rocket.chat/core-typings'; import type { AutoUpdateRecord } from './types/IMeteor'; @@ -242,7 +243,8 @@ export type EventSignatures = { data: | { type: 'agentStatus'; status: string } | { type: 'queueData'; data: { [k: string]: unknown } | undefined } - | { type: 'agentData'; data: ILivechatAgent | undefined | { hiddenInfo: boolean } }, + | { type: 'agentData'; data: ILivechatAgent | undefined | { hiddenInfo: boolean } } + | { type: 'visitorData'; visitor: ILivechatVisitor }, ): void; // Send all events from here diff --git a/packages/core-typings/src/ILivechatVisitor.ts b/packages/core-typings/src/ILivechatVisitor.ts index e80d63ab15d02..21819cc23f246 100644 --- a/packages/core-typings/src/ILivechatVisitor.ts +++ b/packages/core-typings/src/ILivechatVisitor.ts @@ -7,7 +7,7 @@ export interface IVisitorPhone { export interface IVisitorLastChat { _id: string; - ts: string; + ts: Date; } export interface ILivechatVisitorConnectionData { diff --git a/packages/model-typings/src/models/ILivechatDepartmentModel.ts b/packages/model-typings/src/models/ILivechatDepartmentModel.ts index a074d5c311264..75fe0f54b2eb4 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentModel.ts @@ -71,4 +71,6 @@ export interface ILivechatDepartmentModel extends IBaseModel; countArchived(): Promise; findEnabledInIds(departmentsIds: string[], options?: FindOptions): FindCursor; + archiveDepartment(_id: string): Promise; + unarchiveDepartment(_id: string): Promise; } From c38711b3464b75905b88ea7eadb289bfb4946061 Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Mon, 16 Oct 2023 15:50:31 -0300 Subject: [PATCH 3/7] feat: Count daily and monthly peaks of concurrent connections (#30573) --- .changeset/perfect-pianos-yawn.md | 5 +++++ .changeset/slow-coats-shout.md | 7 +++++++ .../app/statistics/server/lib/statistics.ts | 7 ++++++- .../admin/info/DeploymentCard.stories.tsx | 2 ++ .../admin/info/InformationPage.stories.tsx | 2 ++ .../views/admin/info/UsageCard.stories.tsx | 2 ++ apps/meteor/server/cron/statistics.ts | 4 +++- apps/meteor/server/models/raw/Statistics.ts | 21 +++++++++++++++++++ ee/packages/presence/src/Presence.ts | 15 +++++++++++++ packages/core-services/src/types/IPresence.ts | 2 ++ packages/core-typings/src/IStats.ts | 2 ++ .../src/models/IStatisticsModel.ts | 1 + 12 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 .changeset/perfect-pianos-yawn.md create mode 100644 .changeset/slow-coats-shout.md diff --git a/.changeset/perfect-pianos-yawn.md b/.changeset/perfect-pianos-yawn.md new file mode 100644 index 0000000000000..349bca33ecf7d --- /dev/null +++ b/.changeset/perfect-pianos-yawn.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/presence': minor +--- + +Add peak connections monitoring and methods to get and reset the counter diff --git a/.changeset/slow-coats-shout.md b/.changeset/slow-coats-shout.md new file mode 100644 index 0000000000000..4a226e84d1611 --- /dev/null +++ b/.changeset/slow-coats-shout.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +--- + +Add the daily and monthly peaks of concurrent connections to statistics + - Added `dailyPeakConnections` statistic for monitoring the daily peak of concurrent connections in a workspace; + - Added `maxMonthlyPeakConnections` statistic for monitoring the last 30 days peak of concurrent connections in a workspace; diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 64543deb88a11..1d60def846b54 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -1,7 +1,7 @@ import { log } from 'console'; import os from 'os'; -import { Analytics, Team, VideoConf } from '@rocket.chat/core-services'; +import { Analytics, Team, VideoConf, Presence } from '@rocket.chat/core-services'; import type { IRoom, IStats } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; import { @@ -579,6 +579,11 @@ export const statistics = { const defaultLoggedInCustomScript = (await Settings.findOneById('Custom_Script_Logged_In'))?.packageValue; statistics.loggedInCustomScriptChanged = settings.get('Custom_Script_Logged_In') !== defaultLoggedInCustomScript; + statistics.dailyPeakConnections = await Presence.getPeakConnections(true); + + const peak = await Statistics.findMonthlyPeakConnections(); + statistics.maxMonthlyPeakConnections = Math.max(statistics.dailyPeakConnections, peak?.dailyPeakConnections || 0); + statistics.matrixFederation = await getMatrixFederationStatistics(); // Omnichannel call stats diff --git a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx index 41709d247f8b4..d3242e68ae2cc 100644 --- a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx +++ b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx @@ -272,6 +272,8 @@ export default { totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, push: 0, + dailyPeakConnections: 0, + maxMonthlyPeakConnections: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx index 29c0c00d58140..d05c0f7372a25 100644 --- a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx +++ b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx @@ -302,6 +302,8 @@ export default { totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, push: 0, + dailyPeakConnections: 0, + maxMonthlyPeakConnections: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx index a65e645b17d06..4c6cc871ede7e 100644 --- a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx +++ b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx @@ -250,6 +250,8 @@ export default { totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, push: 0, + dailyPeakConnections: 0, + maxMonthlyPeakConnections: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/server/cron/statistics.ts b/apps/meteor/server/cron/statistics.ts index 27c1fc064e25f..7e7dea6adbc77 100644 --- a/apps/meteor/server/cron/statistics.ts +++ b/apps/meteor/server/cron/statistics.ts @@ -37,5 +37,7 @@ export async function statsCron(logger: Logger): Promise { const now = new Date(); - await cronJobs.add(name, `12 ${now.getHours()} * * *`, async () => generateStatistics(logger)); + await cronJobs.add(name, `12 ${now.getHours()} * * *`, async () => { + await generateStatistics(logger); + }); } diff --git a/apps/meteor/server/models/raw/Statistics.ts b/apps/meteor/server/models/raw/Statistics.ts index 1ad0ab9939100..bad44ee07c236 100644 --- a/apps/meteor/server/models/raw/Statistics.ts +++ b/apps/meteor/server/models/raw/Statistics.ts @@ -25,4 +25,25 @@ export class StatisticsRaw extends BaseRaw implements IStatisticsModel { ).toArray(); return records?.[0]; } + + async findMonthlyPeakConnections() { + const oneMonthAgo = new Date(); + oneMonthAgo.setDate(oneMonthAgo.getDate() - 30); + oneMonthAgo.setHours(0, 0, 0, 0); + + return this.findOne>( + { + createdAt: { $gte: oneMonthAgo }, + }, + { + sort: { + dailyPeakConnections: -1, + }, + projection: { + dailyPeakConnections: 1, + createdAt: 1, + }, + }, + ); + } } diff --git a/ee/packages/presence/src/Presence.ts b/ee/packages/presence/src/Presence.ts index fb656fc3e1580..5bd69e1f4fc80 100755 --- a/ee/packages/presence/src/Presence.ts +++ b/ee/packages/presence/src/Presence.ts @@ -19,6 +19,8 @@ export class Presence extends ServiceClass implements IPresence { private connsPerInstance = new Map(); + private peakConnections = 0; + constructor() { super(); @@ -35,6 +37,7 @@ export class Presence extends ServiceClass implements IPresence { if (diff?.hasOwnProperty('extraInformation.conns')) { this.connsPerInstance.set(id, diff['extraInformation.conns']); + this.peakConnections = Math.max(this.peakConnections, this.getTotalConnections()); this.validateAvailability(); } }); @@ -251,4 +254,16 @@ export class Presence extends ServiceClass implements IPresence { private getTotalConnections(): number { return Array.from(this.connsPerInstance.values()).reduce((acc, conns) => acc + conns, 0); } + + getPeakConnections(reset = false): number { + const peak = this.peakConnections; + if (reset) { + this.resetPeakConnections(); + } + return peak; + } + + resetPeakConnections(): void { + this.peakConnections = 0; + } } diff --git a/packages/core-services/src/types/IPresence.ts b/packages/core-services/src/types/IPresence.ts index 197f9b685cf80..5f7c57d679955 100644 --- a/packages/core-services/src/types/IPresence.ts +++ b/packages/core-services/src/types/IPresence.ts @@ -19,4 +19,6 @@ export interface IPresence extends IServiceClass { updateUserPresence(uid: string): Promise; toggleBroadcast(enabled: boolean): void; getConnectionCount(): { current: number; max: number }; + getPeakConnections(reset?: boolean): number; + resetPeakConnections(): void; } diff --git a/packages/core-typings/src/IStats.ts b/packages/core-typings/src/IStats.ts index 443cbfb239578..7fd5cd8218bc9 100644 --- a/packages/core-typings/src/IStats.ts +++ b/packages/core-typings/src/IStats.ts @@ -219,6 +219,8 @@ export interface IStats { totalWebRTCCalls: number; uncaughtExceptionsCount: number; push: number; + dailyPeakConnections: number; + maxMonthlyPeakConnections: number; matrixFederation: { enabled: boolean; }; diff --git a/packages/model-typings/src/models/IStatisticsModel.ts b/packages/model-typings/src/models/IStatisticsModel.ts index ac84a49525b36..fe4534eaee0f2 100644 --- a/packages/model-typings/src/models/IStatisticsModel.ts +++ b/packages/model-typings/src/models/IStatisticsModel.ts @@ -4,4 +4,5 @@ import type { IBaseModel } from './IBaseModel'; export interface IStatisticsModel extends IBaseModel { findLast(): Promise; + findMonthlyPeakConnections(): Promise | null>; } From 809eb63d79d03cbbde4d40f4970f5cc25ffe4f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=87=E3=83=AF=E3=83=B3=E3=82=B7=E3=83=A5?= <61188295+Dnouv@users.noreply.github.com> Date: Tue, 17 Oct 2023 01:20:26 +0530 Subject: [PATCH 4/7] fix: apps typing indicator not working (#30360) --- .changeset/rich-dogs-smell.md | 5 +++++ apps/meteor/app/apps/server/bridges/messages.ts | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/rich-dogs-smell.md diff --git a/.changeset/rich-dogs-smell.md b/.changeset/rich-dogs-smell.md new file mode 100644 index 0000000000000..be27db28e2272 --- /dev/null +++ b/.changeset/rich-dogs-smell.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Fix typing indicator of Apps user diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts index d75a0c2446743..e4d09018176d5 100644 --- a/apps/meteor/app/apps/server/bridges/messages.ts +++ b/apps/meteor/app/apps/server/bridges/messages.ts @@ -103,7 +103,11 @@ export class AppMessageBridge extends MessageBridge { protected async typing({ scope, id, username, isTyping }: ITypingDescriptor): Promise { switch (scope) { case 'room': - notifications.notifyRoom(id, 'typing', username!, isTyping); + if (!username) { + throw new Error('Invalid username'); + } + + notifications.notifyRoom(id, 'user-activity', username, isTyping ? ['user-typing'] : []); return; default: throw new Error('Unrecognized typing scope provided'); From a0dcc38574c10a250b266209a783bea48a13fddd Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:35:46 -0300 Subject: [PATCH 5/7] regression: failing to create rooms in some integrations (#30649) --- apps/meteor/app/cas/server/cas_server.js | 2 +- .../app/irc/server/irc-bridge/peerHandlers/joinedChannel.js | 2 +- apps/meteor/app/slackbridge/server/RocketAdapter.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/cas/server/cas_server.js b/apps/meteor/app/cas/server/cas_server.js index 25d3b9fdd698e..60880c77d4f40 100644 --- a/apps/meteor/app/cas/server/cas_server.js +++ b/apps/meteor/app/cas/server/cas_server.js @@ -257,7 +257,7 @@ Accounts.registerLoginHandler('cas', async (options) => { if (roomName) { let room = await Rooms.findOneByNameAndType(roomName, 'c'); if (!room) { - room = await createRoom('c', roomName, user.username); + room = await createRoom('c', roomName, user); } } } diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js index 0968eacc5340e..bb5053ffdd71c 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js @@ -16,7 +16,7 @@ export default async function handleJoinedChannel(args) { let room = await Rooms.findOneByName(args.roomName); if (!room) { - const createdRoom = await createRoom('c', args.roomName, user.username, []); + const createdRoom = await createRoom('c', args.roomName, user, []); room = await Rooms.findOne({ _id: createdRoom.rid }); this.log(`${user.username} created room ${args.roomName}`); diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.js b/apps/meteor/app/slackbridge/server/RocketAdapter.js index d0ef8157137da..f76c33fa1f81f 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.js +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.js @@ -295,7 +295,7 @@ export default class RocketAdapter { try { const isPrivate = slackChannel.is_private; - const rocketChannel = await createRoom(isPrivate ? 'p' : 'c', slackChannel.name, rocketUserCreator.username, rocketUsers); + const rocketChannel = await createRoom(isPrivate ? 'p' : 'c', slackChannel.name, rocketUserCreator, rocketUsers); slackChannel.rocketId = rocketChannel.rid; } catch (e) { if (!hasRetried) { From 85ddfb24baccdcbae56ffaf7a070b83128b4c7fb Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Mon, 16 Oct 2023 18:46:51 -0300 Subject: [PATCH 6/7] fix: licenses.info endpoint only available for admins (#30644) --- apps/meteor/ee/server/api/licenses.ts | 7 ++- .../tests/end-to-end/api/20-licenses.js | 46 +++++++++++++++++++ .../license/src/definition/LicenseInfo.ts | 10 ++++ ee/packages/license/src/index.ts | 11 ++--- ee/packages/license/src/license.ts | 19 +++++--- packages/rest-typings/src/v1/licenses.ts | 8 +--- 6 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 ee/packages/license/src/definition/LicenseInfo.ts diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index ff5c3fcc3e47f..b7ac3ba81e9c0 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -25,10 +25,13 @@ API.v1.addRoute( API.v1.addRoute( 'licenses.info', - { authRequired: true, validateParams: isLicensesInfoProps, permissionsRequired: ['view-privileged-setting'] }, + { authRequired: true, validateParams: isLicensesInfoProps }, { async get() { - const data = await License.getInfo(Boolean(this.queryParams.loadValues)); + const unrestrictedAccess = await hasPermissionAsync(this.userId, 'view-privileged-setting'); + const loadCurrentValues = unrestrictedAccess && Boolean(this.queryParams.loadValues); + + const data = await License.getInfo({ limits: unrestrictedAccess, license: unrestrictedAccess, currentValues: loadCurrentValues }); return API.v1.success({ data }); }, diff --git a/apps/meteor/tests/end-to-end/api/20-licenses.js b/apps/meteor/tests/end-to-end/api/20-licenses.js index 993428d344099..302011addef9f 100644 --- a/apps/meteor/tests/end-to-end/api/20-licenses.js +++ b/apps/meteor/tests/end-to-end/api/20-licenses.js @@ -105,6 +105,52 @@ describe('licenses', function () { }); }); + describe('[/licenses.info]', () => { + it('should fail if not logged in', (done) => { + request + .get(api('licenses.info')) + .expect('Content-Type', 'application/json') + .expect(401) + .expect((res) => { + expect(res.body).to.have.property('status', 'error'); + expect(res.body).to.have.property('message'); + }) + .end(done); + }); + + it('should return limited information if user is unauthorized', (done) => { + request + .get(api('licenses.info')) + .set(unauthorizedUserCredentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('data').and.to.be.an('object'); + expect(res.body.data).to.not.have.property('license'); + expect(res.body.data).to.have.property('tags').and.to.be.an('array'); + }) + .end(done); + }); + + it('should return unrestricted info if user is logged in and is authorized', (done) => { + request + .get(api('licenses.info')) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('data').and.to.be.an('object'); + if (process.env.IS_EE) { + expect(res.body.data).to.have.property('license').and.to.be.an('object'); + } + expect(res.body.data).to.have.property('tags').and.to.be.an('array'); + }) + + .end(done); + }); + }); + describe('[/licenses.isEnterprise]', () => { it('should fail if not logged in', (done) => { request diff --git a/ee/packages/license/src/definition/LicenseInfo.ts b/ee/packages/license/src/definition/LicenseInfo.ts new file mode 100644 index 0000000000000..7de3c0cfbdd60 --- /dev/null +++ b/ee/packages/license/src/definition/LicenseInfo.ts @@ -0,0 +1,10 @@ +import type { ILicenseTag } from './ILicenseTag'; +import type { ILicenseV3, LicenseLimitKind } from './ILicenseV3'; +import type { LicenseModule } from './LicenseModule'; + +export type LicenseInfo = { + license?: ILicenseV3; + activeModules: LicenseModule[]; + limits: Record; + tags: ILicenseTag[]; +}; diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index 77e2976f156a7..9707a41d96ab7 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -1,5 +1,5 @@ -import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; -import type { LicenseModule } from './definition/LicenseModule'; +import type { LicenseLimitKind } from './definition/ILicenseV3'; +import type { LicenseInfo } from './definition/LicenseInfo'; import type { LimitContext } from './definition/LimitContext'; import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; import { onLicense } from './events/deprecated'; @@ -24,6 +24,7 @@ export * from './definition/ILicenseTag'; export * from './definition/ILicenseV2'; export * from './definition/ILicenseV3'; export * from './definition/LicenseBehavior'; +export * from './definition/LicenseInfo'; export * from './definition/LicenseLimit'; export * from './definition/LicenseModule'; export * from './definition/LicensePeriod'; @@ -49,11 +50,7 @@ interface License { onBehaviorTriggered: typeof onBehaviorTriggered; revalidateLicense: () => Promise; - getInfo: (loadCurrentValues: boolean) => Promise<{ - license: ILicenseV3 | undefined; - activeModules: LicenseModule[]; - limits: Record; - }>; + getInfo: (info: { limits: boolean; currentValues: boolean; license: boolean }) => Promise; // Deprecated: onLicense: typeof onLicense; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 8449d4136810b..d24d91287d1ea 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -4,6 +4,7 @@ import { type ILicenseTag } from './definition/ILicenseTag'; import type { ILicenseV2 } from './definition/ILicenseV2'; import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; import type { BehaviorWithContext } from './definition/LicenseBehavior'; +import type { LicenseInfo } from './definition/LicenseInfo'; import type { LicenseModule } from './definition/LicenseModule'; import type { LicenseValidationOptions } from './definition/LicenseValidationOptions'; import type { LimitContext } from './definition/LimitContext'; @@ -291,17 +292,22 @@ export class LicenseManager extends Emitter { return isBehaviorsInResult(validationResult, ['prevent_action']); } - public async getInfo(loadCurrentValues = false): Promise<{ - license: ILicenseV3 | undefined; - activeModules: LicenseModule[]; - limits: Record; - }> { + public async getInfo({ + limits: includeLimits, + currentValues: loadCurrentValues, + license: includeLicense, + }: { + limits: boolean; + currentValues: boolean; + license: boolean; + }): Promise { const activeModules = getModules.call(this); const license = this.getLicense(); // Get all limits present in the license and their current value const limits = ( (license && + includeLimits && (await Promise.all( globalLimitKinds .map((limitKey) => ({ @@ -322,9 +328,10 @@ export class LicenseManager extends Emitter { ).reduce((prev, curr) => ({ ...prev, ...curr }), {}); return { - license, + license: (includeLicense && license) || undefined, activeModules, limits: limits as Record, + tags: license?.information.tags || [], }; } } diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index 87c0106f6d3f9..d229ca49f1fc9 100644 --- a/packages/rest-typings/src/v1/licenses.ts +++ b/packages/rest-typings/src/v1/licenses.ts @@ -1,4 +1,4 @@ -import type { ILicenseV2, ILicenseV3, LicenseLimitKind } from '@rocket.chat/license'; +import type { ILicenseV2, ILicenseV3, LicenseInfo } from '@rocket.chat/license'; import Ajv from 'ajv'; const ajv = new Ajv({ @@ -45,11 +45,7 @@ export type LicensesEndpoints = { }; '/v1/licenses.info': { GET: (params: licensesInfoProps) => { - data: { - license: ILicenseV3 | undefined; - activeModules: string[]; - limits: Record; - }; + data: LicenseInfo; }; }; '/v1/licenses.add': { From ff2263a3c11d59f9e964c4f1f6b6926521f9283c Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:25:51 -0300 Subject: [PATCH 7/7] fix: Read receipts are not created on the first time a user reads a room (#30610) Co-authored-by: Heitor Tanoue <68477006+heitortanoue@users.noreply.github.com> --- .changeset/weak-cameras-pay.md | 5 +++++ apps/meteor/server/lib/readMessages.ts | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/weak-cameras-pay.md diff --git a/.changeset/weak-cameras-pay.md b/.changeset/weak-cameras-pay.md new file mode 100644 index 0000000000000..724f3af69a298 --- /dev/null +++ b/.changeset/weak-cameras-pay.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue with message read receipts not being created when accessing a room the first time diff --git a/apps/meteor/server/lib/readMessages.ts b/apps/meteor/server/lib/readMessages.ts index 00bf04bd3449b..d7c8cf5592887 100644 --- a/apps/meteor/server/lib/readMessages.ts +++ b/apps/meteor/server/lib/readMessages.ts @@ -6,7 +6,7 @@ import { callbacks } from '../../lib/callbacks'; export async function readMessages(rid: IRoom['_id'], uid: IUser['_id'], readThreads: boolean): Promise { await callbacks.run('beforeReadMessages', rid, uid); - const projection = { ls: 1, tunread: 1, alert: 1 }; + const projection = { ls: 1, tunread: 1, alert: 1, ts: 1 }; const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, uid, { projection }); if (!sub) { throw new Error('error-invalid-subscription'); @@ -19,5 +19,6 @@ export async function readMessages(rid: IRoom['_id'], uid: IUser['_id'], readThr await NotificationQueue.clearQueueByUserId(uid); - callbacks.runAsync('afterReadMessages', rid, { uid, lastSeen: sub.ls }); + const lastSeen = sub.ls || sub.ts; + callbacks.runAsync('afterReadMessages', rid, { uid, lastSeen }); }