From 7eef8ff3d49c6b276da53d8b93bf488a0e19d05d Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Tue, 23 Feb 2021 11:29:08 +0530 Subject: [PATCH 01/30] Add settings for on-hold feature --- ee/app/livechat-enterprise/server/settings.js | 73 +++++++++++++++---- packages/rocketchat-i18n/i18n/en.i18n.json | 9 ++- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/ee/app/livechat-enterprise/server/settings.js b/ee/app/livechat-enterprise/server/settings.js index 89c15ea1890f4..070216c942b29 100644 --- a/ee/app/livechat-enterprise/server/settings.js +++ b/ee/app/livechat-enterprise/server/settings.js @@ -53,11 +53,16 @@ export const createSettings = () => { ], }); - settings.add('Livechat_auto_close_abandoned_rooms', false, { - type: 'boolean', + settings.add('Livechat_abandoned_rooms_action', false, { + type: 'select', group: 'Omnichannel', section: 'Sessions', - i18nLabel: 'Enable_omnichannel_auto_close_abandoned_rooms', + values: [ + { key: 'none', i18nLabel: 'Do_Nothing' }, + { key: 'close', i18nLabel: 'Livechat_close_chat' }, + { key: 'on-hold', i18nLabel: 'Livechat_onHold_Chat' }, + ], + value: 'none', enterprise: true, invalidValue: false, modules: [ @@ -70,7 +75,7 @@ export const createSettings = () => { group: 'Omnichannel', section: 'Sessions', i18nLabel: 'Livechat_abandoned_rooms_closed_custom_message', - enableQuery: { _id: 'Livechat_auto_close_abandoned_rooms', value: true }, + enableQuery: { _id: 'Livechat_abandoned_rooms_action', value: 'close' }, enterprise: true, invalidValue: '', modules: [ @@ -89,18 +94,6 @@ export const createSettings = () => { ], }); - settings.add('Livechat_auto_transfer_chat_timeout', 0, { - type: 'int', - group: 'Omnichannel', - section: 'Sessions', - i18nDescription: 'Livechat_auto_transfer_chat_timeout_description', - enterprise: true, - invalidValue: 0, - modules: [ - 'livechat-enterprise', - ], - }); - settings.addGroup('Omnichannel', function() { this.section('Business_Hours', function() { this.add('Livechat_business_hour_type', 'Single', { @@ -134,5 +127,53 @@ export const createSettings = () => { ], }); + settings.add('Livechat_auto_close_on_hold_chats_timeout', true, { + type: 'int', + group: 'Omnichannel', + section: 'Sessions', + enterprise: true, + value: 60, + invalidValue: 0, + modules: [ + 'livechat-enterprise', + ], + }); + + settings.add('Livechat_allow_manual_on_hold', true, { + type: 'boolean', + group: 'Omnichannel', + section: 'Sessions', + enterprise: true, + value: true, + invalidValue: false, + modules: [ + 'livechat-enterprise', + ], + }); + + settings.add('Livechat_manual_on_hold_timeout', true, { + type: 'int', + group: 'Omnichannel', + section: 'Sessions', + enterprise: true, + value: 60, + invalidValue: 0, + modules: [ + 'livechat-enterprise', + ], + }); + + settings.add('Livechat_auto_transfer_chat_timeout', 0, { + type: 'int', + group: 'Omnichannel', + section: 'Sessions', + i18nDescription: 'Livechat_auto_transfer_chat_timeout_description', + enterprise: true, + invalidValue: 0, + modules: [ + 'livechat-enterprise', + ], + }); + Settings.addOptionValueById('Livechat_Routing_Method', { key: 'Load_Balancing', i18nLabel: 'Load_Balancing' }); }; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index b9108a6a8ecbf..4b36e438fe8c1 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1370,6 +1370,7 @@ "Displays_action_text": "Displays action text", "Do_not_display_unread_counter": "Do not display any counter of this channel", "Do_not_provide_this_code_to_anyone": "Do not provide this code to anyone.", + "Do_Nothing": "Do Nothing", "Do_you_want_to_accept": "Do you want to accept?", "Do_you_want_to_change_to_s_question": "Do you want to change to %s?", "Document_Domain": "Document Domain", @@ -2399,15 +2400,19 @@ "List_of_Direct_Messages": "List of Direct Messages", "Omnichannel": "Omnichannel", "Livechat": "Livechat", + "Livechat_abandoned_rooms_action": "How to handle Visitor Abandonment", "Livechat_abandoned_rooms_closed_custom_message": "Custom message when room is automatically closed by visitor inactivity", "Livechat_agents": "Omnichannel agents", "Livechat_Agents": "Agents", + "Livechat_allow_manual_on_hold": "Allow agents to manually place chat On Hold", "Livechat_AllowedDomainsList": "Livechat Allowed Domains", "Livechat_Appearance": "Livechat Appearance", + "Livechat_auto_close_on_hold_chats_timeout": "How long the chat will remain in the On Hold queue until it's automatically closed by the system (in minutes) ?", "Livechat_auto_transfer_chat_timeout": "Timeout (in seconds) for automatic transfer of unanswered chats to another agent", "Livechat_auto_transfer_chat_timeout_description": "This event takes place only when the chat has just started. After the first transfering for inactivity, the room is no longer monitored.", "Livechat_business_hour_type": "Business Hour Type (Single or Multiple)", "Livechat_chat_transcript_sent": "Chat transcript sent: __transcript__", + "Livechat_close_chat": "Close the abandoned chat", "Livechat_custom_fields_options_placeholder": "Comma-separated list used to select a pre-configured value. Spaces between elements are not accepted.", "Livechat_custom_fields_public_description": "Public custom fields will be displayed in external applications, such as Livechat, etc.", "Livechat_Dashboard": "Omnichannel Dashboard", @@ -2426,12 +2431,14 @@ "Livechat_last_chatted_agent_routing_Description": "The Last-Chatted Agent setting allocates chats to the agent who previously interacted with the same visitor if the agent is available when the chat starts.", "Livechat_managers": "Omnichannel managers", "Livechat_Managers": "Managers", + "Livechat_manual_on_hold_timeout": "How long an agent has to wait before placing the chat on-hold (in seconds), provided the agent has sent the last message", "Livechat_message_character_limit": "Livechat message character limit", "Livechat_monitors": "Livechat monitors", "Livechat_Monitors": "Monitors", "Livechat_offline": "Omnichannel offline", "Livechat_offline_message_sent": "Livechat offline message sent", "Livechat_OfflineMessageToChannel_enabled": "Send Livechat offline messages to a channel", + "Livechat_onHold_Chat": "Place the abandoned chat On-Hold", "Livechat_online": "Omnichannel on-line", "Livechat_Queue": "Omnichannel Queue", "Livechat_registration_form": "Registration Form", @@ -4298,4 +4305,4 @@ "Your_temporary_password_is_password": "Your temporary password is [password].", "Your_TOTP_has_been_reset": "Your Two Factor TOTP has been reset.", "Your_workspace_is_ready": "Your workspace is ready to use 🎉" -} \ No newline at end of file +} From 3a73b5cdcb58f71f3bff7b2d917ecd2ab89e084b Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Thu, 25 Feb 2021 00:54:08 +0530 Subject: [PATCH 02/30] Add On-Hold Section within Sidebar --- client/sidebar/hooks/useRoomList.ts | 10 ++++++++++ definition/ISubscription.ts | 2 ++ packages/rocketchat-i18n/i18n/en.i18n.json | 1 + 3 files changed, 13 insertions(+) diff --git a/client/sidebar/hooks/useRoomList.ts b/client/sidebar/hooks/useRoomList.ts index 96087832288c7..6e5e73a60f0bb 100644 --- a/client/sidebar/hooks/useRoomList.ts +++ b/client/sidebar/hooks/useRoomList.ts @@ -33,6 +33,8 @@ export const useRoomList = (): Array => { const direct = new Set(); const discussion = new Set(); const conversation = new Set(); + const omnichannelOnHold = new Set(); + console.log(rooms); rooms.forEach((room) => { if (sidebarShowUnread && (room.alert || room.unread) && !room.hideUnreadStatus) { @@ -55,8 +57,13 @@ export const useRoomList = (): Array => { _private.add(room); } + if (room.t === 'l' && room.isChatOnHold) { + return showOmnichannel && omnichannelOnHold.add(room); + } + if (room.t === 'l') { return showOmnichannel && omnichannel.add(room); + // return showOmnichannel && !room.isChatOnHold && omnichannel.add(room); } if (room.t === 'd') { @@ -66,11 +73,14 @@ export const useRoomList = (): Array => { conversation.add(room); }); + console.log('----omnichannelOnHold', omnichannelOnHold); + const groups = new Map(); showOmnichannel && inquiries.enabled && groups.set('Omnichannel', []); showOmnichannel && !inquiries.enabled && groups.set('Omnichannel', omnichannel); showOmnichannel && inquiries.enabled && inquiries.queue.length && groups.set('Incoming_Livechats', inquiries.queue); showOmnichannel && inquiries.enabled && omnichannel.size && groups.set('Open_Livechats', omnichannel); + showOmnichannel && omnichannelOnHold.size && groups.set('On_Hold_Chats', omnichannelOnHold); sidebarShowUnread && unread.size && groups.set('Unread', unread); favoritesEnabled && favorite.size && groups.set('Favorites', favorite); showDiscussion && discussion.size && groups.set('Discussions', discussion); diff --git a/definition/ISubscription.ts b/definition/ISubscription.ts index 268475735fae9..153e5209abd27 100644 --- a/definition/ISubscription.ts +++ b/definition/ISubscription.ts @@ -31,6 +31,8 @@ export interface ISubscription extends IRocketChatRecord { prid?: RoomID; roles?: string[]; + + isChatOnHold?: boolean; } export interface ISubscriptionDirectMessage extends Omit { diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 4b36e438fe8c1..e46d3d708c688 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2938,6 +2938,7 @@ "Omnichannel_External_Frame_Encryption_JWK_Description": "If provided it will encrypt the user's token with the provided key and the external system will need to decrypt the data to access the token", "Omnichannel_External_Frame_URL": "External frame URL", "On": "On", + "On_Hold_Chats": "On Hold", "online": "online", "Online": "Online", "Only_authorized_users_can_write_new_messages": "Only authorized users can write new messages", From f1be061aa80e5de8f02ac20fda3cdc218b8e5d93 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Sat, 27 Feb 2021 16:30:31 +0530 Subject: [PATCH 03/30] On hold room UI --- .../client/views/app/livechatOnHold.html | 14 +++++++ .../client/views/app/livechatOnHold.js | 39 +++++++++++++++++++ .../client/messageBox/messageBox.html | 6 ++- .../client/messageBox/messageBox.js | 15 +++++++ packages/rocketchat-i18n/i18n/en.i18n.json | 1 + 5 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 app/livechat/client/views/app/livechatOnHold.html create mode 100644 app/livechat/client/views/app/livechatOnHold.js diff --git a/app/livechat/client/views/app/livechatOnHold.html b/app/livechat/client/views/app/livechatOnHold.html new file mode 100644 index 0000000000000..78316aeaf2918 --- /dev/null +++ b/app/livechat/client/views/app/livechatOnHold.html @@ -0,0 +1,14 @@ + diff --git a/app/livechat/client/views/app/livechatOnHold.js b/app/livechat/client/views/app/livechatOnHold.js new file mode 100644 index 0000000000000..05dee9085530c --- /dev/null +++ b/app/livechat/client/views/app/livechatOnHold.js @@ -0,0 +1,39 @@ +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; + +import { ChatRoom } from '../../../../models'; +import './livechatOnHold.html'; + +Template.livechatOnHold.helpers({ + roomOpen() { + const room = Template.instance().room.get(); + console.log('---roomOpen', room.open); + return room && room.open === true; + }, + + isPreparing() { + return Template.instance().preparing.get(); + }, +}); + +Template.livechatOnHold.events({ + async 'click .js-take-it'(event, instance) { + event.preventDefault(); + event.stopPropagation(); + + console.log('---button clicked', instance); + }, +}); + +Template.livechatOnHold.onCreated(function() { + this.rid = Template.currentData().rid; + this.room = new ReactiveVar(); + + this.autorun(() => { + this.room.set(ChatRoom.findOne({ _id: Template.currentData().rid })); + }); +}); + +Template.livechatOnHold.onDestroyed(function() { + +}); diff --git a/app/ui-message/client/messageBox/messageBox.html b/app/ui-message/client/messageBox/messageBox.html index 12def35badffa..09420c71a7c8b 100644 --- a/app/ui-message/client/messageBox/messageBox.html +++ b/app/ui-message/client/messageBox/messageBox.html @@ -84,7 +84,11 @@ {{#if isBlockedOrBlocker}} {{_ "room_is_blocked"}} {{else}} - {{> messageBoxReadOnly rid=rid isSubscribed=isSubscribed}} + {{#if isChatOnHold}} + {{> livechatOnHold rid=rid isSubscribed=isSubscribed}} + {{else}} + {{> messageBoxReadOnly rid=rid isSubscribed=isSubscribed}} + {{/if}} {{/if}} {{/if}} diff --git a/app/ui-message/client/messageBox/messageBox.js b/app/ui-message/client/messageBox/messageBox.js index 46856eeecea10..d44fa4bb76de7 100644 --- a/app/ui-message/client/messageBox/messageBox.js +++ b/app/ui-message/client/messageBox/messageBox.js @@ -37,6 +37,7 @@ import './messageBoxAudioMessage'; import './messageBoxNotSubscribed'; import './messageBox.html'; import './messageBoxReadOnly'; +import '../../../livechat/client/views/app/livechatOnHold'; Template.messageBox.onCreated(function() { this.state = new ReactiveDict(); @@ -209,11 +210,17 @@ Template.messageBox.helpers({ } const isBlockedOrBlocker = Template.instance().state.get('isBlockedOrBlocker'); + console.log('---isBlockedOrBlocker', isBlockedOrBlocker); if (isBlockedOrBlocker) { return false; } + console.log('---isWritable', subscription.isChatOnHold); + if (subscription.isChatOnHold) { + return false; + } + const isReadOnly = roomTypes.readOnly(rid, Users.findOne({ _id: Meteor.userId() }, { fields: { username: 1 } })); const isArchived = roomTypes.archived(rid) || (subscription && subscription.t === 'd' && subscription.archived); @@ -256,6 +263,14 @@ Template.messageBox.helpers({ isBlockedOrBlocker() { return Template.instance().state.get('isBlockedOrBlocker'); }, + isChatOnHold() { + const { rid, subscription } = Template.currentData(); + console.log('----isChatOnHold', rid, subscription.isChatOnHold); + if (!rid) { + return false; + } + return !!subscription.isChatOnHold; + }, isSubscribed() { const { subscription } = Template.currentData(); return !!subscription; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index e46d3d708c688..96cbbd26f2c25 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -722,6 +722,7 @@ "Chat_closed_successfully": "Chat closed successfully", "Chat_History": "Chat History", "Chat_Now": "Chat Now", + "chat_on_hold_due_to_inactivity": "This chat is on-hold due to inactivity", "Chat_queued": "Chat Queued", "Chat_removed": "Chat Removed", "Chat_start": "Chat Start", From 97b8bc467aa78ff0c450c1804227c8d7cf315b1a Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Sun, 28 Feb 2021 12:27:08 +0530 Subject: [PATCH 04/30] OnHold - Automatic --- app/models/server/models/Subscriptions.js | 16 ++++++++++++ .../setPredictedVisitorAbandonmentTime.js | 2 +- .../livechat-enterprise/server/lib/Helper.js | 4 +-- .../server/lib/VisitorInactivityMonitor.js | 26 ++++++++++++++++--- ee/app/livechat-enterprise/server/startup.js | 4 +-- ee/app/models/server/models/LivechatRooms.js | 14 ++++++++++ server/modules/watchers/publishFields.ts | 1 + server/modules/watchers/watchers.module.ts | 1 + 8 files changed, 60 insertions(+), 8 deletions(-) diff --git a/app/models/server/models/Subscriptions.js b/app/models/server/models/Subscriptions.js index 049e6a05cda9d..c21b63487285b 100644 --- a/app/models/server/models/Subscriptions.js +++ b/app/models/server/models/Subscriptions.js @@ -1330,6 +1330,22 @@ export class Subscriptions extends Base { return this.update(query, update, { multi: true }); } + + setIsChatOnHold(roomId) { + return this.update( + { rid: roomId }, + { $set: { isChatOnHold: true } }, + { multi: true }, + ); + } + + unsetIsChatOnHold(roomId) { + return this.update( + { rid: roomId }, + { $unset: { isChatOnHold: 1 } }, + { multi: true }, + ); + } } export default new Subscriptions('subscription', true); diff --git a/ee/app/livechat-enterprise/server/hooks/setPredictedVisitorAbandonmentTime.js b/ee/app/livechat-enterprise/server/hooks/setPredictedVisitorAbandonmentTime.js index a5b6f9cde7bb8..14f20fe556404 100644 --- a/ee/app/livechat-enterprise/server/hooks/setPredictedVisitorAbandonmentTime.js +++ b/ee/app/livechat-enterprise/server/hooks/setPredictedVisitorAbandonmentTime.js @@ -3,7 +3,7 @@ import { settings } from '../../../../../app/settings/server'; import { setPredictedVisitorAbandonmentTime } from '../lib/Helper'; callbacks.add('afterSaveMessage', function(message, room) { - if (!settings.get('Livechat_auto_close_abandoned_rooms') || settings.get('Livechat_visitor_inactivity_timeout') <= 0) { + if (!settings.get('Livechat_abandoned_rooms_action') || settings.get('Livechat_abandoned_rooms_action') === 'none' || settings.get('Livechat_visitor_inactivity_timeout') <= 0) { return message; } // skips this callback if the message was edited diff --git a/ee/app/livechat-enterprise/server/lib/Helper.js b/ee/app/livechat-enterprise/server/lib/Helper.js index b3f8f675f32da..c1768a40b537c 100644 --- a/ee/app/livechat-enterprise/server/lib/Helper.js +++ b/ee/app/livechat-enterprise/server/lib/Helper.js @@ -124,7 +124,7 @@ export const processWaitingQueue = async (department) => { }; export const setPredictedVisitorAbandonmentTime = (room) => { - if (!room.v || !room.v.lastMessageTs || !settings.get('Livechat_auto_close_abandoned_rooms')) { + if (!room.v || !room.v.lastMessageTs || !settings.get('Livechat_abandoned_rooms_action') || settings.get('Livechat_abandoned_rooms_action') === 'none') { return; } @@ -144,7 +144,7 @@ export const setPredictedVisitorAbandonmentTime = (room) => { }; export const updatePredictedVisitorAbandonment = () => { - if (settings.get('Livechat_auto_close_abandoned_rooms')) { + if (settings.get('Livechat_abandoned_rooms_action') && (settings.get('Livechat_abandoned_rooms_action') === 'none')) { LivechatRooms.findLivechat({ open: true }).forEach((room) => setPredictedVisitorAbandonmentTime(room)); } else { LivechatRooms.unsetPredictedVisitorAbandonment(); diff --git a/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js b/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js index 11879836e1829..bf8377c22096b 100644 --- a/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js +++ b/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js @@ -2,7 +2,7 @@ import { SyncedCron } from 'meteor/littledata:synced-cron'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { settings } from '../../../../../app/settings/server'; -import { LivechatRooms, LivechatDepartment, Users } from '../../../../../app/models/server'; +import { LivechatRooms, LivechatDepartment, Users, Subscriptions } from '../../../../../app/models/server'; import { Livechat } from '../../../../../app/livechat/server/lib/Livechat'; export class VisitorInactivityMonitor { @@ -77,11 +77,31 @@ export class VisitorInactivityMonitor { }); } + placeRoomOnHold(room) { + console.log('-------VisitorInactivityMonitor.onHoldRoom', room); + let resp = LivechatRooms.setIsChatOnHold(room._id); + console.log('----placeRoomOnHold rooms db response', resp); + resp = Subscriptions.setIsChatOnHold(room._id); + console.log('----placeRoomOnHold subscription db response', resp); + } + handleAbandonedRooms() { - if (!settings.get('Livechat_auto_close_abandoned_rooms')) { + const action = settings.get('Livechat_abandoned_rooms_action'); + if (!action || action === 'none') { return; } - LivechatRooms.findAbandonedOpenRooms(new Date()).forEach((room) => this.closeRooms(room)); + LivechatRooms.findAbandonedOpenRooms(new Date()).forEach((room) => { + switch (action) { + case 'close': { + this.closeRooms(room); + break; + } + case 'on-hold': { + this.placeRoomOnHold(room); + break; + } + } + }); this._initializeMessageCache(); } } diff --git a/ee/app/livechat-enterprise/server/startup.js b/ee/app/livechat-enterprise/server/startup.js index 52e9640fff91b..71724f59268e3 100644 --- a/ee/app/livechat-enterprise/server/startup.js +++ b/ee/app/livechat-enterprise/server/startup.js @@ -16,9 +16,9 @@ const businessHours = { }; Meteor.startup(async function() { - settings.onload('Livechat_auto_close_abandoned_rooms', function(_, value) { + settings.onload('Livechat_abandoned_rooms_action', function(_, value) { updatePredictedVisitorAbandonment(); - if (!value) { + if (!value || value === 'none') { return visitorActivityMonitor.stop(); } visitorActivityMonitor.start(); diff --git a/ee/app/models/server/models/LivechatRooms.js b/ee/app/models/server/models/LivechatRooms.js index 2dc41df46cbd3..e1adb0b4c5c47 100644 --- a/ee/app/models/server/models/LivechatRooms.js +++ b/ee/app/models/server/models/LivechatRooms.js @@ -46,6 +46,20 @@ LivechatRooms.prototype.findAbandonedOpenRooms = function(date) { }); }; +LivechatRooms.prototype.setIsChatOnHold = function(roomId) { + return this.update( + { _id: roomId }, + { $set: { isChatOnHold: true } }, + ); +}; + +LivechatRooms.prototype.unsetIsChatOnHold = function(roomId) { + return this.update( + { _id: roomId }, + { $unset: { isChatOnHold: 1 } }, + ); +}; + LivechatRooms.prototype.unsetPredictedVisitorAbandonment = function() { return this.update({ open: true, diff --git a/server/modules/watchers/publishFields.ts b/server/modules/watchers/publishFields.ts index 63f1cd8183858..6024cce0488c9 100644 --- a/server/modules/watchers/publishFields.ts +++ b/server/modules/watchers/publishFields.ts @@ -37,6 +37,7 @@ export const subscriptionFields = { tunreadGroup: 1, tunreadUser: 1, v: 1, + isChatOnHold: 1, }; export const roomFields = { diff --git a/server/modules/watchers/watchers.module.ts b/server/modules/watchers/watchers.module.ts index a6f856a4413d0..5430b4be98b06 100644 --- a/server/modules/watchers/watchers.module.ts +++ b/server/modules/watchers/watchers.module.ts @@ -128,6 +128,7 @@ export function initWatchers(models: IModelsParam, broadcast: BroadcastCallback, case 'updated': { // Override data cuz we do not publish all fields const subscription = await Subscriptions.findOneById(id, { projection: subscriptionFields }); + console.log('---subscription found', subscription); if (!subscription) { return; } From 06033c5d684ccc4f5df69feefeb1b4594ad32551 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Mon, 1 Mar 2021 15:28:08 +0530 Subject: [PATCH 05/30] Add Manual on-hold button UI --- app/livechat/client/views/app/tabbar/visitorInfo.html | 4 ++++ app/livechat/client/views/app/tabbar/visitorInfo.js | 6 ++++++ server/modules/watchers/publishFields.ts | 1 + 3 files changed, 11 insertions(+) diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.html b/app/livechat/client/views/app/tabbar/visitorInfo.html index c46ed0b1ec51d..ed58633fa541e 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.html +++ b/app/livechat/client/views/app/tabbar/visitorInfo.html @@ -74,6 +74,10 @@

{{_ "Transcript_Request"}}

{{/if}} + {{#if canPlaceChatOnHold}} + + {{/if}} + {{#if roomOpen}} {{#if canCloseRoom}} diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index f061207a35af6..9e7265a9f4f31 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -206,6 +206,12 @@ Template.visitorInfo.helpers({ return !room.email && hasPermission('send-omnichannel-chat-transcript'); }, + canPlaceChatOnHold() { + const room = Template.instance().room.get(); + console.log('---canPlaceChatOnHold', room); + return !!room.canPlaceChatOnHold; + }, + roomClosedDateTime() { const { closedAt } = this; return DateFormat.formatDateAndTime(closedAt); diff --git a/server/modules/watchers/publishFields.ts b/server/modules/watchers/publishFields.ts index 6024cce0488c9..4fc319bb38989 100644 --- a/server/modules/watchers/publishFields.ts +++ b/server/modules/watchers/publishFields.ts @@ -65,6 +65,7 @@ export const roomFields = { prid: 1, avatarETag: 1, usersCount: 1, + canPlaceChatOnHold: 1, // @TODO create an API to register this fields based on room type livechatData: 1, From 90f4329a7da13533577a208e3c636038a5a80077 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Wed, 3 Mar 2021 11:40:15 +0530 Subject: [PATCH 06/30] OnHold - manual - Start manual On-Hold timer after agent reply - Stop manual On-Hold timer on visitor message - Show manual On-Hold Option when timer expires --- definition/IRoom.ts | 3 + .../server/hooks/scheduleManualOnHold.ts | 68 +++++++++++++++++++ ee/app/livechat-enterprise/server/index.js | 1 + .../server/lib/AutoTransferChatScheduler.ts | 3 + .../server/lib/OnHoldScheduler.ts | 67 ++++++++++++++++++ .../server/lib/VisitorInactivityMonitor.js | 1 + ee/app/livechat-enterprise/server/settings.js | 14 ++-- ee/app/models/server/models/LivechatRooms.js | 15 ++++ 8 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts create mode 100644 ee/app/livechat-enterprise/server/lib/OnHoldScheduler.ts diff --git a/definition/IRoom.ts b/definition/IRoom.ts index ae83e23360bdc..e4c0a60aac795 100644 --- a/definition/IRoom.ts +++ b/definition/IRoom.ts @@ -38,6 +38,9 @@ export interface IRoom extends IRocketChatRecord { balance: number; }[]; }; + + isChatOnHold?: boolean; + canPlaceChatOnHold?: boolean; } export interface IDirectMessageRoom extends Omit { diff --git a/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts b/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts new file mode 100644 index 0000000000000..24c66d93c0d78 --- /dev/null +++ b/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts @@ -0,0 +1,68 @@ +import { callbacks } from '../../../../../app/callbacks/server'; +import { settings } from '../../../../../app/settings/server'; +import { OnHoldChatScheduler } from '../lib/OnHoldScheduler'; + +let manualOnHoldTimeout = -1; +let manualOnHoldEnabled = false; + +const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promise => { + const { _id: rid } = room; + if (!rid) { + return message; + } + + if (!manualOnHoldEnabled || manualOnHoldTimeout < 0) { + return message; + } + // skips this callback if the message was edited + if (message.editedAt) { + return message; + } + // message valid only if it is a livechat room + if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.v && room.v.token)) { + return message; + } + // if the message has a type means it is a special message (like the closing comment), so skips + if (message.t) { + return message; + } + + switch (!!message.token) { + case true: { + // message sent by visitor + // TODO: cancel any monitoring jobs running + console.log('---cancelling all job', rid); + await OnHoldChatScheduler.unscheduleRoom(rid); + break; + } + case false: { + // message sent by agent + // TODO: start a new monitoring job + console.log('---scheduling job', rid); + await OnHoldChatScheduler.scheduleRoom(rid, manualOnHoldTimeout === 0 ? 2 : manualOnHoldTimeout); + break; + } + } + + return message; +}; + + +settings.get('Livechat_allow_manual_on_hold', (_, value) => { + console.log('---setting allow manual onhold called', value); + manualOnHoldEnabled = value as boolean; + if (!manualOnHoldTimeout) { + console.log('--removeing callback'); + callbacks.remove('afterSaveMessage', 'livechat-manual-on-hold'); + return; + } + + console.log('--adding callback'); + callbacks.add('afterSaveMessage', handleAfterSaveMessage, callbacks.priority.HIGH, 'livechat-manual-on-hold'); +}); + + +settings.get('Livechat_manual_on_hold_timeout', (_, value) => { + console.log('---setting Livechat_manual_on_hold_timeout called', value); + manualOnHoldTimeout = value as number ? value as number : -1; +}); diff --git a/ee/app/livechat-enterprise/server/index.js b/ee/app/livechat-enterprise/server/index.js index 0f6006affbed4..84b03992d77ae 100644 --- a/ee/app/livechat-enterprise/server/index.js +++ b/ee/app/livechat-enterprise/server/index.js @@ -26,6 +26,7 @@ import './hooks/onLoadConfigApi'; import './hooks/onCloseLivechat'; import './hooks/onSaveVisitorInfo'; import './hooks/scheduleAutoTransfer'; +import './hooks/scheduleManualOnHold'; import './lib/routing/LoadBalancing'; import { onLicense } from '../../license/server'; import './business-hour'; diff --git a/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index f95557528d2f9..2d9c8c66ef430 100644 --- a/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -6,6 +6,7 @@ import { LivechatRooms, Users } from '../../../../../app/models/server'; import { Livechat } from '../../../../../app/livechat/server'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { forwardRoomToAgent } from '../../../../../app/livechat/server/lib/Helper'; +import { OnHoldChatScheduler } from './OnHoldScheduler'; const schedulerUser = Users.findOneById('rocket.cat'); const SCHEDULER_NAME = 'omnichannel_scheduler'; @@ -85,5 +86,7 @@ class AutoTransferChatSchedulerClass { export const AutoTransferChatScheduler = new AutoTransferChatSchedulerClass(); Meteor.startup(() => { + console.log('---AutoTransferChatScheduler called'); AutoTransferChatScheduler.init(); + OnHoldChatScheduler.init(); }); diff --git a/ee/app/livechat-enterprise/server/lib/OnHoldScheduler.ts b/ee/app/livechat-enterprise/server/lib/OnHoldScheduler.ts new file mode 100644 index 0000000000000..900801254fd9e --- /dev/null +++ b/ee/app/livechat-enterprise/server/lib/OnHoldScheduler.ts @@ -0,0 +1,67 @@ +import Agenda from 'agenda'; +import { MongoInternals } from 'meteor/mongo'; +import { Meteor } from 'meteor/meteor'; + +import { LivechatRooms } from '../../../../../app/models/server'; + +const SCHEDULER_NAME = 'omnichannel_manual_on_hold_scheduler'; + +class OnHoldSchedulerClass { + scheduler: Agenda; + + running: boolean; + + user: {}; + + public init(): void { + console.log('----staring OnHoldSchedulerClass'); + if (this.running) { + return; + } + + this.scheduler = new Agenda({ + mongo: (MongoInternals.defaultRemoteCollectionDriver().mongo as any).client.db(), + db: { collection: SCHEDULER_NAME }, + defaultConcurrency: 1, + }); + + this.scheduler.start(); + this.running = true; + } + + public async scheduleRoom(roomId: string, timeout: number): Promise { + await this.unscheduleRoom(roomId); + + const jobName = `${ SCHEDULER_NAME }-${ roomId }`; + const when = new Date(); + when.setSeconds(when.getSeconds() + timeout); + + this.scheduler.define(jobName, this.executeJob.bind(this)); + await this.scheduler.schedule(when, jobName, { roomId }); + } + + + public async unscheduleRoom(roomId: string): Promise { + const jobName = `${ SCHEDULER_NAME }-${ roomId }`; + + await (LivechatRooms as any).unsetCanPlaceOnHold(roomId); + + await this.scheduler.cancel({ name: jobName }); + } + + private async executeJob({ attrs: { data } }: any = {}): Promise { + console.log('---executeJob called', data); + + const { roomId } = data; + + const result = await (LivechatRooms as any).setCanPlaceOnHold(roomId); + console.log('---result', result); + } +} + +export const OnHoldChatScheduler = new OnHoldSchedulerClass(); + +Meteor.startup(() => { + console.log('---from meteor startup - OnHoldChatScheduler'); + // OnHoldChatScheduler.init(); +}); diff --git a/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js b/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js index bf8377c22096b..d02cb155299c7 100644 --- a/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js +++ b/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js @@ -83,6 +83,7 @@ export class VisitorInactivityMonitor { console.log('----placeRoomOnHold rooms db response', resp); resp = Subscriptions.setIsChatOnHold(room._id); console.log('----placeRoomOnHold subscription db response', resp); + LivechatRooms.unsetCanPlaceOnHold(room._id); } handleAbandonedRooms() { diff --git a/ee/app/livechat-enterprise/server/settings.js b/ee/app/livechat-enterprise/server/settings.js index 070216c942b29..eac9fbebefd1a 100644 --- a/ee/app/livechat-enterprise/server/settings.js +++ b/ee/app/livechat-enterprise/server/settings.js @@ -53,7 +53,7 @@ export const createSettings = () => { ], }); - settings.add('Livechat_abandoned_rooms_action', false, { + settings.add('Livechat_abandoned_rooms_action', 'none', { type: 'select', group: 'Omnichannel', section: 'Sessions', @@ -62,14 +62,15 @@ export const createSettings = () => { { key: 'close', i18nLabel: 'Livechat_close_chat' }, { key: 'on-hold', i18nLabel: 'Livechat_onHold_Chat' }, ], - value: 'none', enterprise: true, - invalidValue: false, + public: true, + invalidValue: 'none', modules: [ 'livechat-enterprise', ], }); + settings.add('Livechat_abandoned_rooms_closed_custom_message', '', { type: 'string', group: 'Omnichannel', @@ -127,12 +128,11 @@ export const createSettings = () => { ], }); - settings.add('Livechat_auto_close_on_hold_chats_timeout', true, { + settings.add('Livechat_auto_close_on_hold_chats_timeout', 60, { type: 'int', group: 'Omnichannel', section: 'Sessions', enterprise: true, - value: 60, invalidValue: 0, modules: [ 'livechat-enterprise', @@ -151,13 +151,13 @@ export const createSettings = () => { ], }); - settings.add('Livechat_manual_on_hold_timeout', true, { + settings.add('Livechat_manual_on_hold_timeout', 60, { type: 'int', group: 'Omnichannel', section: 'Sessions', enterprise: true, - value: 60, invalidValue: 0, + enableQuery: { _id: 'Livechat_allow_manual_on_hold', value: true }, modules: [ 'livechat-enterprise', ], diff --git a/ee/app/models/server/models/LivechatRooms.js b/ee/app/models/server/models/LivechatRooms.js index e1adb0b4c5c47..cd72127a9f78f 100644 --- a/ee/app/models/server/models/LivechatRooms.js +++ b/ee/app/models/server/models/LivechatRooms.js @@ -60,6 +60,21 @@ LivechatRooms.prototype.unsetIsChatOnHold = function(roomId) { ); }; +LivechatRooms.prototype.setCanPlaceOnHold = function(roomId) { + console.log('---within model', roomId); + return this.update( + { _id: roomId }, + { $set: { canPlaceChatOnHold: true } }, + ); +}; + +LivechatRooms.prototype.unsetCanPlaceOnHold = function(roomId) { + return this.update( + { _id: roomId }, + { $unset: { canPlaceChatOnHold: 1 } }, + ); +}; + LivechatRooms.prototype.unsetPredictedVisitorAbandonment = function() { return this.update({ open: true, From 2fbcfaf344ed2308e37c39df859ddd1a9f451755 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Wed, 3 Mar 2021 16:06:28 +0530 Subject: [PATCH 07/30] Handle manual On-Hold event --- .../client/views/app/tabbar/visitorInfo.html | 2 +- .../client/views/app/tabbar/visitorInfo.js | 19 ++++++++++ ee/app/livechat-enterprise/server/index.js | 1 + .../server/lib/LivechatEnterprise.js | 11 ++++++ .../server/methods/onHold.js | 36 +++++++++++++++++++ packages/rocketchat-i18n/i18n/en.i18n.json | 2 ++ 6 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 ee/app/livechat-enterprise/server/methods/onHold.js diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.html b/app/livechat/client/views/app/tabbar/visitorInfo.html index ed58633fa541e..01dafe832c99a 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.html +++ b/app/livechat/client/views/app/tabbar/visitorInfo.html @@ -75,7 +75,7 @@

{{_ "Transcript_Request"}}

{{/if}} {{#if canPlaceChatOnHold}} - + {{/if}} {{#if roomOpen}} diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 9e7265a9f4f31..09791815c3b4a 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -330,6 +330,25 @@ Template.visitorInfo.events({ instance.action.set('transcript'); }, + + 'click .on-hold'(event) { + event.preventDefault(); + console.log('--on-hold manual button clicked'); + + return Meteor.call('livechat:placeChatOnHold', this.rid, function(error/* , result*/) { + if (error) { + return handleError(error); + } + + modal.open({ + title: t('Chat_On_Hold'), + text: t('Chat_On_Hold_Successfully'), + type: 'success', + timer: 1500, + showConfirmButton: false, + }); + }); + }, }); Template.visitorInfo.onCreated(function() { diff --git a/ee/app/livechat-enterprise/server/index.js b/ee/app/livechat-enterprise/server/index.js index 84b03992d77ae..a8c1061243ae9 100644 --- a/ee/app/livechat-enterprise/server/index.js +++ b/ee/app/livechat-enterprise/server/index.js @@ -11,6 +11,7 @@ import './methods/saveUnit'; import './methods/savePriority'; import './methods/removePriority'; import './methods/removeBusinessHour'; +import './methods/onHold'; import LivechatUnit from '../../models/server/models/LivechatUnit'; import LivechatTag from '../../models/server/models/LivechatTag'; import LivechatUnitMonitors from '../../models/server/models/LivechatUnitMonitors'; diff --git a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index 77608e6c53066..755d64aa541fa 100644 --- a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -5,6 +5,7 @@ import { Users } from '../../../../../app/models'; import { LivechatInquiry, OmnichannelQueue } from '../../../../../app/models/server/raw'; import LivechatUnit from '../../../models/server/models/LivechatUnit'; import LivechatTag from '../../../models/server/models/LivechatTag'; +import { LivechatRooms, Subscriptions } from '../../../../../app/models/server'; import LivechatPriority from '../../../models/server/models/LivechatPriority'; import { addUserRoles, removeUserFromRoles } from '../../../../../app/authorization/server'; import { processWaitingQueue, removePriorityFromRooms, updateInquiryQueuePriority, updatePriorityInquiries, updateRoomPriorityHistory } from './Helper'; @@ -163,6 +164,16 @@ export const LivechatEnterprise = { updateInquiryQueuePriority(roomId, priority); updateRoomPriorityHistory(roomId, user, priority); }, + + placeRoomOnHold(roomId) { + check(roomId, String); + console.log('-------LivechatEnterprise.placeRoomOnHold', roomId); + let resp = LivechatRooms.setIsChatOnHold(roomId); + console.log('----placeRoomOnHold rooms db response', resp); + resp = Subscriptions.setIsChatOnHold(roomId); + console.log('----placeRoomOnHold subscription db response', resp); + LivechatRooms.unsetCanPlaceOnHold(roomId); + }, }; const RACE_TIMEOUT = 1000; diff --git a/ee/app/livechat-enterprise/server/methods/onHold.js b/ee/app/livechat-enterprise/server/methods/onHold.js new file mode 100644 index 0000000000000..7fa8703091c75 --- /dev/null +++ b/ee/app/livechat-enterprise/server/methods/onHold.js @@ -0,0 +1,36 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasPermission } from '../../../../../app/authorization'; +import { Subscriptions, LivechatRooms } from '../../../../../app/models/server'; +import { LivechatEnterprise } from '../lib/LivechatEnterprise'; + +Meteor.methods({ + 'livechat:placeChatOnHold'(roomId) { + console.log('--backend called'); + const userId = Meteor.userId(); + // TODO: create a new permission for on-hold + if (!userId || !hasPermission(userId, 'close-livechat-room')) { + throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:placeChatOnHold' }); + } + + + const room = LivechatRooms.findOneById(roomId); + console.log('--room found', room); + if (!room || room.t !== 'l') { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:placeChatOnHold' }); + } + + if (room.isChatOnHold) { + throw new Meteor.Error('room-closed', 'Room is already On-Hold', { method: 'livechat:placeChatOnHold' }); + } + + const user = Meteor.user(); + + const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { _id: 1 }); + if (!subscription && !hasPermission(userId, 'close-others-livechat-room')) { + throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:placeChatOnHold' }); + } + + return LivechatEnterprise.placeRoomOnHold(room._id); + }, +}); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 96cbbd26f2c25..ec10cd71355d4 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -723,6 +723,8 @@ "Chat_History": "Chat History", "Chat_Now": "Chat Now", "chat_on_hold_due_to_inactivity": "This chat is on-hold due to inactivity", + "Chat_On_Hold": "Chat On-Hold", + "Chat_On_Hold_Successfully": "This chat was successfully placed On-Hold", "Chat_queued": "Chat Queued", "Chat_removed": "Chat Removed", "Chat_start": "Chat Start", From 606d6e9ec0cd459448fe5fc36124a72a32f06749 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Wed, 3 Mar 2021 16:56:08 +0530 Subject: [PATCH 08/30] Add permissions to manual on-hold feature --- app/authorization/server/startup.js | 2 ++ ee/app/livechat-enterprise/server/methods/onHold.js | 7 +++---- ee/app/livechat-enterprise/server/permissions.js | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js index 7d5efbdca1679..68e35e00ea435 100644 --- a/app/authorization/server/startup.js +++ b/app/authorization/server/startup.js @@ -96,6 +96,8 @@ Meteor.startup(function() { { _id: 'view-livechat-rooms', roles: ['livechat-manager', 'admin'] }, { _id: 'close-livechat-room', roles: ['livechat-agent', 'livechat-manager', 'admin'] }, { _id: 'close-others-livechat-room', roles: ['livechat-manager', 'admin'] }, + { _id: 'onHold-livechat-room', roles: ['livechat-agent', 'livechat-manager', 'admin'] }, + { _id: 'onHold-others-livechat-room', roles: ['livechat-manager', 'admin'] }, { _id: 'save-others-livechat-room-info', roles: ['livechat-manager'] }, { _id: 'remove-closed-livechat-rooms', roles: ['livechat-manager', 'admin'] }, { _id: 'view-livechat-analytics', roles: ['livechat-manager', 'admin'] }, diff --git a/ee/app/livechat-enterprise/server/methods/onHold.js b/ee/app/livechat-enterprise/server/methods/onHold.js index 7fa8703091c75..6e4fcf36de7a8 100644 --- a/ee/app/livechat-enterprise/server/methods/onHold.js +++ b/ee/app/livechat-enterprise/server/methods/onHold.js @@ -8,12 +8,11 @@ Meteor.methods({ 'livechat:placeChatOnHold'(roomId) { console.log('--backend called'); const userId = Meteor.userId(); - // TODO: create a new permission for on-hold - if (!userId || !hasPermission(userId, 'close-livechat-room')) { + + if (!userId || !hasPermission(userId, 'onHold-livechat-room')) { throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:placeChatOnHold' }); } - const room = LivechatRooms.findOneById(roomId); console.log('--room found', room); if (!room || room.t !== 'l') { @@ -27,7 +26,7 @@ Meteor.methods({ const user = Meteor.user(); const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { _id: 1 }); - if (!subscription && !hasPermission(userId, 'close-others-livechat-room')) { + if (!subscription && !hasPermission(userId, 'onHold-others-livechat-room')) { throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:placeChatOnHold' }); } diff --git a/ee/app/livechat-enterprise/server/permissions.js b/ee/app/livechat-enterprise/server/permissions.js index ba09adda5ecb4..ed90b60df2d05 100644 --- a/ee/app/livechat-enterprise/server/permissions.js +++ b/ee/app/livechat-enterprise/server/permissions.js @@ -21,6 +21,8 @@ export const createPermissions = () => { 'view-livechat-rooms', 'close-livechat-room', 'close-others-livechat-room', + 'onHold-livechat-room', + 'onHold-others-livechat-room', 'save-others-livechat-room-info', 'remove-closed-livechat-rooms', 'view-livechat-analytics', From dda8636233dfc2490ef86bd3174888bfcaa32561 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Fri, 5 Mar 2021 09:38:38 +0530 Subject: [PATCH 09/30] [New] Auto-Close On hold chats --- app/livechat/server/lib/Livechat.js | 1 + .../server/hooks/afterOnHold.ts | 37 +++++++++ .../server/hooks/scheduleManualOnHold.ts | 20 +++-- ee/app/livechat-enterprise/server/index.js | 2 + .../server/lib/AutoCloseOnHoldScheduler.ts | 76 +++++++++++++++++++ .../server/lib/LivechatEnterprise.js | 13 +++- .../server/lib/OnHoldScheduler.ts | 4 +- .../server/lib/VisitorInactivityMonitor.js | 7 ++ .../server/methods/onHold.js | 2 +- ee/app/livechat-enterprise/server/settings.js | 12 +++ packages/rocketchat-i18n/i18n/en.i18n.json | 1 + 11 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 ee/app/livechat-enterprise/server/hooks/afterOnHold.ts create mode 100644 ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index 31a7edcb361dd..cb08cf22d386d 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -372,6 +372,7 @@ export const Livechat = { sendMessage(user || visitor, message, room); + console.log('---close room servedBy', servedBy); if (servedBy) { Subscriptions.removeByRoomIdAndUserId(rid, servedBy._id); } diff --git a/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts b/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts new file mode 100644 index 0000000000000..cbe6d74e1970a --- /dev/null +++ b/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts @@ -0,0 +1,37 @@ +import { callbacks } from '../../../../../app/callbacks/server'; +import { settings } from '../../../../../app/settings/server'; +import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; + +const DEFAULT_CLOSED_MESSAGE = 'Chat is closed because of inactivity'; + +let autoCloseOnHoldChatTimeout = 0; +let customCloseMessage = DEFAULT_CLOSED_MESSAGE; + +const handleAfterOnHold = async (room: any = {}): Promise => { + const { _id: rid } = room; + if (!rid) { + return; + } + + if (!autoCloseOnHoldChatTimeout || autoCloseOnHoldChatTimeout <= 0) { + return; + } + + await AutoCloseOnHoldScheduler.scheduleRoom(room._id, autoCloseOnHoldChatTimeout, customCloseMessage); + console.log('-auto close job scheduled'); +}; + +settings.get('Livechat_auto_close_on_hold_chats_timeout', (_, value) => { + console.log('---setting Livechat_auto_close_on_hold_chats_timeout called', value); + autoCloseOnHoldChatTimeout = value as number; + if (!value || value <= 0) { + callbacks.remove('livechat:afterOnHold', 'livechat-auto-close-on-hold'); + } + console.log('--adding callback'); + callbacks.add('livechat:afterOnHold', handleAfterOnHold, callbacks.priority.HIGH, 'livechat-auto-close-on-hold'); +}); + +settings.get('Livechat_auto_close_on_hold_chats_custom_message', (_, value) => { + console.log('---setting Livechat_auto_close_on_hold_chats_custom_message called', value); + customCloseMessage = value as string || DEFAULT_CLOSED_MESSAGE; +}); diff --git a/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts b/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts index 24c66d93c0d78..648d9b7e246e0 100644 --- a/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts +++ b/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts @@ -1,6 +1,8 @@ import { callbacks } from '../../../../../app/callbacks/server'; import { settings } from '../../../../../app/settings/server'; +import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; import { OnHoldChatScheduler } from '../lib/OnHoldScheduler'; +import { LivechatRooms, Subscriptions } from '../../../../../app/models/server'; let manualOnHoldTimeout = -1; let manualOnHoldEnabled = false; @@ -11,9 +13,6 @@ const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promis return message; } - if (!manualOnHoldEnabled || manualOnHoldTimeout < 0) { - return message; - } // skips this callback if the message was edited if (message.editedAt) { return message; @@ -27,17 +26,28 @@ const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promis return message; } + // TODO: find a better place to add this + if (message.token) { + await AutoCloseOnHoldScheduler.unscheduleRoom(rid); + let resp = (LivechatRooms as any).unsetIsChatOnHold(rid); + console.log('----handleAfterSaveMessage rooms db response', resp); + resp = Subscriptions.unsetIsChatOnHold(rid); + console.log('----handleAfterSaveMessage subscription db response', resp); + } + + if (!manualOnHoldEnabled || manualOnHoldTimeout < 0) { + return message; + } + switch (!!message.token) { case true: { // message sent by visitor - // TODO: cancel any monitoring jobs running console.log('---cancelling all job', rid); await OnHoldChatScheduler.unscheduleRoom(rid); break; } case false: { // message sent by agent - // TODO: start a new monitoring job console.log('---scheduling job', rid); await OnHoldChatScheduler.scheduleRoom(rid, manualOnHoldTimeout === 0 ? 2 : manualOnHoldTimeout); break; diff --git a/ee/app/livechat-enterprise/server/index.js b/ee/app/livechat-enterprise/server/index.js index a8c1061243ae9..ab72978038304 100644 --- a/ee/app/livechat-enterprise/server/index.js +++ b/ee/app/livechat-enterprise/server/index.js @@ -28,7 +28,9 @@ import './hooks/onCloseLivechat'; import './hooks/onSaveVisitorInfo'; import './hooks/scheduleAutoTransfer'; import './hooks/scheduleManualOnHold'; +import './hooks/afterOnHold'; import './lib/routing/LoadBalancing'; +import './lib/AutoCloseOnHoldScheduler'; import { onLicense } from '../../license/server'; import './business-hour'; diff --git a/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts b/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts new file mode 100644 index 0000000000000..6aa3ffe83e8bc --- /dev/null +++ b/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts @@ -0,0 +1,76 @@ +import Agenda from 'agenda'; +import { MongoInternals } from 'meteor/mongo'; +import { Meteor } from 'meteor/meteor'; +import moment from 'moment'; + +import { Livechat } from '../../../../../app/livechat/server'; +import { LivechatRooms, Users } from '../../../../../app/models/server'; + +const schedulerUser = Users.findOneById('rocket.cat'); +const SCHEDULER_NAME = 'omnichannel_auto_close_on_hold_scheduler'; + +class AutoCloseOnHoldSchedulerClass { + scheduler: Agenda; + + running: boolean; + + public init(): void { + console.log('----staring AutoCloseOnHoldSchedulerClass'); + if (this.running) { + return; + } + + this.scheduler = new Agenda({ + mongo: (MongoInternals.defaultRemoteCollectionDriver().mongo as any).client.db(), + db: { collection: SCHEDULER_NAME }, + defaultConcurrency: 1, + }); + + this.scheduler.start(); + this.running = true; + } + + public async scheduleRoom(roomId: string, timeout: number, comment: string): Promise { + await this.unscheduleRoom(roomId); + + const jobName = `${ SCHEDULER_NAME }-${ roomId }`; + // TODO: remove this + const when = moment(new Date()).add(timeout, 's').toDate(); + // const when = moment(new Date()).add(timeout, 'm').toDate(); + + this.scheduler.define(jobName, this.executeJob.bind(this)); + await this.scheduler.schedule(when, jobName, { roomId, comment }); + } + + + public async unscheduleRoom(roomId: string): Promise { + const jobName = `${ SCHEDULER_NAME }-${ roomId }`; + await this.scheduler.cancel({ name: jobName }); + } + + private async executeJob({ attrs: { data } }: any = {}): Promise { + console.log('---executeJob called', data); + + const { roomId, comment } = data; + + const payload = { + user: schedulerUser, + room: await LivechatRooms.findOneById(roomId), + comment, + options: {}, + visitor: undefined, + }; + + console.log('--payload', payload); + + const result = Livechat.closeRoom(payload); + console.log('---result', result); + } +} + +export const AutoCloseOnHoldScheduler = new AutoCloseOnHoldSchedulerClass(); + +Meteor.startup(() => { + console.log('---from meteor startup - AutoCloseOnHoldScheduler'); + AutoCloseOnHoldScheduler.init(); +}); diff --git a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index 755d64aa541fa..3a28050545ada 100644 --- a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -11,6 +11,7 @@ import { addUserRoles, removeUserFromRoles } from '../../../../../app/authorizat import { processWaitingQueue, removePriorityFromRooms, updateInquiryQueuePriority, updatePriorityInquiries, updateRoomPriorityHistory } from './Helper'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { settings } from '../../../../../app/settings/server'; +import { callbacks } from '../../../../../app/callbacks'; export const LivechatEnterprise = { addMonitor(username) { @@ -165,14 +166,22 @@ export const LivechatEnterprise = { updateRoomPriorityHistory(roomId, user, priority); }, - placeRoomOnHold(roomId) { - check(roomId, String); + placeRoomOnHold(room) { + const { _id: roomId } = room; + if (!roomId) { + return; + } console.log('-------LivechatEnterprise.placeRoomOnHold', roomId); let resp = LivechatRooms.setIsChatOnHold(roomId); console.log('----placeRoomOnHold rooms db response', resp); resp = Subscriptions.setIsChatOnHold(roomId); console.log('----placeRoomOnHold subscription db response', resp); LivechatRooms.unsetCanPlaceOnHold(roomId); + + Meteor.defer(() => { + console.log('---livechat:afterOnHold callback set'); + callbacks.run('livechat:afterOnHold', room); + }); }, }; diff --git a/ee/app/livechat-enterprise/server/lib/OnHoldScheduler.ts b/ee/app/livechat-enterprise/server/lib/OnHoldScheduler.ts index 900801254fd9e..dbcba28bc1f01 100644 --- a/ee/app/livechat-enterprise/server/lib/OnHoldScheduler.ts +++ b/ee/app/livechat-enterprise/server/lib/OnHoldScheduler.ts @@ -1,6 +1,7 @@ import Agenda from 'agenda'; import { MongoInternals } from 'meteor/mongo'; import { Meteor } from 'meteor/meteor'; +import moment from 'moment'; import { LivechatRooms } from '../../../../../app/models/server'; @@ -33,8 +34,7 @@ class OnHoldSchedulerClass { await this.unscheduleRoom(roomId); const jobName = `${ SCHEDULER_NAME }-${ roomId }`; - const when = new Date(); - when.setSeconds(when.getSeconds() + timeout); + const when = moment(new Date()).add(timeout, 's').toDate(); this.scheduler.define(jobName, this.executeJob.bind(this)); await this.scheduler.schedule(when, jobName, { roomId }); diff --git a/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js b/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js index d02cb155299c7..a94898e2f1700 100644 --- a/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js +++ b/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js @@ -1,6 +1,8 @@ +import { Meteor } from 'meteor/meteor'; import { SyncedCron } from 'meteor/littledata:synced-cron'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { callbacks } from '../../../../../app/callbacks'; import { settings } from '../../../../../app/settings/server'; import { LivechatRooms, LivechatDepartment, Users, Subscriptions } from '../../../../../app/models/server'; import { Livechat } from '../../../../../app/livechat/server/lib/Livechat'; @@ -84,6 +86,11 @@ export class VisitorInactivityMonitor { resp = Subscriptions.setIsChatOnHold(room._id); console.log('----placeRoomOnHold subscription db response', resp); LivechatRooms.unsetCanPlaceOnHold(room._id); + + Meteor.defer(() => { + console.log('---livechat:afterOnHold callback set'); + callbacks.run('livechat:afterOnHold', room); + }); } handleAbandonedRooms() { diff --git a/ee/app/livechat-enterprise/server/methods/onHold.js b/ee/app/livechat-enterprise/server/methods/onHold.js index 6e4fcf36de7a8..27c26cdf49f7f 100644 --- a/ee/app/livechat-enterprise/server/methods/onHold.js +++ b/ee/app/livechat-enterprise/server/methods/onHold.js @@ -30,6 +30,6 @@ Meteor.methods({ throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:placeChatOnHold' }); } - return LivechatEnterprise.placeRoomOnHold(room._id); + return LivechatEnterprise.placeRoomOnHold(room); }, }); diff --git a/ee/app/livechat-enterprise/server/settings.js b/ee/app/livechat-enterprise/server/settings.js index eac9fbebefd1a..78f385f60cd4d 100644 --- a/ee/app/livechat-enterprise/server/settings.js +++ b/ee/app/livechat-enterprise/server/settings.js @@ -139,6 +139,18 @@ export const createSettings = () => { ], }); + settings.add('Livechat_auto_close_on_hold_chats_custom_message', '', { + type: 'string', + group: 'Omnichannel', + section: 'Sessions', + enableQuery: { _id: 'Livechat_auto_close_on_hold_chats_timeout', value: { $gte: 1 } }, + enterprise: true, + invalidValue: '', + modules: [ + 'livechat-enterprise', + ], + }); + settings.add('Livechat_allow_manual_on_hold', true, { type: 'boolean', group: 'Omnichannel', diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index ec10cd71355d4..7d734d082b71a 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2410,6 +2410,7 @@ "Livechat_allow_manual_on_hold": "Allow agents to manually place chat On Hold", "Livechat_AllowedDomainsList": "Livechat Allowed Domains", "Livechat_Appearance": "Livechat Appearance", + "Livechat_auto_close_on_hold_chats_custom_message": "Custom Message to be sent when a room in On-Hold queue gets automatically closed by the system", "Livechat_auto_close_on_hold_chats_timeout": "How long the chat will remain in the On Hold queue until it's automatically closed by the system (in minutes) ?", "Livechat_auto_transfer_chat_timeout": "Timeout (in seconds) for automatic transfer of unanswered chats to another agent", "Livechat_auto_transfer_chat_timeout_description": "This event takes place only when the chat has just started. After the first transfering for inactivity, the room is no longer monitored.", From 28f6b50185ce9845b3ee2d11bea16e8e78fe9d30 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Tue, 16 Mar 2021 15:14:31 +0530 Subject: [PATCH 10/30] Routing chat when an on-hold get is resumed --- .../client/views/app/livechatOnHold.js | 6 +- .../client/views/app/tabbar/visitorInfo.js | 12 +++- app/livechat/server/lib/QueueManager.js | 2 +- app/livechat/server/lib/RoutingManager.js | 3 + app/models/server/raw/Users.js | 2 +- client/sidebar/hooks/useRoomList.ts | 7 ++- .../hooks/checkAgentBeforeTakeInquiry.js | 3 + .../server/hooks/scheduleManualOnHold.ts | 13 ++-- ee/app/livechat-enterprise/server/index.js | 1 + .../server/lib/AutoCloseOnHoldScheduler.ts | 2 - .../server/lib/VisitorInactivityMonitor.js | 18 ++---- .../server/methods/resumeOnHold.ts | 62 +++++++++++++++++++ ee/app/models/server/models/LivechatRooms.js | 8 +++ packages/rocketchat-i18n/i18n/en.i18n.json | 5 +- 14 files changed, 111 insertions(+), 33 deletions(-) create mode 100644 ee/app/livechat-enterprise/server/methods/resumeOnHold.ts diff --git a/app/livechat/client/views/app/livechatOnHold.js b/app/livechat/client/views/app/livechatOnHold.js index 05dee9085530c..57e1c9b389fea 100644 --- a/app/livechat/client/views/app/livechatOnHold.js +++ b/app/livechat/client/views/app/livechatOnHold.js @@ -3,6 +3,7 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { ChatRoom } from '../../../../models'; import './livechatOnHold.html'; +import { call } from '../../../../ui-utils/client'; Template.livechatOnHold.helpers({ roomOpen() { @@ -21,7 +22,10 @@ Template.livechatOnHold.events({ event.preventDefault(); event.stopPropagation(); - console.log('---button clicked', instance); + const room = instance.room.get(); + + console.log('---button clicked', room); + await call('livechat:resumeOnHold', room._id, { clientAction: true }); }, }); diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 09791815c3b4a..34004830943cd 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -335,11 +335,17 @@ Template.visitorInfo.events({ event.preventDefault(); console.log('--on-hold manual button clicked'); - return Meteor.call('livechat:placeChatOnHold', this.rid, function(error/* , result*/) { + modal.open({ + title: t('Would_you_like_to_place_chat_on_hold'), + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#3085d6', + cancelButtonColor: '#d33', + confirmButtonText: t('Yes'), + }, () => Meteor.call('livechat:placeChatOnHold', this.rid, function(error/* , result*/) { if (error) { return handleError(error); } - modal.open({ title: t('Chat_On_Hold'), text: t('Chat_On_Hold_Successfully'), @@ -347,7 +353,7 @@ Template.visitorInfo.events({ timer: 1500, showConfirmButton: false, }); - }); + })); }, }); diff --git a/app/livechat/server/lib/QueueManager.js b/app/livechat/server/lib/QueueManager.js index febd9cbf59ba5..37a5805f12d81 100644 --- a/app/livechat/server/lib/QueueManager.js +++ b/app/livechat/server/lib/QueueManager.js @@ -7,7 +7,7 @@ import { callbacks } from '../../../callbacks/server'; import { RoutingManager } from './RoutingManager'; -const queueInquiry = async (room, inquiry, defaultAgent) => { +export const queueInquiry = async (room, inquiry, defaultAgent) => { const inquiryAgent = RoutingManager.delegateAgent(defaultAgent, inquiry); await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent); inquiry = LivechatInquiry.findOneById(inquiry._id); diff --git a/app/livechat/server/lib/RoutingManager.js b/app/livechat/server/lib/RoutingManager.js index 49aec01b625a1..d640b56dd0895 100644 --- a/app/livechat/server/lib/RoutingManager.js +++ b/app/livechat/server/lib/RoutingManager.js @@ -49,6 +49,8 @@ export const RoutingManager = { agent = await this.getNextAgent(department); } + console.log('---delegateInquiry called'); + if (!agent) { return LivechatRooms.findOneById(rid); } @@ -121,6 +123,7 @@ export const RoutingManager = { rid: String, status: String, })); + console.log('---takeInquiry called', inquiry, agent); const { _id, rid } = inquiry; const room = LivechatRooms.findOneById(rid); diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js index c89ecd9068230..61ec941f6eb13 100644 --- a/app/models/server/raw/Users.js +++ b/app/models/server/raw/Users.js @@ -191,7 +191,7 @@ export class UsersRaw extends BaseRaw { const aggregate = [ { $match: { _id: userId, status: { $exists: true, $ne: 'offline' }, statusLivechat: 'available', roles: 'livechat-agent' } }, { $lookup: { from: 'rocketchat_subscription', localField: '_id', foreignField: 'u._id', as: 'subs' } }, - { $project: { agentId: '$_id', username: 1, lastAssignTime: 1, lastRoutingTime: 1, 'queueInfo.chats': { $size: { $filter: { input: '$subs', as: 'sub', cond: { $eq: ['$$sub.t', 'l'] } } } } } }, + { $project: { agentId: '$_id', username: 1, lastAssignTime: 1, lastRoutingTime: 1, 'queueInfo.chats': { $size: { $filter: { input: '$subs', as: 'sub', cond: { $and: [{ $eq: ['$$sub.t', 'l'] }, { $eq: ['$$sub.open', true] }, { $ne: ['$$sub.isChatOnHold', true] }] } } } } } }, { $sort: { 'queueInfo.chats': 1, lastAssignTime: 1, lastRoutingTime: 1, username: 1 } }, ]; diff --git a/client/sidebar/hooks/useRoomList.ts b/client/sidebar/hooks/useRoomList.ts index 6e5e73a60f0bb..0ac05b6c2824c 100644 --- a/client/sidebar/hooks/useRoomList.ts +++ b/client/sidebar/hooks/useRoomList.ts @@ -74,12 +74,13 @@ export const useRoomList = (): Array => { }); console.log('----omnichannelOnHold', omnichannelOnHold); + console.log('-----inquiries', inquiries); const groups = new Map(); - showOmnichannel && inquiries.enabled && groups.set('Omnichannel', []); - showOmnichannel && !inquiries.enabled && groups.set('Omnichannel', omnichannel); + showOmnichannel && (inquiries.enabled || omnichannelOnHold.size) && groups.set('Omnichannel', []); + showOmnichannel && !inquiries.enabled && !omnichannelOnHold.size && groups.set('Omnichannel', omnichannel); showOmnichannel && inquiries.enabled && inquiries.queue.length && groups.set('Incoming_Livechats', inquiries.queue); - showOmnichannel && inquiries.enabled && omnichannel.size && groups.set('Open_Livechats', omnichannel); + showOmnichannel && (inquiries.enabled || omnichannelOnHold.size) && omnichannel.size && groups.set('Open_Livechats', omnichannel); showOmnichannel && omnichannelOnHold.size && groups.set('On_Hold_Chats', omnichannelOnHold); sidebarShowUnread && unread.size && groups.set('Unread', unread); favoritesEnabled && favorite.size && groups.set('Favorites', favorite); diff --git a/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js b/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js index 3dcb33c08d1de..9db3f54b1334e 100644 --- a/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js +++ b/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js @@ -24,6 +24,7 @@ callbacks.add('livechat.checkAgentBeforeTakeInquiry', async (agent, inquiry) => const { agentId } = agent; const maxNumberSimultaneousChat = getMaxNumberSimultaneousChat({ agentId, departmentId }); + console.log('----maxNumberSimultaneousChat', maxNumberSimultaneousChat); if (maxNumberSimultaneousChat === 0) { return agent; } @@ -33,6 +34,8 @@ callbacks.add('livechat.checkAgentBeforeTakeInquiry', async (agent, inquiry) => return null; } + console.log('---max limit', maxNumberSimultaneousChat, '---curr', user.queueInfo); + const { queueInfo: { chats = 0 } = {} } = user; if (maxNumberSimultaneousChat <= chats) { callbacks.run('livechat.onMaxNumberSimultaneousChatsReached', inquiry); diff --git a/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts b/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts index 648d9b7e246e0..fd8125926a848 100644 --- a/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts +++ b/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts @@ -1,14 +1,15 @@ +import { Meteor } from 'meteor/meteor'; + import { callbacks } from '../../../../../app/callbacks/server'; import { settings } from '../../../../../app/settings/server'; import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; import { OnHoldChatScheduler } from '../lib/OnHoldScheduler'; -import { LivechatRooms, Subscriptions } from '../../../../../app/models/server'; let manualOnHoldTimeout = -1; let manualOnHoldEnabled = false; const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promise => { - const { _id: rid } = room; + const { _id: rid, isChatOnHold } = room; if (!rid) { return message; } @@ -27,12 +28,10 @@ const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promis } // TODO: find a better place to add this - if (message.token) { + if (message.token && isChatOnHold) { await AutoCloseOnHoldScheduler.unscheduleRoom(rid); - let resp = (LivechatRooms as any).unsetIsChatOnHold(rid); - console.log('----handleAfterSaveMessage rooms db response', resp); - resp = Subscriptions.unsetIsChatOnHold(rid); - console.log('----handleAfterSaveMessage subscription db response', resp); + await Meteor.call('livechat:resumeOnHold', room._id, { clientAction: false }); + return message; } if (!manualOnHoldEnabled || manualOnHoldTimeout < 0) { diff --git a/ee/app/livechat-enterprise/server/index.js b/ee/app/livechat-enterprise/server/index.js index ab72978038304..3a3a86b15b432 100644 --- a/ee/app/livechat-enterprise/server/index.js +++ b/ee/app/livechat-enterprise/server/index.js @@ -12,6 +12,7 @@ import './methods/savePriority'; import './methods/removePriority'; import './methods/removeBusinessHour'; import './methods/onHold'; +import './methods/resumeOnHold'; import LivechatUnit from '../../models/server/models/LivechatUnit'; import LivechatTag from '../../models/server/models/LivechatTag'; import LivechatUnitMonitors from '../../models/server/models/LivechatUnitMonitors'; diff --git a/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts b/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts index 6aa3ffe83e8bc..edbb0dc5b7b52 100644 --- a/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts +++ b/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts @@ -34,9 +34,7 @@ class AutoCloseOnHoldSchedulerClass { await this.unscheduleRoom(roomId); const jobName = `${ SCHEDULER_NAME }-${ roomId }`; - // TODO: remove this const when = moment(new Date()).add(timeout, 's').toDate(); - // const when = moment(new Date()).add(timeout, 'm').toDate(); this.scheduler.define(jobName, this.executeJob.bind(this)); await this.scheduler.schedule(when, jobName, { roomId, comment }); diff --git a/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js b/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js index a94898e2f1700..2651951706f69 100644 --- a/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js +++ b/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js @@ -1,11 +1,10 @@ -import { Meteor } from 'meteor/meteor'; import { SyncedCron } from 'meteor/littledata:synced-cron'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { callbacks } from '../../../../../app/callbacks'; import { settings } from '../../../../../app/settings/server'; -import { LivechatRooms, LivechatDepartment, Users, Subscriptions } from '../../../../../app/models/server'; +import { LivechatRooms, LivechatDepartment, Users } from '../../../../../app/models/server'; import { Livechat } from '../../../../../app/livechat/server/lib/Livechat'; +import { LivechatEnterprise } from './LivechatEnterprise'; export class VisitorInactivityMonitor { constructor() { @@ -80,17 +79,8 @@ export class VisitorInactivityMonitor { } placeRoomOnHold(room) { - console.log('-------VisitorInactivityMonitor.onHoldRoom', room); - let resp = LivechatRooms.setIsChatOnHold(room._id); - console.log('----placeRoomOnHold rooms db response', resp); - resp = Subscriptions.setIsChatOnHold(room._id); - console.log('----placeRoomOnHold subscription db response', resp); - LivechatRooms.unsetCanPlaceOnHold(room._id); - - Meteor.defer(() => { - console.log('---livechat:afterOnHold callback set'); - callbacks.run('livechat:afterOnHold', room); - }); + LivechatEnterprise.placeRoomOnHold(room); + LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id); } handleAbandonedRooms() { diff --git a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts new file mode 100644 index 0000000000000..2638866e52585 --- /dev/null +++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -0,0 +1,62 @@ +import { Meteor } from 'meteor/meteor'; + +import { Subscriptions, LivechatRooms, LivechatInquiry } from '../../../../../app/models/server'; +import { callbacks } from '../../../../../app/callbacks/server'; +import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; +import { Livechat } from '../../../../../app/livechat/server'; +import { dispatchInquiryPosition } from '../lib/Helper'; + + +Meteor.methods({ + async 'livechat:resumeOnHold'(roomId, options = { clientAction: false }) { + console.log('--livechat:resumeOnHold called'); + + const room = await LivechatRooms.findOneById(roomId); + console.log('--room found', room); + if (!room || room.t !== 'l') { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:resumeOnHold' }); + } + + if (!room.isChatOnHold) { + throw new Meteor.Error('room-closed', 'Room is not OnHold', { method: 'livechat:resumeOnHold' }); + } + + const { servedBy: { _id: agentId = null, username = null } = {} } = room; + let agent: any = { agentId, username }; + const inquiry = await LivechatInquiry.findOneByRoomId(roomId); + const { departmentId } = inquiry; + + try { + console.log('---b4 livechat.checkAgentBeforeTakeInquiry', agent, inquiry); + agent = await callbacks.run('livechat.checkAgentBeforeTakeInquiry', agent, inquiry); + } catch (e) { + console.log('--error', e); + if (options.clientAction) { + throw new Meteor.Error('error-max-number-simultaneous-chats-reached', 'Not allowed'); + } + agent = null; + } + + console.log('----resumeOnHold agent', agent); + + if (!agent) { + Livechat.returnRoomAsInquiry(room._id, departmentId); + + if (RoutingManager.getConfig().autoAssignAgent) { + LivechatInquiry.queueInquiry(inquiry._id); + + const [inq] = await LivechatInquiry.getCurrentSortedQueueAsync({ _id: inquiry._id, department: undefined }); + if (inq) { + dispatchInquiryPosition(inq); + } + } + } + + let resp = (LivechatRooms as any).unsetIsChatOnHold(roomId); + console.log('----resumeOnHold rooms db response', resp); + resp = Subscriptions.unsetIsChatOnHold(roomId); + console.log('----resumeOnHold subscription db response', resp); + (LivechatRooms as any).unsetCanPlaceOnHold(roomId); + (LivechatRooms as any).unsetPredictedVisitorAbandonmentByRoomId(roomId); + }, +}); diff --git a/ee/app/models/server/models/LivechatRooms.js b/ee/app/models/server/models/LivechatRooms.js index cd72127a9f78f..21fc732f4a649 100644 --- a/ee/app/models/server/models/LivechatRooms.js +++ b/ee/app/models/server/models/LivechatRooms.js @@ -86,6 +86,14 @@ LivechatRooms.prototype.unsetPredictedVisitorAbandonment = function() { }); }; +LivechatRooms.prototype.unsetPredictedVisitorAbandonmentByRoomId = function(roomId) { + return this.update({ + _id: roomId, + }, { + $unset: { 'omnichannel.predictedVisitorAbandonmentAt': 1 }, + }); +}; + LivechatRooms.prototype.unsetPriorityById = function(priorityId) { return this.update({ open: true, diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 7d734d082b71a..e122ad1e3f42a 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -727,6 +727,8 @@ "Chat_On_Hold_Successfully": "This chat was successfully placed On-Hold", "Chat_queued": "Chat Queued", "Chat_removed": "Chat Removed", + "Chat_resumed": "Chat Resumed", + "Chat_resumed_successfully": "Chat Resumed Successfully from On-Hold", "Chat_start": "Chat Start", "Chat_started": "Chat started", "Chat_taken": "Chat Taken", @@ -2411,7 +2413,7 @@ "Livechat_AllowedDomainsList": "Livechat Allowed Domains", "Livechat_Appearance": "Livechat Appearance", "Livechat_auto_close_on_hold_chats_custom_message": "Custom Message to be sent when a room in On-Hold queue gets automatically closed by the system", - "Livechat_auto_close_on_hold_chats_timeout": "How long the chat will remain in the On Hold queue until it's automatically closed by the system (in minutes) ?", + "Livechat_auto_close_on_hold_chats_timeout": "How long the chat will remain in the On Hold queue until it's automatically closed by the system (in seconds) ?", "Livechat_auto_transfer_chat_timeout": "Timeout (in seconds) for automatic transfer of unanswered chats to another agent", "Livechat_auto_transfer_chat_timeout_description": "This event takes place only when the chat has just started. After the first transfering for inactivity, the room is no longer monitored.", "Livechat_business_hour_type": "Business Hour Type (Single or Multiple)", @@ -4243,6 +4245,7 @@ "Without_priority": "Without priority", "Worldwide": "Worldwide", "Would_you_like_to_return_the_inquiry": "Would you like to return the inquiry?", + "Would_you_like_to_place_chat_on_hold": "Would you like to place this chat On-Hold?", "Yes": "Yes", "Yes_archive_it": "Yes, archive it!", "Yes_clear_all": "Yes, clear all!", From e77d930df03f46ab33630a92e1ddc12af9f74eed Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Tue, 16 Mar 2021 16:44:00 +0530 Subject: [PATCH 11/30] Apply suggestions from code review --- app/authorization/server/startup.js | 4 +-- .../client/views/app/livechatOnHold.html | 2 +- .../client/views/app/livechatOnHold.js | 11 +++----- .../client/views/app/tabbar/visitorInfo.js | 2 -- app/livechat/server/lib/Livechat.js | 1 - app/livechat/server/lib/RoutingManager.js | 3 --- app/models/server/models/Subscriptions.js | 8 +++--- app/models/server/raw/Users.js | 2 +- .../client/messageBox/messageBox.html | 2 +- .../client/messageBox/messageBox.js | 10 +++----- client/sidebar/hooks/useRoomList.ts | 18 ++++++------- definition/IRoom.ts | 2 +- definition/ISubscription.ts | 2 +- .../server/hooks/afterOnHold.ts | 4 --- .../hooks/checkAgentBeforeTakeInquiry.js | 3 --- .../server/hooks/scheduleManualOnHold.ts | 25 +++---------------- ee/app/livechat-enterprise/server/index.js | 1 + .../server/lib/AutoCloseOnHoldScheduler.ts | 9 +------ .../server/lib/AutoTransferChatScheduler.ts | 3 --- .../server/lib/LivechatEnterprise.js | 8 ++---- ...dScheduler.ts => ManualOnHoldScheduler.ts} | 13 +++------- .../server/methods/onHold.js | 8 +++--- .../server/methods/resumeOnHold.ts | 20 ++++----------- .../livechat-enterprise/server/permissions.js | 4 +-- ee/app/models/server/models/LivechatRooms.js | 21 ++++++++++++---- server/modules/watchers/publishFields.ts | 2 +- server/modules/watchers/watchers.module.ts | 1 - 27 files changed, 64 insertions(+), 125 deletions(-) rename ee/app/livechat-enterprise/server/lib/{OnHoldScheduler.ts => ManualOnHoldScheduler.ts} (77%) diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js index 68e35e00ea435..759c363bb3d50 100644 --- a/app/authorization/server/startup.js +++ b/app/authorization/server/startup.js @@ -96,8 +96,8 @@ Meteor.startup(function() { { _id: 'view-livechat-rooms', roles: ['livechat-manager', 'admin'] }, { _id: 'close-livechat-room', roles: ['livechat-agent', 'livechat-manager', 'admin'] }, { _id: 'close-others-livechat-room', roles: ['livechat-manager', 'admin'] }, - { _id: 'onHold-livechat-room', roles: ['livechat-agent', 'livechat-manager', 'admin'] }, - { _id: 'onHold-others-livechat-room', roles: ['livechat-manager', 'admin'] }, + { _id: 'on-hold-livechat-room', roles: ['livechat-agent', 'livechat-manager', 'admin'] }, + { _id: 'on-hold-others-livechat-room', roles: ['livechat-manager', 'admin'] }, { _id: 'save-others-livechat-room-info', roles: ['livechat-manager'] }, { _id: 'remove-closed-livechat-rooms', roles: ['livechat-manager', 'admin'] }, { _id: 'view-livechat-analytics', roles: ['livechat-manager', 'admin'] }, diff --git a/app/livechat/client/views/app/livechatOnHold.html b/app/livechat/client/views/app/livechatOnHold.html index 78316aeaf2918..f959eeab64de5 100644 --- a/app/livechat/client/views/app/livechatOnHold.html +++ b/app/livechat/client/views/app/livechatOnHold.html @@ -5,7 +5,7 @@ {{else}}
{{{_ "chat_on_hold_due_to_inactivity"}}} - +
{{/if}} {{else}} diff --git a/app/livechat/client/views/app/livechatOnHold.js b/app/livechat/client/views/app/livechatOnHold.js index 57e1c9b389fea..9629476af7f04 100644 --- a/app/livechat/client/views/app/livechatOnHold.js +++ b/app/livechat/client/views/app/livechatOnHold.js @@ -8,7 +8,6 @@ import { call } from '../../../../ui-utils/client'; Template.livechatOnHold.helpers({ roomOpen() { const room = Template.instance().room.get(); - console.log('---roomOpen', room.open); return room && room.open === true; }, @@ -18,13 +17,12 @@ Template.livechatOnHold.helpers({ }); Template.livechatOnHold.events({ - async 'click .js-take-it'(event, instance) { + async 'click .js-resume-it'(event, instance) { event.preventDefault(); event.stopPropagation(); const room = instance.room.get(); - console.log('---button clicked', room); await call('livechat:resumeOnHold', room._id, { clientAction: true }); }, }); @@ -32,12 +30,11 @@ Template.livechatOnHold.events({ Template.livechatOnHold.onCreated(function() { this.rid = Template.currentData().rid; this.room = new ReactiveVar(); + this.preparing = new ReactiveVar(true); this.autorun(() => { + this.preparing.set(true); this.room.set(ChatRoom.findOne({ _id: Template.currentData().rid })); + this.preparing.set(false); }); }); - -Template.livechatOnHold.onDestroyed(function() { - -}); diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 34004830943cd..372c425fa3ead 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -208,7 +208,6 @@ Template.visitorInfo.helpers({ canPlaceChatOnHold() { const room = Template.instance().room.get(); - console.log('---canPlaceChatOnHold', room); return !!room.canPlaceChatOnHold; }, @@ -333,7 +332,6 @@ Template.visitorInfo.events({ 'click .on-hold'(event) { event.preventDefault(); - console.log('--on-hold manual button clicked'); modal.open({ title: t('Would_you_like_to_place_chat_on_hold'), diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index cb08cf22d386d..31a7edcb361dd 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -372,7 +372,6 @@ export const Livechat = { sendMessage(user || visitor, message, room); - console.log('---close room servedBy', servedBy); if (servedBy) { Subscriptions.removeByRoomIdAndUserId(rid, servedBy._id); } diff --git a/app/livechat/server/lib/RoutingManager.js b/app/livechat/server/lib/RoutingManager.js index d640b56dd0895..49aec01b625a1 100644 --- a/app/livechat/server/lib/RoutingManager.js +++ b/app/livechat/server/lib/RoutingManager.js @@ -49,8 +49,6 @@ export const RoutingManager = { agent = await this.getNextAgent(department); } - console.log('---delegateInquiry called'); - if (!agent) { return LivechatRooms.findOneById(rid); } @@ -123,7 +121,6 @@ export const RoutingManager = { rid: String, status: String, })); - console.log('---takeInquiry called', inquiry, agent); const { _id, rid } = inquiry; const room = LivechatRooms.findOneById(rid); diff --git a/app/models/server/models/Subscriptions.js b/app/models/server/models/Subscriptions.js index c21b63487285b..fcd24f02c1dcb 100644 --- a/app/models/server/models/Subscriptions.js +++ b/app/models/server/models/Subscriptions.js @@ -1331,18 +1331,18 @@ export class Subscriptions extends Base { return this.update(query, update, { multi: true }); } - setIsChatOnHold(roomId) { + setOnHold(roomId) { return this.update( { rid: roomId }, - { $set: { isChatOnHold: true } }, + { $set: { onHold: true } }, { multi: true }, ); } - unsetIsChatOnHold(roomId) { + unsetOnHold(roomId) { return this.update( { rid: roomId }, - { $unset: { isChatOnHold: 1 } }, + { $unset: { onHold: 1 } }, { multi: true }, ); } diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js index 61ec941f6eb13..b3d798557faaa 100644 --- a/app/models/server/raw/Users.js +++ b/app/models/server/raw/Users.js @@ -191,7 +191,7 @@ export class UsersRaw extends BaseRaw { const aggregate = [ { $match: { _id: userId, status: { $exists: true, $ne: 'offline' }, statusLivechat: 'available', roles: 'livechat-agent' } }, { $lookup: { from: 'rocketchat_subscription', localField: '_id', foreignField: 'u._id', as: 'subs' } }, - { $project: { agentId: '$_id', username: 1, lastAssignTime: 1, lastRoutingTime: 1, 'queueInfo.chats': { $size: { $filter: { input: '$subs', as: 'sub', cond: { $and: [{ $eq: ['$$sub.t', 'l'] }, { $eq: ['$$sub.open', true] }, { $ne: ['$$sub.isChatOnHold', true] }] } } } } } }, + { $project: { agentId: '$_id', username: 1, lastAssignTime: 1, lastRoutingTime: 1, 'queueInfo.chats': { $size: { $filter: { input: '$subs', as: 'sub', cond: { $and: [{ $eq: ['$$sub.t', 'l'] }, { $eq: ['$$sub.open', true] }, { $ne: ['$$sub.onHold', true] }] } } } } } }, { $sort: { 'queueInfo.chats': 1, lastAssignTime: 1, lastRoutingTime: 1, username: 1 } }, ]; diff --git a/app/ui-message/client/messageBox/messageBox.html b/app/ui-message/client/messageBox/messageBox.html index 09420c71a7c8b..09a7bc69c605f 100644 --- a/app/ui-message/client/messageBox/messageBox.html +++ b/app/ui-message/client/messageBox/messageBox.html @@ -84,7 +84,7 @@ {{#if isBlockedOrBlocker}} {{_ "room_is_blocked"}} {{else}} - {{#if isChatOnHold}} + {{#if onHold}} {{> livechatOnHold rid=rid isSubscribed=isSubscribed}} {{else}} {{> messageBoxReadOnly rid=rid isSubscribed=isSubscribed}} diff --git a/app/ui-message/client/messageBox/messageBox.js b/app/ui-message/client/messageBox/messageBox.js index d44fa4bb76de7..d4111dc736d35 100644 --- a/app/ui-message/client/messageBox/messageBox.js +++ b/app/ui-message/client/messageBox/messageBox.js @@ -121,7 +121,6 @@ Template.messageBox.onRendered(function() { } $input.on('dataChange', () => { const messages = $input.data('reply') || []; - console.log('dataChange', messages); this.replyMessageData.set(messages); }); } @@ -210,14 +209,12 @@ Template.messageBox.helpers({ } const isBlockedOrBlocker = Template.instance().state.get('isBlockedOrBlocker'); - console.log('---isBlockedOrBlocker', isBlockedOrBlocker); if (isBlockedOrBlocker) { return false; } - console.log('---isWritable', subscription.isChatOnHold); - if (subscription.isChatOnHold) { + if (subscription?.onHold) { return false; } @@ -263,13 +260,12 @@ Template.messageBox.helpers({ isBlockedOrBlocker() { return Template.instance().state.get('isBlockedOrBlocker'); }, - isChatOnHold() { + onHold() { const { rid, subscription } = Template.currentData(); - console.log('----isChatOnHold', rid, subscription.isChatOnHold); if (!rid) { return false; } - return !!subscription.isChatOnHold; + return !!subscription?.onHold; }, isSubscribed() { const { subscription } = Template.currentData(); diff --git a/client/sidebar/hooks/useRoomList.ts b/client/sidebar/hooks/useRoomList.ts index 0ac05b6c2824c..2efe703a46a94 100644 --- a/client/sidebar/hooks/useRoomList.ts +++ b/client/sidebar/hooks/useRoomList.ts @@ -33,8 +33,7 @@ export const useRoomList = (): Array => { const direct = new Set(); const discussion = new Set(); const conversation = new Set(); - const omnichannelOnHold = new Set(); - console.log(rooms); + const onHold = new Set(); rooms.forEach((room) => { if (sidebarShowUnread && (room.alert || room.unread) && !room.hideUnreadStatus) { @@ -57,13 +56,12 @@ export const useRoomList = (): Array => { _private.add(room); } - if (room.t === 'l' && room.isChatOnHold) { - return showOmnichannel && omnichannelOnHold.add(room); + if (room.t === 'l' && room.onHold) { + return showOmnichannel && onHold.add(room); } if (room.t === 'l') { return showOmnichannel && omnichannel.add(room); - // return showOmnichannel && !room.isChatOnHold && omnichannel.add(room); } if (room.t === 'd') { @@ -73,15 +71,13 @@ export const useRoomList = (): Array => { conversation.add(room); }); - console.log('----omnichannelOnHold', omnichannelOnHold); - console.log('-----inquiries', inquiries); const groups = new Map(); - showOmnichannel && (inquiries.enabled || omnichannelOnHold.size) && groups.set('Omnichannel', []); - showOmnichannel && !inquiries.enabled && !omnichannelOnHold.size && groups.set('Omnichannel', omnichannel); + showOmnichannel && (inquiries.enabled || onHold.size) && groups.set('Omnichannel', []); + showOmnichannel && !inquiries.enabled && !onHold.size && groups.set('Omnichannel', omnichannel); showOmnichannel && inquiries.enabled && inquiries.queue.length && groups.set('Incoming_Livechats', inquiries.queue); - showOmnichannel && (inquiries.enabled || omnichannelOnHold.size) && omnichannel.size && groups.set('Open_Livechats', omnichannel); - showOmnichannel && omnichannelOnHold.size && groups.set('On_Hold_Chats', omnichannelOnHold); + showOmnichannel && (inquiries.enabled || onHold.size) && omnichannel.size && groups.set('Open_Livechats', omnichannel); + showOmnichannel && onHold.size && groups.set('On_Hold_Chats', onHold); sidebarShowUnread && unread.size && groups.set('Unread', unread); favoritesEnabled && favorite.size && groups.set('Favorites', favorite); showDiscussion && discussion.size && groups.set('Discussions', discussion); diff --git a/definition/IRoom.ts b/definition/IRoom.ts index e4c0a60aac795..9809ba69be7f9 100644 --- a/definition/IRoom.ts +++ b/definition/IRoom.ts @@ -39,7 +39,7 @@ export interface IRoom extends IRocketChatRecord { }[]; }; - isChatOnHold?: boolean; + onHold?: boolean; canPlaceChatOnHold?: boolean; } diff --git a/definition/ISubscription.ts b/definition/ISubscription.ts index 153e5209abd27..69b5a154d89d8 100644 --- a/definition/ISubscription.ts +++ b/definition/ISubscription.ts @@ -32,7 +32,7 @@ export interface ISubscription extends IRocketChatRecord { roles?: string[]; - isChatOnHold?: boolean; + onHold?: boolean; } export interface ISubscriptionDirectMessage extends Omit { diff --git a/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts b/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts index cbe6d74e1970a..279e64c2dec78 100644 --- a/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts +++ b/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts @@ -18,20 +18,16 @@ const handleAfterOnHold = async (room: any = {}): Promise => { } await AutoCloseOnHoldScheduler.scheduleRoom(room._id, autoCloseOnHoldChatTimeout, customCloseMessage); - console.log('-auto close job scheduled'); }; settings.get('Livechat_auto_close_on_hold_chats_timeout', (_, value) => { - console.log('---setting Livechat_auto_close_on_hold_chats_timeout called', value); autoCloseOnHoldChatTimeout = value as number; if (!value || value <= 0) { callbacks.remove('livechat:afterOnHold', 'livechat-auto-close-on-hold'); } - console.log('--adding callback'); callbacks.add('livechat:afterOnHold', handleAfterOnHold, callbacks.priority.HIGH, 'livechat-auto-close-on-hold'); }); settings.get('Livechat_auto_close_on_hold_chats_custom_message', (_, value) => { - console.log('---setting Livechat_auto_close_on_hold_chats_custom_message called', value); customCloseMessage = value as string || DEFAULT_CLOSED_MESSAGE; }); diff --git a/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js b/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js index 9db3f54b1334e..3dcb33c08d1de 100644 --- a/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js +++ b/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js @@ -24,7 +24,6 @@ callbacks.add('livechat.checkAgentBeforeTakeInquiry', async (agent, inquiry) => const { agentId } = agent; const maxNumberSimultaneousChat = getMaxNumberSimultaneousChat({ agentId, departmentId }); - console.log('----maxNumberSimultaneousChat', maxNumberSimultaneousChat); if (maxNumberSimultaneousChat === 0) { return agent; } @@ -34,8 +33,6 @@ callbacks.add('livechat.checkAgentBeforeTakeInquiry', async (agent, inquiry) => return null; } - console.log('---max limit', maxNumberSimultaneousChat, '---curr', user.queueInfo); - const { queueInfo: { chats = 0 } = {} } = user; if (maxNumberSimultaneousChat <= chats) { callbacks.run('livechat.onMaxNumberSimultaneousChatsReached', inquiry); diff --git a/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts b/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts index fd8125926a848..8cbae330360fc 100644 --- a/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts +++ b/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts @@ -3,13 +3,13 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../app/callbacks/server'; import { settings } from '../../../../../app/settings/server'; import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; -import { OnHoldChatScheduler } from '../lib/OnHoldScheduler'; +import { ManualOnHoldChatScheduler } from '../lib/ManualOnHoldScheduler'; let manualOnHoldTimeout = -1; let manualOnHoldEnabled = false; const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promise => { - const { _id: rid, isChatOnHold } = room; + const { _id: rid, onHold } = room; if (!rid) { return message; } @@ -28,7 +28,7 @@ const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promis } // TODO: find a better place to add this - if (message.token && isChatOnHold) { + if (message.token && onHold) { await AutoCloseOnHoldScheduler.unscheduleRoom(rid); await Meteor.call('livechat:resumeOnHold', room._id, { clientAction: false }); return message; @@ -38,40 +38,23 @@ const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promis return message; } - switch (!!message.token) { - case true: { - // message sent by visitor - console.log('---cancelling all job', rid); - await OnHoldChatScheduler.unscheduleRoom(rid); - break; - } - case false: { - // message sent by agent - console.log('---scheduling job', rid); - await OnHoldChatScheduler.scheduleRoom(rid, manualOnHoldTimeout === 0 ? 2 : manualOnHoldTimeout); - break; - } - } + message.token ? await ManualOnHoldChatScheduler.unscheduleRoom(rid) : await ManualOnHoldChatScheduler.scheduleRoom(rid, manualOnHoldTimeout === 0 ? 2 : manualOnHoldTimeout); return message; }; settings.get('Livechat_allow_manual_on_hold', (_, value) => { - console.log('---setting allow manual onhold called', value); manualOnHoldEnabled = value as boolean; if (!manualOnHoldTimeout) { - console.log('--removeing callback'); callbacks.remove('afterSaveMessage', 'livechat-manual-on-hold'); return; } - console.log('--adding callback'); callbacks.add('afterSaveMessage', handleAfterSaveMessage, callbacks.priority.HIGH, 'livechat-manual-on-hold'); }); settings.get('Livechat_manual_on_hold_timeout', (_, value) => { - console.log('---setting Livechat_manual_on_hold_timeout called', value); manualOnHoldTimeout = value as number ? value as number : -1; }); diff --git a/ee/app/livechat-enterprise/server/index.js b/ee/app/livechat-enterprise/server/index.js index 3a3a86b15b432..34355a82a1ecd 100644 --- a/ee/app/livechat-enterprise/server/index.js +++ b/ee/app/livechat-enterprise/server/index.js @@ -32,6 +32,7 @@ import './hooks/scheduleManualOnHold'; import './hooks/afterOnHold'; import './lib/routing/LoadBalancing'; import './lib/AutoCloseOnHoldScheduler'; +import './lib/ManualOnHoldScheduler'; import { onLicense } from '../../license/server'; import './business-hour'; diff --git a/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts b/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts index edbb0dc5b7b52..136fde4cdeb9e 100644 --- a/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts +++ b/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts @@ -15,7 +15,6 @@ class AutoCloseOnHoldSchedulerClass { running: boolean; public init(): void { - console.log('----staring AutoCloseOnHoldSchedulerClass'); if (this.running) { return; } @@ -47,8 +46,6 @@ class AutoCloseOnHoldSchedulerClass { } private async executeJob({ attrs: { data } }: any = {}): Promise { - console.log('---executeJob called', data); - const { roomId, comment } = data; const payload = { @@ -59,16 +56,12 @@ class AutoCloseOnHoldSchedulerClass { visitor: undefined, }; - console.log('--payload', payload); - - const result = Livechat.closeRoom(payload); - console.log('---result', result); + Livechat.closeRoom(payload); } } export const AutoCloseOnHoldScheduler = new AutoCloseOnHoldSchedulerClass(); Meteor.startup(() => { - console.log('---from meteor startup - AutoCloseOnHoldScheduler'); AutoCloseOnHoldScheduler.init(); }); diff --git a/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index 2d9c8c66ef430..f95557528d2f9 100644 --- a/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -6,7 +6,6 @@ import { LivechatRooms, Users } from '../../../../../app/models/server'; import { Livechat } from '../../../../../app/livechat/server'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { forwardRoomToAgent } from '../../../../../app/livechat/server/lib/Helper'; -import { OnHoldChatScheduler } from './OnHoldScheduler'; const schedulerUser = Users.findOneById('rocket.cat'); const SCHEDULER_NAME = 'omnichannel_scheduler'; @@ -86,7 +85,5 @@ class AutoTransferChatSchedulerClass { export const AutoTransferChatScheduler = new AutoTransferChatSchedulerClass(); Meteor.startup(() => { - console.log('---AutoTransferChatScheduler called'); AutoTransferChatScheduler.init(); - OnHoldChatScheduler.init(); }); diff --git a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index 3a28050545ada..d8794cbec41e7 100644 --- a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -171,15 +171,11 @@ export const LivechatEnterprise = { if (!roomId) { return; } - console.log('-------LivechatEnterprise.placeRoomOnHold', roomId); - let resp = LivechatRooms.setIsChatOnHold(roomId); - console.log('----placeRoomOnHold rooms db response', resp); - resp = Subscriptions.setIsChatOnHold(roomId); - console.log('----placeRoomOnHold subscription db response', resp); + LivechatRooms.setOnHold(roomId); + Subscriptions.setOnHold(roomId); LivechatRooms.unsetCanPlaceOnHold(roomId); Meteor.defer(() => { - console.log('---livechat:afterOnHold callback set'); callbacks.run('livechat:afterOnHold', room); }); }, diff --git a/ee/app/livechat-enterprise/server/lib/OnHoldScheduler.ts b/ee/app/livechat-enterprise/server/lib/ManualOnHoldScheduler.ts similarity index 77% rename from ee/app/livechat-enterprise/server/lib/OnHoldScheduler.ts rename to ee/app/livechat-enterprise/server/lib/ManualOnHoldScheduler.ts index dbcba28bc1f01..02965b37a06ac 100644 --- a/ee/app/livechat-enterprise/server/lib/OnHoldScheduler.ts +++ b/ee/app/livechat-enterprise/server/lib/ManualOnHoldScheduler.ts @@ -7,7 +7,7 @@ import { LivechatRooms } from '../../../../../app/models/server'; const SCHEDULER_NAME = 'omnichannel_manual_on_hold_scheduler'; -class OnHoldSchedulerClass { +class ManualOnHoldSchedulerClass { scheduler: Agenda; running: boolean; @@ -15,7 +15,6 @@ class OnHoldSchedulerClass { user: {}; public init(): void { - console.log('----staring OnHoldSchedulerClass'); if (this.running) { return; } @@ -50,18 +49,14 @@ class OnHoldSchedulerClass { } private async executeJob({ attrs: { data } }: any = {}): Promise { - console.log('---executeJob called', data); - const { roomId } = data; - const result = await (LivechatRooms as any).setCanPlaceOnHold(roomId); - console.log('---result', result); + await (LivechatRooms as any).setCanPlaceOnHold(roomId); } } -export const OnHoldChatScheduler = new OnHoldSchedulerClass(); +export const ManualOnHoldChatScheduler = new ManualOnHoldSchedulerClass(); Meteor.startup(() => { - console.log('---from meteor startup - OnHoldChatScheduler'); - // OnHoldChatScheduler.init(); + ManualOnHoldChatScheduler.init(); }); diff --git a/ee/app/livechat-enterprise/server/methods/onHold.js b/ee/app/livechat-enterprise/server/methods/onHold.js index 27c26cdf49f7f..32cc6791d42bc 100644 --- a/ee/app/livechat-enterprise/server/methods/onHold.js +++ b/ee/app/livechat-enterprise/server/methods/onHold.js @@ -6,27 +6,25 @@ import { LivechatEnterprise } from '../lib/LivechatEnterprise'; Meteor.methods({ 'livechat:placeChatOnHold'(roomId) { - console.log('--backend called'); const userId = Meteor.userId(); - if (!userId || !hasPermission(userId, 'onHold-livechat-room')) { + if (!userId || !hasPermission(userId, 'on-hold-livechat-room')) { throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:placeChatOnHold' }); } const room = LivechatRooms.findOneById(roomId); - console.log('--room found', room); if (!room || room.t !== 'l') { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:placeChatOnHold' }); } - if (room.isChatOnHold) { + if (room.onHold) { throw new Meteor.Error('room-closed', 'Room is already On-Hold', { method: 'livechat:placeChatOnHold' }); } const user = Meteor.user(); const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { _id: 1 }); - if (!subscription && !hasPermission(userId, 'onHold-others-livechat-room')) { + if (!subscription && !hasPermission(userId, 'on-hold-others-livechat-room')) { throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:placeChatOnHold' }); } diff --git a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index 2638866e52585..dafb771244f44 100644 --- a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -9,36 +9,30 @@ import { dispatchInquiryPosition } from '../lib/Helper'; Meteor.methods({ async 'livechat:resumeOnHold'(roomId, options = { clientAction: false }) { - console.log('--livechat:resumeOnHold called'); - const room = await LivechatRooms.findOneById(roomId); - console.log('--room found', room); if (!room || room.t !== 'l') { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:resumeOnHold' }); } - if (!room.isChatOnHold) { + if (!room.onHold) { throw new Meteor.Error('room-closed', 'Room is not OnHold', { method: 'livechat:resumeOnHold' }); } const { servedBy: { _id: agentId = null, username = null } = {} } = room; let agent: any = { agentId, username }; - const inquiry = await LivechatInquiry.findOneByRoomId(roomId); + const inquiry = await LivechatInquiry.findOneByRoomId(roomId, {}); const { departmentId } = inquiry; try { - console.log('---b4 livechat.checkAgentBeforeTakeInquiry', agent, inquiry); agent = await callbacks.run('livechat.checkAgentBeforeTakeInquiry', agent, inquiry); } catch (e) { - console.log('--error', e); + console.log(e); if (options.clientAction) { throw new Meteor.Error('error-max-number-simultaneous-chats-reached', 'Not allowed'); } agent = null; } - console.log('----resumeOnHold agent', agent); - if (!agent) { Livechat.returnRoomAsInquiry(room._id, departmentId); @@ -52,11 +46,7 @@ Meteor.methods({ } } - let resp = (LivechatRooms as any).unsetIsChatOnHold(roomId); - console.log('----resumeOnHold rooms db response', resp); - resp = Subscriptions.unsetIsChatOnHold(roomId); - console.log('----resumeOnHold subscription db response', resp); - (LivechatRooms as any).unsetCanPlaceOnHold(roomId); - (LivechatRooms as any).unsetPredictedVisitorAbandonmentByRoomId(roomId); + (LivechatRooms as any).unsetAllOnHoldFieldsByRoomId(roomId); + Subscriptions.unsetOnHold(roomId); }, }); diff --git a/ee/app/livechat-enterprise/server/permissions.js b/ee/app/livechat-enterprise/server/permissions.js index ed90b60df2d05..2e934be9cea44 100644 --- a/ee/app/livechat-enterprise/server/permissions.js +++ b/ee/app/livechat-enterprise/server/permissions.js @@ -21,8 +21,8 @@ export const createPermissions = () => { 'view-livechat-rooms', 'close-livechat-room', 'close-others-livechat-room', - 'onHold-livechat-room', - 'onHold-others-livechat-room', + 'on-hold-livechat-room', + 'on-hold-others-livechat-room', 'save-others-livechat-room-info', 'remove-closed-livechat-rooms', 'view-livechat-analytics', diff --git a/ee/app/models/server/models/LivechatRooms.js b/ee/app/models/server/models/LivechatRooms.js index 21fc732f4a649..3abdfc076dba0 100644 --- a/ee/app/models/server/models/LivechatRooms.js +++ b/ee/app/models/server/models/LivechatRooms.js @@ -46,22 +46,21 @@ LivechatRooms.prototype.findAbandonedOpenRooms = function(date) { }); }; -LivechatRooms.prototype.setIsChatOnHold = function(roomId) { +LivechatRooms.prototype.setOnHold = function(roomId) { return this.update( { _id: roomId }, - { $set: { isChatOnHold: true } }, + { $set: { onHold: true } }, ); }; -LivechatRooms.prototype.unsetIsChatOnHold = function(roomId) { +LivechatRooms.prototype.unsetOnHold = function(roomId) { return this.update( { _id: roomId }, - { $unset: { isChatOnHold: 1 } }, + { $unset: { onHold: 1 } }, ); }; LivechatRooms.prototype.setCanPlaceOnHold = function(roomId) { - console.log('---within model', roomId); return this.update( { _id: roomId }, { $set: { canPlaceChatOnHold: true } }, @@ -94,6 +93,18 @@ LivechatRooms.prototype.unsetPredictedVisitorAbandonmentByRoomId = function(room }); }; +LivechatRooms.prototype.unsetAllOnHoldFieldsByRoomId = function(roomId) { + return this.update({ + _id: roomId, + }, { + $unset: { + 'omnichannel.predictedVisitorAbandonmentAt': 1, + onHold: 1, + canPlaceChatOnHold: 1, + }, + }); +}; + LivechatRooms.prototype.unsetPriorityById = function(priorityId) { return this.update({ open: true, diff --git a/server/modules/watchers/publishFields.ts b/server/modules/watchers/publishFields.ts index 4fc319bb38989..21aeb67d859b7 100644 --- a/server/modules/watchers/publishFields.ts +++ b/server/modules/watchers/publishFields.ts @@ -37,7 +37,7 @@ export const subscriptionFields = { tunreadGroup: 1, tunreadUser: 1, v: 1, - isChatOnHold: 1, + onHold: 1, }; export const roomFields = { diff --git a/server/modules/watchers/watchers.module.ts b/server/modules/watchers/watchers.module.ts index 5430b4be98b06..a6f856a4413d0 100644 --- a/server/modules/watchers/watchers.module.ts +++ b/server/modules/watchers/watchers.module.ts @@ -128,7 +128,6 @@ export function initWatchers(models: IModelsParam, broadcast: BroadcastCallback, case 'updated': { // Override data cuz we do not publish all fields const subscription = await Subscriptions.findOneById(id, { projection: subscriptionFields }); - console.log('---subscription found', subscription); if (!subscription) { return; } From f22ce7229829de087908d4808e12279ca7151814 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Wed, 17 Mar 2021 21:34:40 +0530 Subject: [PATCH 12/30] Apply suggestions from code review --- app/livechat/server/lib/QueueManager.js | 2 +- app/theme/client/imports/components/message-box.css | 4 ++++ app/ui-message/client/messageBox/messageBox.js | 5 +---- ee/app/livechat-enterprise/server/hooks/afterOnHold.ts | 5 ++++- ee/app/livechat-enterprise/server/lib/Helper.js | 6 +++--- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/livechat/server/lib/QueueManager.js b/app/livechat/server/lib/QueueManager.js index 37a5805f12d81..febd9cbf59ba5 100644 --- a/app/livechat/server/lib/QueueManager.js +++ b/app/livechat/server/lib/QueueManager.js @@ -7,7 +7,7 @@ import { callbacks } from '../../../callbacks/server'; import { RoutingManager } from './RoutingManager'; -export const queueInquiry = async (room, inquiry, defaultAgent) => { +const queueInquiry = async (room, inquiry, defaultAgent) => { const inquiryAgent = RoutingManager.delegateAgent(defaultAgent, inquiry); await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent); inquiry = LivechatInquiry.findOneById(inquiry._id); diff --git a/app/theme/client/imports/components/message-box.css b/app/theme/client/imports/components/message-box.css index e596c69fcc65e..676c0fd33813d 100644 --- a/app/theme/client/imports/components/message-box.css +++ b/app/theme/client/imports/components/message-box.css @@ -270,6 +270,10 @@ margin: 0 0.5rem; } + &__resume-it-button { + margin: 0 0.5rem; + } + &__cannot-send { display: flex; justify-content: space-between; diff --git a/app/ui-message/client/messageBox/messageBox.js b/app/ui-message/client/messageBox/messageBox.js index d4111dc736d35..5c0ac2c3fe837 100644 --- a/app/ui-message/client/messageBox/messageBox.js +++ b/app/ui-message/client/messageBox/messageBox.js @@ -262,10 +262,7 @@ Template.messageBox.helpers({ }, onHold() { const { rid, subscription } = Template.currentData(); - if (!rid) { - return false; - } - return !!subscription?.onHold; + return rid && !!subscription?.onHold; }, isSubscribed() { const { subscription } = Template.currentData(); diff --git a/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts b/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts index 279e64c2dec78..6a7a4063f28bc 100644 --- a/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts +++ b/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts @@ -1,8 +1,11 @@ +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; + import { callbacks } from '../../../../../app/callbacks/server'; import { settings } from '../../../../../app/settings/server'; import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; -const DEFAULT_CLOSED_MESSAGE = 'Chat is closed because of inactivity'; + +const DEFAULT_CLOSED_MESSAGE = TAPi18n.__('Closed_automatically'); let autoCloseOnHoldChatTimeout = 0; let customCloseMessage = DEFAULT_CLOSED_MESSAGE; diff --git a/ee/app/livechat-enterprise/server/lib/Helper.js b/ee/app/livechat-enterprise/server/lib/Helper.js index c1768a40b537c..ed11b351334b4 100644 --- a/ee/app/livechat-enterprise/server/lib/Helper.js +++ b/ee/app/livechat-enterprise/server/lib/Helper.js @@ -144,10 +144,10 @@ export const setPredictedVisitorAbandonmentTime = (room) => { }; export const updatePredictedVisitorAbandonment = () => { - if (settings.get('Livechat_abandoned_rooms_action') && (settings.get('Livechat_abandoned_rooms_action') === 'none')) { - LivechatRooms.findLivechat({ open: true }).forEach((room) => setPredictedVisitorAbandonmentTime(room)); - } else { + if (!settings.get('Livechat_abandoned_rooms_action') || (settings.get('Livechat_abandoned_rooms_action') === 'none')) { LivechatRooms.unsetPredictedVisitorAbandonment(); + } else { + LivechatRooms.findLivechat({ open: true }).forEach((room) => setPredictedVisitorAbandonmentTime(room)); } }; From b6717b58124e21b394fff8f56d307ad637ec7713 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Thu, 18 Mar 2021 00:38:29 +0530 Subject: [PATCH 13/30] Add migration --- server/startup/migrations/v219.js | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 server/startup/migrations/v219.js diff --git a/server/startup/migrations/v219.js b/server/startup/migrations/v219.js new file mode 100644 index 0000000000000..3982711420485 --- /dev/null +++ b/server/startup/migrations/v219.js @@ -0,0 +1,35 @@ +import { Migrations } from '../../../app/migrations/server'; +import { Settings } from '../../../app/models/server'; + +Migrations.add({ + version: 219, + up() { + const SettingIds = [ + { + old: 'Livechat_auto_close_abandoned_rooms', + new: 'Livechat_abandoned_rooms_action', + }, + ]; + + const oldSetting = Settings.findOne({ _id: SettingIds.old }); + if (!oldSetting) { + return; + } + + const oldValue = oldSetting.value; + + const newValue = oldValue && oldValue === true ? 'close' : 'none'; + + Settings.update({ + _id: SettingIds.new, + }, { + $set: { + value: newValue, + }, + }); + + Settings.remove({ + _id: SettingIds.old, + }); + }, +}); From 016070576bb2395adea0274ef42be997bbc250e8 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Thu, 18 Mar 2021 01:42:31 +0530 Subject: [PATCH 14/30] Add new endpoint - livechat/placeChatOnHold --- .../client/views/app/tabbar/visitorInfo.js | 23 ++++++----- .../livechat-enterprise/server/api/index.js | 1 + .../livechat-enterprise/server/api/on-hold.js | 40 +++++++++++++++++++ .../server/hooks/scheduleManualOnHold.ts | 2 +- ee/app/livechat-enterprise/server/index.js | 1 - .../server/methods/onHold.js | 33 --------------- 6 files changed, 54 insertions(+), 46 deletions(-) create mode 100644 ee/app/livechat-enterprise/server/api/on-hold.js delete mode 100644 ee/app/livechat-enterprise/server/methods/onHold.js diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 372c425fa3ead..187e6a68e920e 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -340,18 +340,19 @@ Template.visitorInfo.events({ confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: t('Yes'), - }, () => Meteor.call('livechat:placeChatOnHold', this.rid, function(error/* , result*/) { - if (error) { - return handleError(error); + }, + async () => { + const { success } = await APIClient.v1.get(`livechat/placeChatOnHold?roomId=${ this.rid }`); + if (success) { + modal.open({ + title: t('Chat_On_Hold'), + text: t('Chat_On_Hold_Successfully'), + type: 'success', + timer: 1500, + showConfirmButton: false, + }); } - modal.open({ - title: t('Chat_On_Hold'), - text: t('Chat_On_Hold_Successfully'), - type: 'success', - timer: 1500, - showConfirmButton: false, - }); - })); + }); }, }); diff --git a/ee/app/livechat-enterprise/server/api/index.js b/ee/app/livechat-enterprise/server/api/index.js index da303b6e498ab..a96ccb73b2488 100644 --- a/ee/app/livechat-enterprise/server/api/index.js +++ b/ee/app/livechat-enterprise/server/api/index.js @@ -6,3 +6,4 @@ import './priorities'; import './tags'; import './units'; import './business-hours'; +import './on-hold'; diff --git a/ee/app/livechat-enterprise/server/api/on-hold.js b/ee/app/livechat-enterprise/server/api/on-hold.js new file mode 100644 index 0000000000000..f68328dc2c8bb --- /dev/null +++ b/ee/app/livechat-enterprise/server/api/on-hold.js @@ -0,0 +1,40 @@ +import { Meteor } from 'meteor/meteor'; + +import { API } from '../../../../../app/api/server'; +import { hasPermission } from '../../../../../app/authorization'; +import { Subscriptions, LivechatRooms } from '../../../../../app/models/server'; +import { LivechatEnterprise } from '../lib/LivechatEnterprise'; + + +API.v1.addRoute('livechat/placeChatOnHold', { authRequired: true }, { + get() { + const { roomId } = this.queryParams; + if (!roomId || roomId.trim() === '') { + return API.v1.failure('Invalid room Id'); + } + + if (!this.userId || !hasPermission(this.userId, 'on-hold-livechat-room')) { + return API.v1.failure('Not authorized'); + } + + const room = LivechatRooms.findOneById(roomId); + if (!room || room.t !== 'l') { + return API.v1.failure('Invalid room Id'); + } + + if (room.onHold) { + return API.v1.failure('Room is already On-Hold'); + } + + const user = Meteor.user(); + + const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { _id: 1 }); + if (!subscription && !hasPermission(this.userId, 'on-hold-others-livechat-room')) { + return API.v1.failure('Not authorized'); + } + + LivechatEnterprise.placeRoomOnHold(room); + + return API.v1.success(); + }, +}); diff --git a/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts b/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts index 8cbae330360fc..2194cbbb2d362 100644 --- a/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts +++ b/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts @@ -46,7 +46,7 @@ const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promis settings.get('Livechat_allow_manual_on_hold', (_, value) => { manualOnHoldEnabled = value as boolean; - if (!manualOnHoldTimeout) { + if (!manualOnHoldEnabled) { callbacks.remove('afterSaveMessage', 'livechat-manual-on-hold'); return; } diff --git a/ee/app/livechat-enterprise/server/index.js b/ee/app/livechat-enterprise/server/index.js index 34355a82a1ecd..8be8bedf57a6c 100644 --- a/ee/app/livechat-enterprise/server/index.js +++ b/ee/app/livechat-enterprise/server/index.js @@ -11,7 +11,6 @@ import './methods/saveUnit'; import './methods/savePriority'; import './methods/removePriority'; import './methods/removeBusinessHour'; -import './methods/onHold'; import './methods/resumeOnHold'; import LivechatUnit from '../../models/server/models/LivechatUnit'; import LivechatTag from '../../models/server/models/LivechatTag'; diff --git a/ee/app/livechat-enterprise/server/methods/onHold.js b/ee/app/livechat-enterprise/server/methods/onHold.js deleted file mode 100644 index 32cc6791d42bc..0000000000000 --- a/ee/app/livechat-enterprise/server/methods/onHold.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../../../app/authorization'; -import { Subscriptions, LivechatRooms } from '../../../../../app/models/server'; -import { LivechatEnterprise } from '../lib/LivechatEnterprise'; - -Meteor.methods({ - 'livechat:placeChatOnHold'(roomId) { - const userId = Meteor.userId(); - - if (!userId || !hasPermission(userId, 'on-hold-livechat-room')) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:placeChatOnHold' }); - } - - const room = LivechatRooms.findOneById(roomId); - if (!room || room.t !== 'l') { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:placeChatOnHold' }); - } - - if (room.onHold) { - throw new Meteor.Error('room-closed', 'Room is already On-Hold', { method: 'livechat:placeChatOnHold' }); - } - - const user = Meteor.user(); - - const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { _id: 1 }); - if (!subscription && !hasPermission(userId, 'on-hold-others-livechat-room')) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:placeChatOnHold' }); - } - - return LivechatEnterprise.placeRoomOnHold(room); - }, -}); From 5eead6b2027ccd541bea84ecdec59fcc644bb375 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Fri, 19 Mar 2021 09:02:05 +0530 Subject: [PATCH 15/30] Move Resume Button login within livechatReadOnly file --- .../client/views/app/livechatOnHold.html | 14 ------- .../client/views/app/livechatOnHold.js | 40 ------------------- .../client/views/app/livechatReadOnly.html | 15 +++++-- .../client/views/app/livechatReadOnly.js | 13 ++++++ .../client/messageBox/messageBox.html | 6 +-- .../client/messageBox/messageBox.js | 1 - 6 files changed, 25 insertions(+), 64 deletions(-) delete mode 100644 app/livechat/client/views/app/livechatOnHold.html delete mode 100644 app/livechat/client/views/app/livechatOnHold.js diff --git a/app/livechat/client/views/app/livechatOnHold.html b/app/livechat/client/views/app/livechatOnHold.html deleted file mode 100644 index f959eeab64de5..0000000000000 --- a/app/livechat/client/views/app/livechatOnHold.html +++ /dev/null @@ -1,14 +0,0 @@ - diff --git a/app/livechat/client/views/app/livechatOnHold.js b/app/livechat/client/views/app/livechatOnHold.js deleted file mode 100644 index 9629476af7f04..0000000000000 --- a/app/livechat/client/views/app/livechatOnHold.js +++ /dev/null @@ -1,40 +0,0 @@ -import { Template } from 'meteor/templating'; -import { ReactiveVar } from 'meteor/reactive-var'; - -import { ChatRoom } from '../../../../models'; -import './livechatOnHold.html'; -import { call } from '../../../../ui-utils/client'; - -Template.livechatOnHold.helpers({ - roomOpen() { - const room = Template.instance().room.get(); - return room && room.open === true; - }, - - isPreparing() { - return Template.instance().preparing.get(); - }, -}); - -Template.livechatOnHold.events({ - async 'click .js-resume-it'(event, instance) { - event.preventDefault(); - event.stopPropagation(); - - const room = instance.room.get(); - - await call('livechat:resumeOnHold', room._id, { clientAction: true }); - }, -}); - -Template.livechatOnHold.onCreated(function() { - this.rid = Template.currentData().rid; - this.room = new ReactiveVar(); - this.preparing = new ReactiveVar(true); - - this.autorun(() => { - this.preparing.set(true); - this.room.set(ChatRoom.findOne({ _id: Template.currentData().rid })); - this.preparing.set(false); - }); -}); diff --git a/app/livechat/client/views/app/livechatReadOnly.html b/app/livechat/client/views/app/livechatReadOnly.html index 88c84a34435c6..4eea70906b1ef 100644 --- a/app/livechat/client/views/app/livechatReadOnly.html +++ b/app/livechat/client/views/app/livechatReadOnly.html @@ -4,13 +4,20 @@ {{#if isPreparing}} {{> loading}} {{else}} - {{#if inquiryOpen}} + {{#if isOnHold}}
- {{{_ "you_are_in_preview_mode_of_incoming_livechat"}}} - + {{{_ "chat_on_hold_due_to_inactivity"}}} +
{{else}} - {{_ "room_is_read_only"}} + {{#if inquiryOpen}} +
+ {{{_ "you_are_in_preview_mode_of_incoming_livechat"}}} + +
+ {{else}} + {{_ "room_is_read_only"}} + {{/if}} {{/if}} {{/if}} {{/if}} diff --git a/app/livechat/client/views/app/livechatReadOnly.js b/app/livechat/client/views/app/livechatReadOnly.js index ef516a7ac8854..ac397d5c4159a 100644 --- a/app/livechat/client/views/app/livechatReadOnly.js +++ b/app/livechat/client/views/app/livechatReadOnly.js @@ -28,6 +28,10 @@ Template.livechatReadOnly.helpers({ isPreparing() { return Template.instance().preparing.get(); }, + + isOnHold() { + return Template.currentData().onHold; + }, }); Template.livechatReadOnly.events({ @@ -40,6 +44,15 @@ Template.livechatReadOnly.events({ await call('livechat:takeInquiry', _id); instance.loadInquiry(inquiry.rid); }, + + async 'click .js-resume-it'(event, instance) { + event.preventDefault(); + event.stopPropagation(); + + const room = instance.room.get(); + + await call('livechat:resumeOnHold', room._id, { clientAction: true }); + }, }); Template.livechatReadOnly.onCreated(function() { diff --git a/app/ui-message/client/messageBox/messageBox.html b/app/ui-message/client/messageBox/messageBox.html index 09a7bc69c605f..a5fb8327baace 100644 --- a/app/ui-message/client/messageBox/messageBox.html +++ b/app/ui-message/client/messageBox/messageBox.html @@ -84,11 +84,7 @@ {{#if isBlockedOrBlocker}} {{_ "room_is_blocked"}} {{else}} - {{#if onHold}} - {{> livechatOnHold rid=rid isSubscribed=isSubscribed}} - {{else}} - {{> messageBoxReadOnly rid=rid isSubscribed=isSubscribed}} - {{/if}} + {{> messageBoxReadOnly rid=rid isSubscribed=isSubscribed onHold=onHold }} {{/if}} {{/if}} diff --git a/app/ui-message/client/messageBox/messageBox.js b/app/ui-message/client/messageBox/messageBox.js index 5c0ac2c3fe837..7fcf12f371a90 100644 --- a/app/ui-message/client/messageBox/messageBox.js +++ b/app/ui-message/client/messageBox/messageBox.js @@ -37,7 +37,6 @@ import './messageBoxAudioMessage'; import './messageBoxNotSubscribed'; import './messageBox.html'; import './messageBoxReadOnly'; -import '../../../livechat/client/views/app/livechatOnHold'; Template.messageBox.onCreated(function() { this.state = new ReactiveDict(); From 2005de7fddc54458ad94061869b2b9fa32c0824c Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Fri, 19 Mar 2021 09:41:25 +0530 Subject: [PATCH 16/30] Remove timeout on Manual On-Hold feature - From now on, the On-Hold option will appear within visitor Info panel, provided the agent has sent the last message --- .../client/views/app/tabbar/visitorInfo.html | 8 +-- .../client/views/app/tabbar/visitorInfo.js | 2 +- definition/IRoom.ts | 1 - .../server/hooks/resumeOnHold.ts | 34 ++++++++++ .../server/hooks/scheduleManualOnHold.ts | 60 ------------------ ee/app/livechat-enterprise/server/index.js | 3 +- .../server/lib/LivechatEnterprise.js | 1 - .../server/lib/ManualOnHoldScheduler.ts | 62 ------------------- ee/app/livechat-enterprise/server/settings.js | 13 +--- ee/app/models/server/models/LivechatRooms.js | 15 ----- packages/rocketchat-i18n/i18n/en.i18n.json | 2 +- server/modules/watchers/publishFields.ts | 1 - 12 files changed, 42 insertions(+), 160 deletions(-) create mode 100644 ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts delete mode 100644 ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts delete mode 100644 ee/app/livechat-enterprise/server/lib/ManualOnHoldScheduler.ts diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.html b/app/livechat/client/views/app/tabbar/visitorInfo.html index 01dafe832c99a..233bd77ccd360 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.html +++ b/app/livechat/client/views/app/tabbar/visitorInfo.html @@ -74,10 +74,6 @@

{{_ "Transcript_Request"}}

{{/if}} - {{#if canPlaceChatOnHold}} - - {{/if}} - {{#if roomOpen}} {{#if canCloseRoom}} @@ -93,6 +89,10 @@

{{_ "Transcript_Request"}}

{{#if canSendTranscript}} {{/if}} + + {{#if canPlaceChatOnHold}} + + {{/if}} {{/if}} diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 187e6a68e920e..2b42c6ea04343 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -208,7 +208,7 @@ Template.visitorInfo.helpers({ canPlaceChatOnHold() { const room = Template.instance().room.get(); - return !!room.canPlaceChatOnHold; + return room.open && !room.onHold && settings.get('Livechat_allow_manual_on_hold') && room.lastMessage && !room.lastMessage?.token; }, roomClosedDateTime() { diff --git a/definition/IRoom.ts b/definition/IRoom.ts index 9809ba69be7f9..a6504dedc7d6c 100644 --- a/definition/IRoom.ts +++ b/definition/IRoom.ts @@ -40,7 +40,6 @@ export interface IRoom extends IRocketChatRecord { }; onHold?: boolean; - canPlaceChatOnHold?: boolean; } export interface IDirectMessageRoom extends Omit { diff --git a/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts b/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts new file mode 100644 index 0000000000000..247b010cf918e --- /dev/null +++ b/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts @@ -0,0 +1,34 @@ +import { Meteor } from 'meteor/meteor'; + +import { callbacks } from '../../../../../app/callbacks/server'; +import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; + +const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promise => { + const { _id: rid, onHold } = room; + if (!rid) { + return message; + } + + // skips this callback if the message was edited + if (message.editedAt) { + return message; + } + // message valid only if it is a livechat room + if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.v && room.v.token)) { + return message; + } + // if the message has a type means it is a special message (like the closing comment), so skips + if (message.t) { + return message; + } + + // if a visitor sends a message in room which is On Hold + if (message.token && onHold) { + await AutoCloseOnHoldScheduler.unscheduleRoom(rid); + await Meteor.call('livechat:resumeOnHold', room._id, { clientAction: false }); + } + + return message; +}; + +callbacks.add('afterSaveMessage', handleAfterSaveMessage, callbacks.priority.HIGH, 'livechat-resume-on-hold'); diff --git a/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts b/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts deleted file mode 100644 index 2194cbbb2d362..0000000000000 --- a/ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { callbacks } from '../../../../../app/callbacks/server'; -import { settings } from '../../../../../app/settings/server'; -import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; -import { ManualOnHoldChatScheduler } from '../lib/ManualOnHoldScheduler'; - -let manualOnHoldTimeout = -1; -let manualOnHoldEnabled = false; - -const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promise => { - const { _id: rid, onHold } = room; - if (!rid) { - return message; - } - - // skips this callback if the message was edited - if (message.editedAt) { - return message; - } - // message valid only if it is a livechat room - if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.v && room.v.token)) { - return message; - } - // if the message has a type means it is a special message (like the closing comment), so skips - if (message.t) { - return message; - } - - // TODO: find a better place to add this - if (message.token && onHold) { - await AutoCloseOnHoldScheduler.unscheduleRoom(rid); - await Meteor.call('livechat:resumeOnHold', room._id, { clientAction: false }); - return message; - } - - if (!manualOnHoldEnabled || manualOnHoldTimeout < 0) { - return message; - } - - message.token ? await ManualOnHoldChatScheduler.unscheduleRoom(rid) : await ManualOnHoldChatScheduler.scheduleRoom(rid, manualOnHoldTimeout === 0 ? 2 : manualOnHoldTimeout); - - return message; -}; - - -settings.get('Livechat_allow_manual_on_hold', (_, value) => { - manualOnHoldEnabled = value as boolean; - if (!manualOnHoldEnabled) { - callbacks.remove('afterSaveMessage', 'livechat-manual-on-hold'); - return; - } - - callbacks.add('afterSaveMessage', handleAfterSaveMessage, callbacks.priority.HIGH, 'livechat-manual-on-hold'); -}); - - -settings.get('Livechat_manual_on_hold_timeout', (_, value) => { - manualOnHoldTimeout = value as number ? value as number : -1; -}); diff --git a/ee/app/livechat-enterprise/server/index.js b/ee/app/livechat-enterprise/server/index.js index 8be8bedf57a6c..3763b924c2195 100644 --- a/ee/app/livechat-enterprise/server/index.js +++ b/ee/app/livechat-enterprise/server/index.js @@ -27,11 +27,10 @@ import './hooks/onLoadConfigApi'; import './hooks/onCloseLivechat'; import './hooks/onSaveVisitorInfo'; import './hooks/scheduleAutoTransfer'; -import './hooks/scheduleManualOnHold'; +import './hooks/resumeOnHold'; import './hooks/afterOnHold'; import './lib/routing/LoadBalancing'; import './lib/AutoCloseOnHoldScheduler'; -import './lib/ManualOnHoldScheduler'; import { onLicense } from '../../license/server'; import './business-hour'; diff --git a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index d8794cbec41e7..17521d9e8d611 100644 --- a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -173,7 +173,6 @@ export const LivechatEnterprise = { } LivechatRooms.setOnHold(roomId); Subscriptions.setOnHold(roomId); - LivechatRooms.unsetCanPlaceOnHold(roomId); Meteor.defer(() => { callbacks.run('livechat:afterOnHold', room); diff --git a/ee/app/livechat-enterprise/server/lib/ManualOnHoldScheduler.ts b/ee/app/livechat-enterprise/server/lib/ManualOnHoldScheduler.ts deleted file mode 100644 index 02965b37a06ac..0000000000000 --- a/ee/app/livechat-enterprise/server/lib/ManualOnHoldScheduler.ts +++ /dev/null @@ -1,62 +0,0 @@ -import Agenda from 'agenda'; -import { MongoInternals } from 'meteor/mongo'; -import { Meteor } from 'meteor/meteor'; -import moment from 'moment'; - -import { LivechatRooms } from '../../../../../app/models/server'; - -const SCHEDULER_NAME = 'omnichannel_manual_on_hold_scheduler'; - -class ManualOnHoldSchedulerClass { - scheduler: Agenda; - - running: boolean; - - user: {}; - - public init(): void { - if (this.running) { - return; - } - - this.scheduler = new Agenda({ - mongo: (MongoInternals.defaultRemoteCollectionDriver().mongo as any).client.db(), - db: { collection: SCHEDULER_NAME }, - defaultConcurrency: 1, - }); - - this.scheduler.start(); - this.running = true; - } - - public async scheduleRoom(roomId: string, timeout: number): Promise { - await this.unscheduleRoom(roomId); - - const jobName = `${ SCHEDULER_NAME }-${ roomId }`; - const when = moment(new Date()).add(timeout, 's').toDate(); - - this.scheduler.define(jobName, this.executeJob.bind(this)); - await this.scheduler.schedule(when, jobName, { roomId }); - } - - - public async unscheduleRoom(roomId: string): Promise { - const jobName = `${ SCHEDULER_NAME }-${ roomId }`; - - await (LivechatRooms as any).unsetCanPlaceOnHold(roomId); - - await this.scheduler.cancel({ name: jobName }); - } - - private async executeJob({ attrs: { data } }: any = {}): Promise { - const { roomId } = data; - - await (LivechatRooms as any).setCanPlaceOnHold(roomId); - } -} - -export const ManualOnHoldChatScheduler = new ManualOnHoldSchedulerClass(); - -Meteor.startup(() => { - ManualOnHoldChatScheduler.init(); -}); diff --git a/ee/app/livechat-enterprise/server/settings.js b/ee/app/livechat-enterprise/server/settings.js index 78f385f60cd4d..c8674caabc3b8 100644 --- a/ee/app/livechat-enterprise/server/settings.js +++ b/ee/app/livechat-enterprise/server/settings.js @@ -158,18 +158,7 @@ export const createSettings = () => { enterprise: true, value: true, invalidValue: false, - modules: [ - 'livechat-enterprise', - ], - }); - - settings.add('Livechat_manual_on_hold_timeout', 60, { - type: 'int', - group: 'Omnichannel', - section: 'Sessions', - enterprise: true, - invalidValue: 0, - enableQuery: { _id: 'Livechat_allow_manual_on_hold', value: true }, + public: true, modules: [ 'livechat-enterprise', ], diff --git a/ee/app/models/server/models/LivechatRooms.js b/ee/app/models/server/models/LivechatRooms.js index 3abdfc076dba0..69144374584e0 100644 --- a/ee/app/models/server/models/LivechatRooms.js +++ b/ee/app/models/server/models/LivechatRooms.js @@ -60,20 +60,6 @@ LivechatRooms.prototype.unsetOnHold = function(roomId) { ); }; -LivechatRooms.prototype.setCanPlaceOnHold = function(roomId) { - return this.update( - { _id: roomId }, - { $set: { canPlaceChatOnHold: true } }, - ); -}; - -LivechatRooms.prototype.unsetCanPlaceOnHold = function(roomId) { - return this.update( - { _id: roomId }, - { $unset: { canPlaceChatOnHold: 1 } }, - ); -}; - LivechatRooms.prototype.unsetPredictedVisitorAbandonment = function() { return this.update({ open: true, @@ -100,7 +86,6 @@ LivechatRooms.prototype.unsetAllOnHoldFieldsByRoomId = function(roomId) { $unset: { 'omnichannel.predictedVisitorAbandonmentAt': 1, onHold: 1, - canPlaceChatOnHold: 1, }, }); }; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 36e66339250c1..71734f7a7af5d 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2410,6 +2410,7 @@ "Livechat_agents": "Omnichannel agents", "Livechat_Agents": "Agents", "Livechat_allow_manual_on_hold": "Allow agents to manually place chat On Hold", + "Livechat_allow_manual_on_hold_description": "If enabled, the agent will get a new option to place a chat On Hold, provided the agent has sent the last message", "Livechat_AllowedDomainsList": "Livechat Allowed Domains", "Livechat_Appearance": "Livechat Appearance", "Livechat_auto_close_on_hold_chats_custom_message": "Custom Message to be sent when a room in On-Hold queue gets automatically closed by the system", @@ -2437,7 +2438,6 @@ "Livechat_last_chatted_agent_routing_Description": "The Last-Chatted Agent setting allocates chats to the agent who previously interacted with the same visitor if the agent is available when the chat starts.", "Livechat_managers": "Omnichannel managers", "Livechat_Managers": "Managers", - "Livechat_manual_on_hold_timeout": "How long an agent has to wait before placing the chat on-hold (in seconds), provided the agent has sent the last message", "Livechat_message_character_limit": "Livechat message character limit", "Livechat_monitors": "Livechat monitors", "Livechat_Monitors": "Monitors", diff --git a/server/modules/watchers/publishFields.ts b/server/modules/watchers/publishFields.ts index 21aeb67d859b7..4b05d89372e7d 100644 --- a/server/modules/watchers/publishFields.ts +++ b/server/modules/watchers/publishFields.ts @@ -65,7 +65,6 @@ export const roomFields = { prid: 1, avatarETag: 1, usersCount: 1, - canPlaceChatOnHold: 1, // @TODO create an API to register this fields based on room type livechatData: 1, From f38eb4e6e131454a592c1b5cfe68db9ac92dfb77 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Fri, 19 Mar 2021 10:25:53 +0530 Subject: [PATCH 17/30] Apply suggestions from code review --- app/livechat/client/views/app/tabbar/visitorInfo.js | 2 +- ee/app/livechat-enterprise/server/api/index.js | 2 +- .../server/api/{on-hold.js => rooms.js} | 6 +++--- .../server/lib/AutoCloseOnHoldScheduler.ts | 2 +- ee/app/livechat-enterprise/server/settings.js | 5 ++--- packages/rocketchat-i18n/i18n/en.i18n.json | 7 ++++--- server/startup/migrations/index.js | 1 + 7 files changed, 13 insertions(+), 12 deletions(-) rename ee/app/livechat-enterprise/server/api/{on-hold.js => rooms.js} (90%) diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 2b42c6ea04343..a5930cb33eef6 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -342,7 +342,7 @@ Template.visitorInfo.events({ confirmButtonText: t('Yes'), }, async () => { - const { success } = await APIClient.v1.get(`livechat/placeChatOnHold?roomId=${ this.rid }`); + const { success } = await APIClient.v1.post('livechat/room.onHold', { roomId: this.rid }); if (success) { modal.open({ title: t('Chat_On_Hold'), diff --git a/ee/app/livechat-enterprise/server/api/index.js b/ee/app/livechat-enterprise/server/api/index.js index a96ccb73b2488..1906206a69b1b 100644 --- a/ee/app/livechat-enterprise/server/api/index.js +++ b/ee/app/livechat-enterprise/server/api/index.js @@ -6,4 +6,4 @@ import './priorities'; import './tags'; import './units'; import './business-hours'; -import './on-hold'; +import './rooms'; diff --git a/ee/app/livechat-enterprise/server/api/on-hold.js b/ee/app/livechat-enterprise/server/api/rooms.js similarity index 90% rename from ee/app/livechat-enterprise/server/api/on-hold.js rename to ee/app/livechat-enterprise/server/api/rooms.js index f68328dc2c8bb..4cec68f48ddb4 100644 --- a/ee/app/livechat-enterprise/server/api/on-hold.js +++ b/ee/app/livechat-enterprise/server/api/rooms.js @@ -6,9 +6,9 @@ import { Subscriptions, LivechatRooms } from '../../../../../app/models/server'; import { LivechatEnterprise } from '../lib/LivechatEnterprise'; -API.v1.addRoute('livechat/placeChatOnHold', { authRequired: true }, { - get() { - const { roomId } = this.queryParams; +API.v1.addRoute('livechat/room.onHold', { authRequired: true }, { + post() { + const { roomId } = this.bodyParams; if (!roomId || roomId.trim() === '') { return API.v1.failure('Invalid room Id'); } diff --git a/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts b/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts index 136fde4cdeb9e..9042f96b8e17c 100644 --- a/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts +++ b/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts @@ -50,7 +50,7 @@ class AutoCloseOnHoldSchedulerClass { const payload = { user: schedulerUser, - room: await LivechatRooms.findOneById(roomId), + room: LivechatRooms.findOneById(roomId), comment, options: {}, visitor: undefined, diff --git a/ee/app/livechat-enterprise/server/settings.js b/ee/app/livechat-enterprise/server/settings.js index c8674caabc3b8..4963cc256981a 100644 --- a/ee/app/livechat-enterprise/server/settings.js +++ b/ee/app/livechat-enterprise/server/settings.js @@ -128,7 +128,7 @@ export const createSettings = () => { ], }); - settings.add('Livechat_auto_close_on_hold_chats_timeout', 60, { + settings.add('Livechat_auto_close_on_hold_chats_timeout', 3600, { type: 'int', group: 'Omnichannel', section: 'Sessions', @@ -151,12 +151,11 @@ export const createSettings = () => { ], }); - settings.add('Livechat_allow_manual_on_hold', true, { + settings.add('Livechat_allow_manual_on_hold', false, { type: 'boolean', group: 'Omnichannel', section: 'Sessions', enterprise: true, - value: true, invalidValue: false, public: true, modules: [ diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 71734f7a7af5d..2260e46dcdd36 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -728,7 +728,6 @@ "Chat_queued": "Chat Queued", "Chat_removed": "Chat Removed", "Chat_resumed": "Chat Resumed", - "Chat_resumed_successfully": "Chat Resumed Successfully from On-Hold", "Chat_start": "Chat Start", "Chat_started": "Chat started", "Chat_taken": "Chat Taken", @@ -2413,8 +2412,10 @@ "Livechat_allow_manual_on_hold_description": "If enabled, the agent will get a new option to place a chat On Hold, provided the agent has sent the last message", "Livechat_AllowedDomainsList": "Livechat Allowed Domains", "Livechat_Appearance": "Livechat Appearance", - "Livechat_auto_close_on_hold_chats_custom_message": "Custom Message to be sent when a room in On-Hold queue gets automatically closed by the system", - "Livechat_auto_close_on_hold_chats_timeout": "How long the chat will remain in the On Hold queue until it's automatically closed by the system (in seconds) ?", + "Livechat_auto_close_on_hold_chats_custom_message": "Custom message for closed chats in On Hold queue", + "Livechat_auto_close_on_hold_chats_custom_message_description": "Custom Message to be sent when a room in On-Hold queue gets automatically closed by the system", + "Livechat_auto_close_on_hold_chats_timeout": "How long to wait before closing a chat in On Hold Queue ?", + "Livechat_auto_close_on_hold_chats_timeout_description": "Define how long the chat will remain in the On Hold queue until it's automatically closed by the system. Time in seconds", "Livechat_auto_transfer_chat_timeout": "Timeout (in seconds) for automatic transfer of unanswered chats to another agent", "Livechat_auto_transfer_chat_timeout_description": "This event takes place only when the chat has just started. After the first transfering for inactivity, the room is no longer monitored.", "Livechat_business_hour_type": "Business Hour Type (Single or Multiple)", diff --git a/server/startup/migrations/index.js b/server/startup/migrations/index.js index e4e5470f4846d..b36ce20ef6b98 100644 --- a/server/startup/migrations/index.js +++ b/server/startup/migrations/index.js @@ -215,4 +215,5 @@ import './v215'; import './v216'; import './v217'; import './v218'; +import './v219'; import './xrun'; From bf2d641fa478c1bc1267361077877d392ab7d409 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Fri, 19 Mar 2021 11:19:20 +0530 Subject: [PATCH 18/30] Move resume On-Hold chat logic inside Queue Manager --- .../livechat-enterprise/lib/QueueManager.js | 42 +++++++++++++++++++ .../server/methods/resumeOnHold.ts | 39 ++--------------- 2 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 ee/app/livechat-enterprise/lib/QueueManager.js diff --git a/ee/app/livechat-enterprise/lib/QueueManager.js b/ee/app/livechat-enterprise/lib/QueueManager.js new file mode 100644 index 0000000000000..f373ba3d788f2 --- /dev/null +++ b/ee/app/livechat-enterprise/lib/QueueManager.js @@ -0,0 +1,42 @@ +import { Meteor } from 'meteor/meteor'; + +import { Subscriptions, LivechatRooms, LivechatInquiry } from '../../../../app/models/server'; +import { callbacks } from '../../../../app/callbacks/server'; +import { RoutingManager } from '../../../../app/livechat/server/lib/RoutingManager'; +import { Livechat } from '../../../../app/livechat/server'; +import { dispatchInquiryPosition } from '../server/lib/Helper'; + +export const QueueManager = { + resumeOnHoldChat(room, options) { + const { _id: roomId, servedBy: { _id: agentId = null, username = null } = {} } = room; + let agent = { agentId, username }; + const inquiry = LivechatInquiry.findOneByRoomId(roomId, {}); + const { departmentId } = inquiry; + + try { + agent = Promise.await(callbacks.run('livechat.checkAgentBeforeTakeInquiry', agent, inquiry)); + } catch (e) { + console.log(e); + if (options.clientAction) { + throw new Meteor.Error('error-max-number-simultaneous-chats-reached', 'Not allowed'); + } + agent = null; + } + + if (!agent) { + Livechat.returnRoomAsInquiry(room._id, departmentId); + + if (RoutingManager.getConfig().autoAssignAgent) { + LivechatInquiry.queueInquiry(inquiry._id); + + const [inq] = LivechatInquiry.getCurrentSortedQueueAsync({ _id: inquiry._id, department: undefined }); + if (inq) { + dispatchInquiryPosition(inq); + } + } + } + + LivechatRooms.unsetAllOnHoldFieldsByRoomId(roomId); + Subscriptions.unsetOnHold(roomId); + }, +}; diff --git a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index dafb771244f44..a427a05adfddc 100644 --- a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -1,11 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import { Subscriptions, LivechatRooms, LivechatInquiry } from '../../../../../app/models/server'; -import { callbacks } from '../../../../../app/callbacks/server'; -import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; -import { Livechat } from '../../../../../app/livechat/server'; -import { dispatchInquiryPosition } from '../lib/Helper'; - +import { LivechatRooms } from '../../../../../app/models/server'; +import { QueueManager } from '../../lib/QueueManager'; Meteor.methods({ async 'livechat:resumeOnHold'(roomId, options = { clientAction: false }) { @@ -18,35 +14,6 @@ Meteor.methods({ throw new Meteor.Error('room-closed', 'Room is not OnHold', { method: 'livechat:resumeOnHold' }); } - const { servedBy: { _id: agentId = null, username = null } = {} } = room; - let agent: any = { agentId, username }; - const inquiry = await LivechatInquiry.findOneByRoomId(roomId, {}); - const { departmentId } = inquiry; - - try { - agent = await callbacks.run('livechat.checkAgentBeforeTakeInquiry', agent, inquiry); - } catch (e) { - console.log(e); - if (options.clientAction) { - throw new Meteor.Error('error-max-number-simultaneous-chats-reached', 'Not allowed'); - } - agent = null; - } - - if (!agent) { - Livechat.returnRoomAsInquiry(room._id, departmentId); - - if (RoutingManager.getConfig().autoAssignAgent) { - LivechatInquiry.queueInquiry(inquiry._id); - - const [inq] = await LivechatInquiry.getCurrentSortedQueueAsync({ _id: inquiry._id, department: undefined }); - if (inq) { - dispatchInquiryPosition(inq); - } - } - } - - (LivechatRooms as any).unsetAllOnHoldFieldsByRoomId(roomId); - Subscriptions.unsetOnHold(roomId); + (QueueManager as any).resumeOnHoldChat(room, options); }, }); From 077161d9bc0b1316b69ce8af9b3667e10a7d7266 Mon Sep 17 00:00:00 2001 From: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Date: Fri, 19 Mar 2021 19:25:53 +0530 Subject: [PATCH 19/30] Apply suggestions from code review Co-authored-by: Renato Becker --- packages/rocketchat-i18n/i18n/en.i18n.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 2260e46dcdd36..db029d175442c 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2420,7 +2420,7 @@ "Livechat_auto_transfer_chat_timeout_description": "This event takes place only when the chat has just started. After the first transfering for inactivity, the room is no longer monitored.", "Livechat_business_hour_type": "Business Hour Type (Single or Multiple)", "Livechat_chat_transcript_sent": "Chat transcript sent: __transcript__", - "Livechat_close_chat": "Close the abandoned chat", + "Livechat_close_chat": "Close chat", "Livechat_custom_fields_options_placeholder": "Comma-separated list used to select a pre-configured value. Spaces between elements are not accepted.", "Livechat_custom_fields_public_description": "Public custom fields will be displayed in external applications, such as Livechat, etc.", "Livechat_Dashboard": "Omnichannel Dashboard", @@ -2445,7 +2445,7 @@ "Livechat_offline": "Omnichannel offline", "Livechat_offline_message_sent": "Livechat offline message sent", "Livechat_OfflineMessageToChannel_enabled": "Send Livechat offline messages to a channel", - "Livechat_onHold_Chat": "Place the abandoned chat On-Hold", + "Livechat_onHold_Chat": "Place chat On-Hold", "Livechat_online": "Omnichannel on-line", "Livechat_Queue": "Omnichannel Queue", "Livechat_registration_form": "Registration Form", From 8e3e713567942aafe46ea1e3c40a6b8e5fe1f7e3 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Fri, 19 Mar 2021 20:46:34 +0530 Subject: [PATCH 20/30] Apply suggestions from code review --- app/livechat/client/views/app/tabbar/visitorInfo.js | 2 +- .../livechat-enterprise/server/hooks/resumeOnHold.ts | 2 -- .../server/lib/LivechatEnterprise.js | 8 +++++--- .../server/lib/VisitorInactivityMonitor.js | 3 +-- .../server/methods/resumeOnHold.ts | 3 +++ server/startup/migrations/v219.js | 11 +++++------ 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index a5930cb33eef6..77715e01c5cf1 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -208,7 +208,7 @@ Template.visitorInfo.helpers({ canPlaceChatOnHold() { const room = Template.instance().room.get(); - return room.open && !room.onHold && settings.get('Livechat_allow_manual_on_hold') && room.lastMessage && !room.lastMessage?.token; + return room.open && !room.onHold && room.lastMessage && !room.lastMessage?.token && settings.get('Livechat_allow_manual_on_hold'); }, roomClosedDateTime() { diff --git a/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts b/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts index 247b010cf918e..9ea8aab39fa42 100644 --- a/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../app/callbacks/server'; -import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promise => { const { _id: rid, onHold } = room; @@ -24,7 +23,6 @@ const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promis // if a visitor sends a message in room which is On Hold if (message.token && onHold) { - await AutoCloseOnHoldScheduler.unscheduleRoom(rid); await Meteor.call('livechat:resumeOnHold', room._id, { clientAction: false }); } diff --git a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index 17521d9e8d611..0ccc1ffed8a75 100644 --- a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -167,9 +167,9 @@ export const LivechatEnterprise = { }, placeRoomOnHold(room) { - const { _id: roomId } = room; - if (!roomId) { - return; + const { _id: roomId, onHold } = room; + if (!roomId || onHold) { + return false; } LivechatRooms.setOnHold(roomId); Subscriptions.setOnHold(roomId); @@ -177,6 +177,8 @@ export const LivechatEnterprise = { Meteor.defer(() => { callbacks.run('livechat:afterOnHold', room); }); + + return true; }, }; diff --git a/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js b/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js index 2651951706f69..3f32e99126cdc 100644 --- a/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js +++ b/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js @@ -79,8 +79,7 @@ export class VisitorInactivityMonitor { } placeRoomOnHold(room) { - LivechatEnterprise.placeRoomOnHold(room); - LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id); + LivechatEnterprise.placeRoomOnHold(room) && LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id); } handleAbandonedRooms() { diff --git a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index a427a05adfddc..fe747a7554104 100644 --- a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { LivechatRooms } from '../../../../../app/models/server'; import { QueueManager } from '../../lib/QueueManager'; +import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; Meteor.methods({ async 'livechat:resumeOnHold'(roomId, options = { clientAction: false }) { @@ -14,6 +15,8 @@ Meteor.methods({ throw new Meteor.Error('room-closed', 'Room is not OnHold', { method: 'livechat:resumeOnHold' }); } + await AutoCloseOnHoldScheduler.unscheduleRoom(room._id); + (QueueManager as any).resumeOnHoldChat(room, options); }, }); diff --git a/server/startup/migrations/v219.js b/server/startup/migrations/v219.js index 3982711420485..b155c086d2552 100644 --- a/server/startup/migrations/v219.js +++ b/server/startup/migrations/v219.js @@ -4,12 +4,11 @@ import { Settings } from '../../../app/models/server'; Migrations.add({ version: 219, up() { - const SettingIds = [ - { - old: 'Livechat_auto_close_abandoned_rooms', - new: 'Livechat_abandoned_rooms_action', - }, - ]; + const SettingIds = { + old: 'Livechat_auto_close_abandoned_rooms', + new: 'Livechat_abandoned_rooms_action', + }; + const oldSetting = Settings.findOne({ _id: SettingIds.old }); if (!oldSetting) { From 5ca9cba17eccd5df0df93ae81d36a09733f36df3 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Sat, 20 Mar 2021 23:03:03 +0530 Subject: [PATCH 21/30] Use takeInquiry() to resume On-Hold chats --- .../client/views/app/livechatReadOnly.js | 4 +- app/livechat/server/lib/QueueManager.js | 2 +- app/livechat/server/lib/RoutingManager.js | 13 ++++-- app/livechat/server/methods/takeInquiry.js | 4 +- app/models/server/models/LivechatInquiry.js | 13 ++++++ .../livechat-enterprise/lib/QueueManager.js | 42 ------------------- .../hooks/checkAgentBeforeTakeInquiry.js | 6 +-- .../livechat-enterprise/server/hooks/index.js | 1 + .../server/hooks/onAgentAssignmentFailed.ts | 30 +++++++++++++ .../server/methods/resumeOnHold.ts | 18 ++++++-- 10 files changed, 74 insertions(+), 59 deletions(-) delete mode 100644 ee/app/livechat-enterprise/lib/QueueManager.js create mode 100644 ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts diff --git a/app/livechat/client/views/app/livechatReadOnly.js b/app/livechat/client/views/app/livechatReadOnly.js index ac397d5c4159a..fcfaed1111298 100644 --- a/app/livechat/client/views/app/livechatReadOnly.js +++ b/app/livechat/client/views/app/livechatReadOnly.js @@ -22,7 +22,7 @@ Template.livechatReadOnly.helpers({ showPreview() { const config = Template.instance().routingConfig.get(); - return config.previewRoom; + return config.previewRoom || Template.currentData().onHold; }, isPreparing() { @@ -41,7 +41,7 @@ Template.livechatReadOnly.events({ const inquiry = instance.inquiry.get(); const { _id } = inquiry; - await call('livechat:takeInquiry', _id); + await call('livechat:takeInquiry', _id, { clientAction: true }); instance.loadInquiry(inquiry.rid); }, diff --git a/app/livechat/server/lib/QueueManager.js b/app/livechat/server/lib/QueueManager.js index febd9cbf59ba5..37a5805f12d81 100644 --- a/app/livechat/server/lib/QueueManager.js +++ b/app/livechat/server/lib/QueueManager.js @@ -7,7 +7,7 @@ import { callbacks } from '../../../callbacks/server'; import { RoutingManager } from './RoutingManager'; -const queueInquiry = async (room, inquiry, defaultAgent) => { +export const queueInquiry = async (room, inquiry, defaultAgent) => { const inquiryAgent = RoutingManager.delegateAgent(defaultAgent, inquiry); await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent); inquiry = LivechatInquiry.findOneById(inquiry._id); diff --git a/app/livechat/server/lib/RoutingManager.js b/app/livechat/server/lib/RoutingManager.js index 49aec01b625a1..9f147539d4637 100644 --- a/app/livechat/server/lib/RoutingManager.js +++ b/app/livechat/server/lib/RoutingManager.js @@ -13,7 +13,7 @@ import { allowAgentSkipQueue, } from './Helper'; import { callbacks } from '../../../callbacks/server'; -import { LivechatRooms, Rooms, Messages, Users, LivechatInquiry } from '../../../models/server'; +import { LivechatRooms, Rooms, Messages, Users, LivechatInquiry, Subscriptions } from '../../../models/server'; import { Apps, AppEvents } from '../../../apps/server'; export const RoutingManager = { @@ -110,7 +110,7 @@ export const RoutingManager = { return true; }, - async takeInquiry(inquiry, agent) { + async takeInquiry(inquiry, agent, options = { clientAction: false }) { check(agent, Match.ObjectIncluding({ agentId: String, username: String, @@ -128,15 +128,20 @@ export const RoutingManager = { return room; } - if (room.servedBy && room.servedBy._id === agent.agentId) { + if (room.servedBy && room.servedBy._id === agent.agentId && !room.onHold) { return room; } - agent = await callbacks.run('livechat.checkAgentBeforeTakeInquiry', agent, inquiry); + agent = await callbacks.run('livechat.checkAgentBeforeTakeInquiry', { agent, inquiry, options }); if (!agent) { + await callbacks.run('livechat.onAgentAssignmentFailed', { inquiry, room, options }); return null; } + if (room.onHold) { + Subscriptions.removeByRoomIdAndUserId(room._id, agent.agentId); + } + LivechatInquiry.takeInquiry(_id); const inq = this.assignAgent(inquiry, agent); diff --git a/app/livechat/server/methods/takeInquiry.js b/app/livechat/server/methods/takeInquiry.js index abb006593ef8f..b367467d01348 100644 --- a/app/livechat/server/methods/takeInquiry.js +++ b/app/livechat/server/methods/takeInquiry.js @@ -6,7 +6,7 @@ import { RoutingManager } from '../lib/RoutingManager'; import { userCanTakeInquiry } from '../lib/Helper'; Meteor.methods({ - 'livechat:takeInquiry'(inquiryId) { + 'livechat:takeInquiry'(inquiryId, options) { if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-l-room')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:takeInquiry' }); } @@ -27,6 +27,6 @@ Meteor.methods({ username: user.username, }; - return RoutingManager.takeInquiry(inquiry, agent); + return RoutingManager.takeInquiry(inquiry, agent, options); }, }); diff --git a/app/models/server/models/LivechatInquiry.js b/app/models/server/models/LivechatInquiry.js index 8d2aef5ec4f5c..7b79c53226182 100644 --- a/app/models/server/models/LivechatInquiry.js +++ b/app/models/server/models/LivechatInquiry.js @@ -73,6 +73,19 @@ export class LivechatInquiry extends Base { }); } + /* + * mark inquiry as ready + */ + readyInquiry(inquiryId) { + return this.update({ + _id: inquiryId, + }, { + $set: { + status: 'ready', + }, + }); + } + changeDepartmentIdByRoomId(rid, department) { const query = { rid, diff --git a/ee/app/livechat-enterprise/lib/QueueManager.js b/ee/app/livechat-enterprise/lib/QueueManager.js deleted file mode 100644 index f373ba3d788f2..0000000000000 --- a/ee/app/livechat-enterprise/lib/QueueManager.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Subscriptions, LivechatRooms, LivechatInquiry } from '../../../../app/models/server'; -import { callbacks } from '../../../../app/callbacks/server'; -import { RoutingManager } from '../../../../app/livechat/server/lib/RoutingManager'; -import { Livechat } from '../../../../app/livechat/server'; -import { dispatchInquiryPosition } from '../server/lib/Helper'; - -export const QueueManager = { - resumeOnHoldChat(room, options) { - const { _id: roomId, servedBy: { _id: agentId = null, username = null } = {} } = room; - let agent = { agentId, username }; - const inquiry = LivechatInquiry.findOneByRoomId(roomId, {}); - const { departmentId } = inquiry; - - try { - agent = Promise.await(callbacks.run('livechat.checkAgentBeforeTakeInquiry', agent, inquiry)); - } catch (e) { - console.log(e); - if (options.clientAction) { - throw new Meteor.Error('error-max-number-simultaneous-chats-reached', 'Not allowed'); - } - agent = null; - } - - if (!agent) { - Livechat.returnRoomAsInquiry(room._id, departmentId); - - if (RoutingManager.getConfig().autoAssignAgent) { - LivechatInquiry.queueInquiry(inquiry._id); - - const [inq] = LivechatInquiry.getCurrentSortedQueueAsync({ _id: inquiry._id, department: undefined }); - if (inq) { - dispatchInquiryPosition(inq); - } - } - } - - LivechatRooms.unsetAllOnHoldFieldsByRoomId(roomId); - Subscriptions.unsetOnHold(roomId); - }, -}; diff --git a/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js b/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js index 3dcb33c08d1de..09a192c357a27 100644 --- a/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js +++ b/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.js @@ -4,10 +4,9 @@ import { callbacks } from '../../../../../app/callbacks'; import { Users } from '../../../../../app/models/server/raw'; import { settings } from '../../../../../app/settings'; import { getMaxNumberSimultaneousChat } from '../lib/Helper'; -import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { allowAgentSkipQueue } from '../../../../../app/livechat/server/lib/Helper'; -callbacks.add('livechat.checkAgentBeforeTakeInquiry', async (agent, inquiry) => { +callbacks.add('livechat.checkAgentBeforeTakeInquiry', async ({ agent, inquiry, options }) => { if (!settings.get('Livechat_waiting_queue')) { return agent; } @@ -36,8 +35,7 @@ callbacks.add('livechat.checkAgentBeforeTakeInquiry', async (agent, inquiry) => const { queueInfo: { chats = 0 } = {} } = user; if (maxNumberSimultaneousChat <= chats) { callbacks.run('livechat.onMaxNumberSimultaneousChatsReached', inquiry); - - if (!RoutingManager.getConfig().autoAssignAgent) { + if (options.clientAction) { throw new Meteor.Error('error-max-number-simultaneous-chats-reached', 'Not allowed'); } diff --git a/ee/app/livechat-enterprise/server/hooks/index.js b/ee/app/livechat-enterprise/server/hooks/index.js index 5489b3b065884..d7ace549d8d66 100644 --- a/ee/app/livechat-enterprise/server/hooks/index.js +++ b/ee/app/livechat-enterprise/server/hooks/index.js @@ -15,3 +15,4 @@ import './onLoadConfigApi'; import './onCloseLivechat'; import './onSaveVisitorInfo'; import './onBusinessHourStart'; +import './onAgentAssignmentFailed'; diff --git a/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts b/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts new file mode 100644 index 0000000000000..7493367199f3f --- /dev/null +++ b/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts @@ -0,0 +1,30 @@ +import { callbacks } from '../../../../../app/callbacks/server'; +import { LivechatInquiry, Subscriptions, LivechatRooms } from '../../../../../app/models/server'; +import { queueInquiry } from '../../../../../app/livechat/server/lib/QueueManager'; + +const handleOnAgentAssignmentFailed = async ({ inquiry, room }: { inquiry: any; room: any }): Promise => { + if (!inquiry || !room) { + return; + } + + const { onHold, _id: roomId, servedBy } = room; + if (!onHold) { + return; + } + + const defaultAgent = { agentId: servedBy._id, username: servedBy._id }; + + const { _id: inquiryId } = inquiry; + LivechatInquiry.readyInquiry(inquiryId); + LivechatInquiry.removeDefaultAgentById(inquiryId); + LivechatRooms.removeAgentByRoomId(roomId); + if (defaultAgent.agentId) { + Subscriptions.removeByRoomIdAndUserId(roomId, defaultAgent.agentId); + } + + const newInquiry = LivechatInquiry.findOneById(inquiryId); + + await queueInquiry(room, newInquiry, defaultAgent); +}; + +callbacks.add('livechat.onAgentAssignmentFailed', handleOnAgentAssignmentFailed, callbacks.priority.HIGH, 'livechat-agent-assignment-failed'); diff --git a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index fe747a7554104..1ac1795b93b9e 100644 --- a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; -import { LivechatRooms } from '../../../../../app/models/server'; -import { QueueManager } from '../../lib/QueueManager'; +import { LivechatRooms, LivechatInquiry, Subscriptions } from '../../../../../app/models/server'; import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; +import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; Meteor.methods({ async 'livechat:resumeOnHold'(roomId, options = { clientAction: false }) { @@ -15,8 +15,18 @@ Meteor.methods({ throw new Meteor.Error('room-closed', 'Room is not OnHold', { method: 'livechat:resumeOnHold' }); } - await AutoCloseOnHoldScheduler.unscheduleRoom(room._id); + const { servedBy: { _id: agentId, username } } = room; - (QueueManager as any).resumeOnHoldChat(room, options); + const inquiry = LivechatInquiry.findOneByRoomId(roomId); + if (!inquiry) { + throw new Meteor.Error('inquiry-not-found', 'Error! No inquiry found for this room', { method: 'livechat:resumeOnHold' }); + } + + await AutoCloseOnHoldScheduler.unscheduleRoom(roomId); + + await RoutingManager.takeInquiry(inquiry, { agentId, username }, options); + + (LivechatRooms as any).unsetAllOnHoldFieldsByRoomId(roomId); + Subscriptions.unsetOnHold(roomId); }, }); From 966dd0d2f81cccac50eead01235509844ef1ac2d Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Sat, 20 Mar 2021 23:46:43 +0530 Subject: [PATCH 22/30] Fix failing test case --- ee/app/livechat-enterprise/server/methods/resumeOnHold.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index 1ac1795b93b9e..0323928ad78f3 100644 --- a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -17,7 +17,7 @@ Meteor.methods({ const { servedBy: { _id: agentId, username } } = room; - const inquiry = LivechatInquiry.findOneByRoomId(roomId); + const inquiry = LivechatInquiry.findOneByRoomId(roomId, {}); if (!inquiry) { throw new Meteor.Error('inquiry-not-found', 'Error! No inquiry found for this room', { method: 'livechat:resumeOnHold' }); } From 29830874a0c4bd908f88da9bf96ac0bd5525c868 Mon Sep 17 00:00:00 2001 From: Renato Becker Date: Sat, 20 Mar 2021 18:14:18 -0300 Subject: [PATCH 23/30] Minor improvements. --- .../server/hooks/onAgentAssignmentFailed.ts | 15 +++++---------- packages/rocketchat-i18n/i18n/en.i18n.json | 8 ++++---- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts b/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts index 7493367199f3f..b8592fff228d7 100644 --- a/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts +++ b/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts @@ -3,28 +3,23 @@ import { LivechatInquiry, Subscriptions, LivechatRooms } from '../../../../../ap import { queueInquiry } from '../../../../../app/livechat/server/lib/QueueManager'; const handleOnAgentAssignmentFailed = async ({ inquiry, room }: { inquiry: any; room: any }): Promise => { - if (!inquiry || !room) { + if (!inquiry || !room || !room.onHold) { return; } - const { onHold, _id: roomId, servedBy } = room; - if (!onHold) { - return; - } - - const defaultAgent = { agentId: servedBy._id, username: servedBy._id }; + const { _id: roomId, servedBy } = room; const { _id: inquiryId } = inquiry; LivechatInquiry.readyInquiry(inquiryId); LivechatInquiry.removeDefaultAgentById(inquiryId); LivechatRooms.removeAgentByRoomId(roomId); - if (defaultAgent.agentId) { - Subscriptions.removeByRoomIdAndUserId(roomId, defaultAgent.agentId); + if (servedBy?._id) { + Subscriptions.removeByRoomIdAndUserId(roomId, servedBy._id); } const newInquiry = LivechatInquiry.findOneById(inquiryId); - await queueInquiry(room, newInquiry, defaultAgent); + await queueInquiry(room, newInquiry); }; callbacks.add('livechat.onAgentAssignmentFailed', handleOnAgentAssignmentFailed, callbacks.priority.HIGH, 'livechat-agent-assignment-failed'); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 83af0a9307736..7ae676016e10f 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2410,15 +2410,15 @@ "Livechat_agents": "Omnichannel agents", "Livechat_Agents": "Agents", "Livechat_allow_manual_on_hold": "Allow agents to manually place chat On Hold", - "Livechat_allow_manual_on_hold_description": "If enabled, the agent will get a new option to place a chat On Hold, provided the agent has sent the last message", + "Livechat_allow_manual_on_hold_Description": "If enabled, the agent will get a new option to place a chat On Hold, provided the agent has sent the last message", "Livechat_AllowedDomainsList": "Livechat Allowed Domains", "Livechat_Appearance": "Livechat Appearance", "Livechat_auto_close_on_hold_chats_custom_message": "Custom message for closed chats in On Hold queue", - "Livechat_auto_close_on_hold_chats_custom_message_description": "Custom Message to be sent when a room in On-Hold queue gets automatically closed by the system", + "Livechat_auto_close_on_hold_chats_custom_message_Description": "Custom Message to be sent when a room in On-Hold queue gets automatically closed by the system", "Livechat_auto_close_on_hold_chats_timeout": "How long to wait before closing a chat in On Hold Queue ?", - "Livechat_auto_close_on_hold_chats_timeout_description": "Define how long the chat will remain in the On Hold queue until it's automatically closed by the system. Time in seconds", + "Livechat_auto_close_on_hold_chats_timeout_Description": "Define how long the chat will remain in the On Hold queue until it's automatically closed by the system. Time in seconds", "Livechat_auto_transfer_chat_timeout": "Timeout (in seconds) for automatic transfer of unanswered chats to another agent", - "Livechat_auto_transfer_chat_timeout_description": "This event takes place only when the chat has just started. After the first transfering for inactivity, the room is no longer monitored.", + "Livechat_auto_transfer_chat_timeout_Description": "This event takes place only when the chat has just started. After the first transfering for inactivity, the room is no longer monitored.", "Livechat_business_hour_type": "Business Hour Type (Single or Multiple)", "Livechat_chat_transcript_sent": "Chat transcript sent: __transcript__", "Livechat_close_chat": "Close chat", From 85dbe77718cadfee11ddec93bd2ef8c7f70b977f Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Mon, 22 Mar 2021 15:40:52 +0530 Subject: [PATCH 24/30] [Regression] Omnichannel On Hold feature - handle following impacted events - Returning chat to the queue - Forwarding chats - Closing chats --- .../client/views/app/tabbar/visitorInfo.js | 2 +- app/livechat/server/lib/Helper.js | 4 ++-- app/livechat/server/lib/Livechat.js | 2 ++ app/livechat/server/lib/RoutingManager.js | 10 ++++++---- .../server/hooks/afterForwardChatToAgent.ts | 17 +++++++++++++++++ .../hooks/afterForwardChatToDepartment.js | 3 +++ .../server/hooks/afterOnHoldChatResumed.ts | 15 +++++++++++++++ .../server/hooks/afterReturnedRoomAsInquiry.ts | 10 ++++++++++ .../livechat-enterprise/server/hooks/index.js | 3 +++ .../server/hooks/onCloseLivechat.js | 2 ++ .../server/methods/resumeOnHold.ts | 10 ++++------ 11 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 ee/app/livechat-enterprise/server/hooks/afterForwardChatToAgent.ts create mode 100644 ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts create mode 100644 ee/app/livechat-enterprise/server/hooks/afterReturnedRoomAsInquiry.ts diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 77715e01c5cf1..56de7d9275c0b 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -208,7 +208,7 @@ Template.visitorInfo.helpers({ canPlaceChatOnHold() { const room = Template.instance().room.get(); - return room.open && !room.onHold && room.lastMessage && !room.lastMessage?.token && settings.get('Livechat_allow_manual_on_hold'); + return room.open && !room.onHold && room.servedBy && room.lastMessage && !room.lastMessage?.token && settings.get('Livechat_allow_manual_on_hold'); }, roomClosedDateTime() { diff --git a/app/livechat/server/lib/Helper.js b/app/livechat/server/lib/Helper.js index f7a321a25e953..7087edca3d46e 100644 --- a/app/livechat/server/lib/Helper.js +++ b/app/livechat/server/lib/Helper.js @@ -293,7 +293,7 @@ export const forwardRoomToAgent = async (room, transferData) => { const agent = { agentId, username }; // There are some Enterprise features that may interrupt the fowarding process // Due to that we need to check whether the agent has been changed or not - const roomTaken = await RoutingManager.takeInquiry(inquiry, agent); + const roomTaken = await RoutingManager.takeInquiry(inquiry, agent, { isForwarding: true }); if (!roomTaken) { return false; } @@ -378,7 +378,7 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => { // Fake the department to forward the inquiry - Case the forward process does not success // the inquiry will stay in the same original department inquiry.department = departmentId; - const roomTaken = await RoutingManager.delegateInquiry(inquiry, agent); + const roomTaken = await RoutingManager.delegateInquiry(inquiry, agent, { isForwarding: true }); if (!roomTaken) { return false; } diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index 31a7edcb361dd..aefe57d082ec6 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -631,6 +631,8 @@ export const Livechat = { throw new Meteor.Error('error-returning-inquiry', 'Error returning inquiry to the queue', { method: 'livechat:returnRoomAsInquiry' }); } + callbacks.run('livechat:afterReturnedRoomAsInquiry', room); + return true; }, diff --git a/app/livechat/server/lib/RoutingManager.js b/app/livechat/server/lib/RoutingManager.js index 9f147539d4637..9b1ce4d2d3f1f 100644 --- a/app/livechat/server/lib/RoutingManager.js +++ b/app/livechat/server/lib/RoutingManager.js @@ -43,7 +43,7 @@ export const RoutingManager = { return this.getMethod().getNextAgent(department, ignoreAgentId); }, - async delegateInquiry(inquiry, agent) { + async delegateInquiry(inquiry, agent = null, options = {}) { const { department, rid } = inquiry; if (!agent || (agent.username && !Users.findOneOnlineAgentByUsername(agent.username) && !allowAgentSkipQueue(agent))) { agent = await this.getNextAgent(department); @@ -53,7 +53,7 @@ export const RoutingManager = { return LivechatRooms.findOneById(rid); } - return this.takeInquiry(inquiry, agent); + return this.takeInquiry(inquiry, agent, options); }, assignAgent(inquiry, agent) { @@ -110,7 +110,7 @@ export const RoutingManager = { return true; }, - async takeInquiry(inquiry, agent, options = { clientAction: false }) { + async takeInquiry(inquiry, agent, options = { clientAction: false, isForwarding: false }) { check(agent, Match.ObjectIncluding({ agentId: String, username: String, @@ -134,7 +134,9 @@ export const RoutingManager = { agent = await callbacks.run('livechat.checkAgentBeforeTakeInquiry', { agent, inquiry, options }); if (!agent) { - await callbacks.run('livechat.onAgentAssignmentFailed', { inquiry, room, options }); + if (!options.isForwarding) { + await callbacks.run('livechat.onAgentAssignmentFailed', { inquiry, room, options }); + } return null; } diff --git a/ee/app/livechat-enterprise/server/hooks/afterForwardChatToAgent.ts b/ee/app/livechat-enterprise/server/hooks/afterForwardChatToAgent.ts new file mode 100644 index 0000000000000..da18c51be8cfd --- /dev/null +++ b/ee/app/livechat-enterprise/server/hooks/afterForwardChatToAgent.ts @@ -0,0 +1,17 @@ +import { callbacks } from '../../../../../app/callbacks/server'; +import { LivechatRooms } from '../../../../../app/models/server'; + +const handleAfterForwardChatToAgent = async ({ rid = null }: { rid: any }): Promise => { + if (!rid) { + return; + } + + const room = LivechatRooms.findOneById(rid); + if (!room) { + return; + } + + await callbacks.run('livechat:afterOnHoldChatResumed', room); +}; + +callbacks.add('livechat.afterForwardChatToAgent', handleAfterForwardChatToAgent, callbacks.priority.HIGH, 'livechat-after-forward-chat-to-agent'); diff --git a/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.js b/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.js index ed8b311c8835d..f3570f9868747 100644 --- a/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.js +++ b/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.js @@ -10,6 +10,9 @@ callbacks.add('livechat.afterForwardChatToDepartment', (options) => { if (!room) { return; } + + callbacks.run('livechat:afterOnHoldChatResumed', room); + setPredictedVisitorAbandonmentTime(room); const department = LivechatDepartment.findOneById(newDepartmentId, { fields: { ancestors: 1 } }); diff --git a/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts b/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts new file mode 100644 index 0000000000000..3ee44db73bea0 --- /dev/null +++ b/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts @@ -0,0 +1,15 @@ +import { callbacks } from '../../../../../app/callbacks/server'; +import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; +import { LivechatRooms, Subscriptions } from '../../../../../app/models/server'; + +const handleAfterOnHoldChatResumed = async (room: any): Promise => { + if (!room || !room._id || !room.onHold) { + return; + } + + await AutoCloseOnHoldScheduler.unscheduleRoom(room._id); + (LivechatRooms as any).unsetAllOnHoldFieldsByRoomId(room._id); + Subscriptions.unsetOnHold(room._id); +}; + +callbacks.add('livechat:afterOnHoldChatResumed', handleAfterOnHoldChatResumed, callbacks.priority.HIGH, 'livechat-after-on-hold-chat-resumed'); diff --git a/ee/app/livechat-enterprise/server/hooks/afterReturnedRoomAsInquiry.ts b/ee/app/livechat-enterprise/server/hooks/afterReturnedRoomAsInquiry.ts new file mode 100644 index 0000000000000..cca1730991916 --- /dev/null +++ b/ee/app/livechat-enterprise/server/hooks/afterReturnedRoomAsInquiry.ts @@ -0,0 +1,10 @@ +import { callbacks } from '../../../../../app/callbacks/server'; + +const handleAfterReturnedRoomAsInquiry = async (room: any): Promise => { + if (!room) { + return; + } + await callbacks.run('livechat:afterOnHoldChatResumed', room); +}; + +callbacks.add('livechat:afterReturnedRoomAsInquiry', handleAfterReturnedRoomAsInquiry, callbacks.priority.HIGH, 'livechat-after-return-room-as-inquiry'); diff --git a/ee/app/livechat-enterprise/server/hooks/index.js b/ee/app/livechat-enterprise/server/hooks/index.js index d7ace549d8d66..1187842aee1f0 100644 --- a/ee/app/livechat-enterprise/server/hooks/index.js +++ b/ee/app/livechat-enterprise/server/hooks/index.js @@ -16,3 +16,6 @@ import './onCloseLivechat'; import './onSaveVisitorInfo'; import './onBusinessHourStart'; import './onAgentAssignmentFailed'; +import './afterOnHoldChatResumed'; +import './afterReturnedRoomAsInquiry'; +import './afterForwardChatToAgent'; diff --git a/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js b/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js index c180849ed9759..91cec0ff26592 100644 --- a/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js +++ b/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js @@ -4,6 +4,8 @@ import { dispatchWaitingQueueStatus } from '../lib/Helper'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; const onCloseLivechat = (room) => { + callbacks.run('livechat:afterOnHoldChatResumed', room); + if (!settings.get('Livechat_waiting_queue')) { return room; } diff --git a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index 0323928ad78f3..a4cc185751974 100644 --- a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; -import { LivechatRooms, LivechatInquiry, Subscriptions } from '../../../../../app/models/server'; -import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; +import { LivechatRooms, LivechatInquiry } from '../../../../../app/models/server'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; +import { callbacks } from '../../../../../app/callbacks/server'; Meteor.methods({ async 'livechat:resumeOnHold'(roomId, options = { clientAction: false }) { @@ -22,11 +22,9 @@ Meteor.methods({ throw new Meteor.Error('inquiry-not-found', 'Error! No inquiry found for this room', { method: 'livechat:resumeOnHold' }); } - await AutoCloseOnHoldScheduler.unscheduleRoom(roomId); - await RoutingManager.takeInquiry(inquiry, { agentId, username }, options); - (LivechatRooms as any).unsetAllOnHoldFieldsByRoomId(roomId); - Subscriptions.unsetOnHold(roomId); + const updatedRoom = LivechatRooms.findById(roomId); + updatedRoom && Meteor.defer(() => callbacks.run('livechat:afterOnHoldChatResumed', updatedRoom)); }, }); From 16db56ac83daec39f84a72a376361d83e1aa8561 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Mon, 22 Mar 2021 15:56:45 +0530 Subject: [PATCH 25/30] Fix failing test cases --- ee/app/livechat-enterprise/server/methods/resumeOnHold.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index a4cc185751974..2fd069cccbc65 100644 --- a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -24,7 +24,7 @@ Meteor.methods({ await RoutingManager.takeInquiry(inquiry, { agentId, username }, options); - const updatedRoom = LivechatRooms.findById(roomId); + const updatedRoom = LivechatRooms.findById(roomId, null); updatedRoom && Meteor.defer(() => callbacks.run('livechat:afterOnHoldChatResumed', updatedRoom)); }, }); From c1c634e3255783162bf85ec1a7cf5de7244e48e1 Mon Sep 17 00:00:00 2001 From: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Date: Mon, 22 Mar 2021 17:29:22 +0530 Subject: [PATCH 26/30] Revert "[Regression] Omnichannel On hold Queue" --- .../client/views/app/tabbar/visitorInfo.js | 2 +- app/livechat/server/lib/Helper.js | 4 ++-- app/livechat/server/lib/Livechat.js | 2 -- app/livechat/server/lib/RoutingManager.js | 10 ++++------ .../server/hooks/afterForwardChatToAgent.ts | 17 ----------------- .../hooks/afterForwardChatToDepartment.js | 3 --- .../server/hooks/afterOnHoldChatResumed.ts | 15 --------------- .../server/hooks/afterReturnedRoomAsInquiry.ts | 10 ---------- .../livechat-enterprise/server/hooks/index.js | 3 --- .../server/hooks/onCloseLivechat.js | 2 -- .../server/methods/resumeOnHold.ts | 10 ++++++---- 11 files changed, 13 insertions(+), 65 deletions(-) delete mode 100644 ee/app/livechat-enterprise/server/hooks/afterForwardChatToAgent.ts delete mode 100644 ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts delete mode 100644 ee/app/livechat-enterprise/server/hooks/afterReturnedRoomAsInquiry.ts diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 56de7d9275c0b..77715e01c5cf1 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -208,7 +208,7 @@ Template.visitorInfo.helpers({ canPlaceChatOnHold() { const room = Template.instance().room.get(); - return room.open && !room.onHold && room.servedBy && room.lastMessage && !room.lastMessage?.token && settings.get('Livechat_allow_manual_on_hold'); + return room.open && !room.onHold && room.lastMessage && !room.lastMessage?.token && settings.get('Livechat_allow_manual_on_hold'); }, roomClosedDateTime() { diff --git a/app/livechat/server/lib/Helper.js b/app/livechat/server/lib/Helper.js index 7087edca3d46e..f7a321a25e953 100644 --- a/app/livechat/server/lib/Helper.js +++ b/app/livechat/server/lib/Helper.js @@ -293,7 +293,7 @@ export const forwardRoomToAgent = async (room, transferData) => { const agent = { agentId, username }; // There are some Enterprise features that may interrupt the fowarding process // Due to that we need to check whether the agent has been changed or not - const roomTaken = await RoutingManager.takeInquiry(inquiry, agent, { isForwarding: true }); + const roomTaken = await RoutingManager.takeInquiry(inquiry, agent); if (!roomTaken) { return false; } @@ -378,7 +378,7 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => { // Fake the department to forward the inquiry - Case the forward process does not success // the inquiry will stay in the same original department inquiry.department = departmentId; - const roomTaken = await RoutingManager.delegateInquiry(inquiry, agent, { isForwarding: true }); + const roomTaken = await RoutingManager.delegateInquiry(inquiry, agent); if (!roomTaken) { return false; } diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index aefe57d082ec6..31a7edcb361dd 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -631,8 +631,6 @@ export const Livechat = { throw new Meteor.Error('error-returning-inquiry', 'Error returning inquiry to the queue', { method: 'livechat:returnRoomAsInquiry' }); } - callbacks.run('livechat:afterReturnedRoomAsInquiry', room); - return true; }, diff --git a/app/livechat/server/lib/RoutingManager.js b/app/livechat/server/lib/RoutingManager.js index 9b1ce4d2d3f1f..9f147539d4637 100644 --- a/app/livechat/server/lib/RoutingManager.js +++ b/app/livechat/server/lib/RoutingManager.js @@ -43,7 +43,7 @@ export const RoutingManager = { return this.getMethod().getNextAgent(department, ignoreAgentId); }, - async delegateInquiry(inquiry, agent = null, options = {}) { + async delegateInquiry(inquiry, agent) { const { department, rid } = inquiry; if (!agent || (agent.username && !Users.findOneOnlineAgentByUsername(agent.username) && !allowAgentSkipQueue(agent))) { agent = await this.getNextAgent(department); @@ -53,7 +53,7 @@ export const RoutingManager = { return LivechatRooms.findOneById(rid); } - return this.takeInquiry(inquiry, agent, options); + return this.takeInquiry(inquiry, agent); }, assignAgent(inquiry, agent) { @@ -110,7 +110,7 @@ export const RoutingManager = { return true; }, - async takeInquiry(inquiry, agent, options = { clientAction: false, isForwarding: false }) { + async takeInquiry(inquiry, agent, options = { clientAction: false }) { check(agent, Match.ObjectIncluding({ agentId: String, username: String, @@ -134,9 +134,7 @@ export const RoutingManager = { agent = await callbacks.run('livechat.checkAgentBeforeTakeInquiry', { agent, inquiry, options }); if (!agent) { - if (!options.isForwarding) { - await callbacks.run('livechat.onAgentAssignmentFailed', { inquiry, room, options }); - } + await callbacks.run('livechat.onAgentAssignmentFailed', { inquiry, room, options }); return null; } diff --git a/ee/app/livechat-enterprise/server/hooks/afterForwardChatToAgent.ts b/ee/app/livechat-enterprise/server/hooks/afterForwardChatToAgent.ts deleted file mode 100644 index da18c51be8cfd..0000000000000 --- a/ee/app/livechat-enterprise/server/hooks/afterForwardChatToAgent.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { callbacks } from '../../../../../app/callbacks/server'; -import { LivechatRooms } from '../../../../../app/models/server'; - -const handleAfterForwardChatToAgent = async ({ rid = null }: { rid: any }): Promise => { - if (!rid) { - return; - } - - const room = LivechatRooms.findOneById(rid); - if (!room) { - return; - } - - await callbacks.run('livechat:afterOnHoldChatResumed', room); -}; - -callbacks.add('livechat.afterForwardChatToAgent', handleAfterForwardChatToAgent, callbacks.priority.HIGH, 'livechat-after-forward-chat-to-agent'); diff --git a/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.js b/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.js index f3570f9868747..ed8b311c8835d 100644 --- a/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.js +++ b/ee/app/livechat-enterprise/server/hooks/afterForwardChatToDepartment.js @@ -10,9 +10,6 @@ callbacks.add('livechat.afterForwardChatToDepartment', (options) => { if (!room) { return; } - - callbacks.run('livechat:afterOnHoldChatResumed', room); - setPredictedVisitorAbandonmentTime(room); const department = LivechatDepartment.findOneById(newDepartmentId, { fields: { ancestors: 1 } }); diff --git a/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts b/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts deleted file mode 100644 index 3ee44db73bea0..0000000000000 --- a/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { callbacks } from '../../../../../app/callbacks/server'; -import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; -import { LivechatRooms, Subscriptions } from '../../../../../app/models/server'; - -const handleAfterOnHoldChatResumed = async (room: any): Promise => { - if (!room || !room._id || !room.onHold) { - return; - } - - await AutoCloseOnHoldScheduler.unscheduleRoom(room._id); - (LivechatRooms as any).unsetAllOnHoldFieldsByRoomId(room._id); - Subscriptions.unsetOnHold(room._id); -}; - -callbacks.add('livechat:afterOnHoldChatResumed', handleAfterOnHoldChatResumed, callbacks.priority.HIGH, 'livechat-after-on-hold-chat-resumed'); diff --git a/ee/app/livechat-enterprise/server/hooks/afterReturnedRoomAsInquiry.ts b/ee/app/livechat-enterprise/server/hooks/afterReturnedRoomAsInquiry.ts deleted file mode 100644 index cca1730991916..0000000000000 --- a/ee/app/livechat-enterprise/server/hooks/afterReturnedRoomAsInquiry.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { callbacks } from '../../../../../app/callbacks/server'; - -const handleAfterReturnedRoomAsInquiry = async (room: any): Promise => { - if (!room) { - return; - } - await callbacks.run('livechat:afterOnHoldChatResumed', room); -}; - -callbacks.add('livechat:afterReturnedRoomAsInquiry', handleAfterReturnedRoomAsInquiry, callbacks.priority.HIGH, 'livechat-after-return-room-as-inquiry'); diff --git a/ee/app/livechat-enterprise/server/hooks/index.js b/ee/app/livechat-enterprise/server/hooks/index.js index 1187842aee1f0..d7ace549d8d66 100644 --- a/ee/app/livechat-enterprise/server/hooks/index.js +++ b/ee/app/livechat-enterprise/server/hooks/index.js @@ -16,6 +16,3 @@ import './onCloseLivechat'; import './onSaveVisitorInfo'; import './onBusinessHourStart'; import './onAgentAssignmentFailed'; -import './afterOnHoldChatResumed'; -import './afterReturnedRoomAsInquiry'; -import './afterForwardChatToAgent'; diff --git a/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js b/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js index 91cec0ff26592..c180849ed9759 100644 --- a/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js +++ b/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js @@ -4,8 +4,6 @@ import { dispatchWaitingQueueStatus } from '../lib/Helper'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; const onCloseLivechat = (room) => { - callbacks.run('livechat:afterOnHoldChatResumed', room); - if (!settings.get('Livechat_waiting_queue')) { return room; } diff --git a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index 2fd069cccbc65..0323928ad78f3 100644 --- a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; -import { LivechatRooms, LivechatInquiry } from '../../../../../app/models/server'; +import { LivechatRooms, LivechatInquiry, Subscriptions } from '../../../../../app/models/server'; +import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; -import { callbacks } from '../../../../../app/callbacks/server'; Meteor.methods({ async 'livechat:resumeOnHold'(roomId, options = { clientAction: false }) { @@ -22,9 +22,11 @@ Meteor.methods({ throw new Meteor.Error('inquiry-not-found', 'Error! No inquiry found for this room', { method: 'livechat:resumeOnHold' }); } + await AutoCloseOnHoldScheduler.unscheduleRoom(roomId); + await RoutingManager.takeInquiry(inquiry, { agentId, username }, options); - const updatedRoom = LivechatRooms.findById(roomId, null); - updatedRoom && Meteor.defer(() => callbacks.run('livechat:afterOnHoldChatResumed', updatedRoom)); + (LivechatRooms as any).unsetAllOnHoldFieldsByRoomId(roomId); + Subscriptions.unsetOnHold(roomId); }, }); From 740e14fdfecf42bd5ba582145d650453659a3c0a Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Mon, 22 Mar 2021 22:54:54 +0530 Subject: [PATCH 27/30] Prevent on hold chat from being returned or forwarded (cherry picked from commit 2edcb8234224ebad2b479282da3f9be6a5626db6) --- .../client/views/app/tabbar/visitorInfo.js | 2 +- app/livechat/server/methods/returnAsInquiry.js | 4 ++++ app/livechat/server/methods/transfer.js | 4 ++++ .../server/hooks/afterOnHoldChatResumed.ts | 15 +++++++++++++++ ee/app/livechat-enterprise/server/hooks/index.js | 1 + .../server/hooks/onCloseLivechat.js | 2 ++ .../server/methods/resumeOnHold.ts | 10 ++++------ packages/rocketchat-i18n/i18n/en.i18n.json | 1 + 8 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 77715e01c5cf1..56de7d9275c0b 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -208,7 +208,7 @@ Template.visitorInfo.helpers({ canPlaceChatOnHold() { const room = Template.instance().room.get(); - return room.open && !room.onHold && room.lastMessage && !room.lastMessage?.token && settings.get('Livechat_allow_manual_on_hold'); + return room.open && !room.onHold && room.servedBy && room.lastMessage && !room.lastMessage?.token && settings.get('Livechat_allow_manual_on_hold'); }, roomClosedDateTime() { diff --git a/app/livechat/server/methods/returnAsInquiry.js b/app/livechat/server/methods/returnAsInquiry.js index bb7aa9c8146a7..d736e21ffcbcb 100644 --- a/app/livechat/server/methods/returnAsInquiry.js +++ b/app/livechat/server/methods/returnAsInquiry.js @@ -19,6 +19,10 @@ Meteor.methods({ throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:returnAsInquiry' }); } + if (room.onHold) { + throw new Meteor.Error('error-room-onHold', 'Room On Hold', { method: 'livechat:returnAsInquiry' }); + } + return Livechat.returnRoomAsInquiry(rid, departmentId); }, }); diff --git a/app/livechat/server/methods/transfer.js b/app/livechat/server/methods/transfer.js index 241a031761ae7..57058d25fc0d8 100644 --- a/app/livechat/server/methods/transfer.js +++ b/app/livechat/server/methods/transfer.js @@ -28,6 +28,10 @@ Meteor.methods({ throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:transfer' }); } + if (room.onHold) { + throw new Meteor.Error('error-room-onHold', 'Room On Hold', { method: 'livechat:transfer' }); + } + const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, Meteor.userId(), { fields: { _id: 1 } }); if (!subscription && !hasPermission(Meteor.userId(), 'transfer-livechat-guest')) { throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:transfer' }); diff --git a/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts b/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts new file mode 100644 index 0000000000000..3ee44db73bea0 --- /dev/null +++ b/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts @@ -0,0 +1,15 @@ +import { callbacks } from '../../../../../app/callbacks/server'; +import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; +import { LivechatRooms, Subscriptions } from '../../../../../app/models/server'; + +const handleAfterOnHoldChatResumed = async (room: any): Promise => { + if (!room || !room._id || !room.onHold) { + return; + } + + await AutoCloseOnHoldScheduler.unscheduleRoom(room._id); + (LivechatRooms as any).unsetAllOnHoldFieldsByRoomId(room._id); + Subscriptions.unsetOnHold(room._id); +}; + +callbacks.add('livechat:afterOnHoldChatResumed', handleAfterOnHoldChatResumed, callbacks.priority.HIGH, 'livechat-after-on-hold-chat-resumed'); diff --git a/ee/app/livechat-enterprise/server/hooks/index.js b/ee/app/livechat-enterprise/server/hooks/index.js index d7ace549d8d66..99824636a152c 100644 --- a/ee/app/livechat-enterprise/server/hooks/index.js +++ b/ee/app/livechat-enterprise/server/hooks/index.js @@ -16,3 +16,4 @@ import './onCloseLivechat'; import './onSaveVisitorInfo'; import './onBusinessHourStart'; import './onAgentAssignmentFailed'; +import './afterOnHoldChatResumed'; diff --git a/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js b/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js index c180849ed9759..91cec0ff26592 100644 --- a/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js +++ b/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js @@ -4,6 +4,8 @@ import { dispatchWaitingQueueStatus } from '../lib/Helper'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; const onCloseLivechat = (room) => { + callbacks.run('livechat:afterOnHoldChatResumed', room); + if (!settings.get('Livechat_waiting_queue')) { return room; } diff --git a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index 0323928ad78f3..2fd069cccbc65 100644 --- a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; -import { LivechatRooms, LivechatInquiry, Subscriptions } from '../../../../../app/models/server'; -import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; +import { LivechatRooms, LivechatInquiry } from '../../../../../app/models/server'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; +import { callbacks } from '../../../../../app/callbacks/server'; Meteor.methods({ async 'livechat:resumeOnHold'(roomId, options = { clientAction: false }) { @@ -22,11 +22,9 @@ Meteor.methods({ throw new Meteor.Error('inquiry-not-found', 'Error! No inquiry found for this room', { method: 'livechat:resumeOnHold' }); } - await AutoCloseOnHoldScheduler.unscheduleRoom(roomId); - await RoutingManager.takeInquiry(inquiry, { agentId, username }, options); - (LivechatRooms as any).unsetAllOnHoldFieldsByRoomId(roomId); - Subscriptions.unsetOnHold(roomId); + const updatedRoom = LivechatRooms.findById(roomId, null); + updatedRoom && Meteor.defer(() => callbacks.run('livechat:afterOnHoldChatResumed', updatedRoom)); }, }); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 7ae676016e10f..cca03e6323e02 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1648,6 +1648,7 @@ "error-role-in-use": "Cannot delete role because it's in use", "error-role-name-required": "Role name is required", "error-room-is-not-closed": "Room is not closed", + "error-room-onHold": "Error! Room is On Hold", "error-selected-agent-room-agent-are-same": "The selected agent and the room agent are the same", "error-starring-message": "Message could not be stared", "error-tags-must-be-assigned-before-closing-chat": "Tag(s) must be assigned before closing the chat", From 657a3c007963cd08fc42393fca7c93396a03695d Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Mon, 22 Mar 2021 23:40:29 +0530 Subject: [PATCH 28/30] Move checks to Livechat methods --- app/livechat/server/lib/Livechat.js | 8 ++++++++ app/livechat/server/methods/returnAsInquiry.js | 4 ---- app/livechat/server/methods/transfer.js | 4 ---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index 31a7edcb361dd..c27b7839769d7 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -589,6 +589,10 @@ export const Livechat = { }, async transfer(room, guest, transferData) { + if (room.onHold) { + throw new Meteor.Error('error-room-onHold', 'Room On Hold', { method: 'livechat:transfer' }); + } + if (transferData.departmentId) { transferData.department = LivechatDepartment.findOneById(transferData.departmentId, { fields: { name: 1 } }); } @@ -606,6 +610,10 @@ export const Livechat = { throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:returnRoomAsInquiry' }); } + if (room.onHold) { + throw new Meteor.Error('error-room-onHold', 'Room On Hold', { method: 'livechat:returnRoomAsInquiry' }); + } + if (!room.servedBy) { return false; } diff --git a/app/livechat/server/methods/returnAsInquiry.js b/app/livechat/server/methods/returnAsInquiry.js index d736e21ffcbcb..bb7aa9c8146a7 100644 --- a/app/livechat/server/methods/returnAsInquiry.js +++ b/app/livechat/server/methods/returnAsInquiry.js @@ -19,10 +19,6 @@ Meteor.methods({ throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:returnAsInquiry' }); } - if (room.onHold) { - throw new Meteor.Error('error-room-onHold', 'Room On Hold', { method: 'livechat:returnAsInquiry' }); - } - return Livechat.returnRoomAsInquiry(rid, departmentId); }, }); diff --git a/app/livechat/server/methods/transfer.js b/app/livechat/server/methods/transfer.js index 57058d25fc0d8..241a031761ae7 100644 --- a/app/livechat/server/methods/transfer.js +++ b/app/livechat/server/methods/transfer.js @@ -28,10 +28,6 @@ Meteor.methods({ throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:transfer' }); } - if (room.onHold) { - throw new Meteor.Error('error-room-onHold', 'Room On Hold', { method: 'livechat:transfer' }); - } - const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, Meteor.userId(), { fields: { _id: 1 } }); if (!subscription && !hasPermission(Meteor.userId(), 'transfer-livechat-guest')) { throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:transfer' }); From fe51c5c37982d45b612db1145205af9b6b4ea5ba Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Tue, 23 Mar 2021 01:21:41 +0530 Subject: [PATCH 29/30] Add releaseOnHoldChat() method --- .../server/hooks/afterOnHoldChatResumed.ts | 7 ++----- .../server/hooks/onCloseLivechat.js | 3 ++- .../server/lib/LivechatEnterprise.js | 12 ++++++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts b/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts index 3ee44db73bea0..82875932ccbfa 100644 --- a/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts +++ b/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts @@ -1,15 +1,12 @@ import { callbacks } from '../../../../../app/callbacks/server'; -import { AutoCloseOnHoldScheduler } from '../lib/AutoCloseOnHoldScheduler'; -import { LivechatRooms, Subscriptions } from '../../../../../app/models/server'; +import { LivechatEnterprise } from '../lib/LivechatEnterprise'; const handleAfterOnHoldChatResumed = async (room: any): Promise => { if (!room || !room._id || !room.onHold) { return; } - await AutoCloseOnHoldScheduler.unscheduleRoom(room._id); - (LivechatRooms as any).unsetAllOnHoldFieldsByRoomId(room._id); - Subscriptions.unsetOnHold(room._id); + LivechatEnterprise.releaseOnHoldChat(room); }; callbacks.add('livechat:afterOnHoldChatResumed', handleAfterOnHoldChatResumed, callbacks.priority.HIGH, 'livechat-after-on-hold-chat-resumed'); diff --git a/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js b/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js index 91cec0ff26592..e8a802b8181b2 100644 --- a/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js +++ b/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js @@ -2,9 +2,10 @@ import { callbacks } from '../../../../../app/callbacks'; import { settings } from '../../../../../app/settings'; import { dispatchWaitingQueueStatus } from '../lib/Helper'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; +import { LivechatEnterprise } from '../lib/LivechatEnterprise'; const onCloseLivechat = (room) => { - callbacks.run('livechat:afterOnHoldChatResumed', room); + Promise.await(LivechatEnterprise.releaseOnHoldChat(room)); if (!settings.get('Livechat_waiting_queue')) { return room; diff --git a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index 0ccc1ffed8a75..6ce8b448ce5f4 100644 --- a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -12,6 +12,7 @@ import { processWaitingQueue, removePriorityFromRooms, updateInquiryQueuePriorit import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../app/callbacks'; +import { AutoCloseOnHoldScheduler } from './AutoCloseOnHoldScheduler'; export const LivechatEnterprise = { addMonitor(username) { @@ -180,6 +181,17 @@ export const LivechatEnterprise = { return true; }, + + async releaseOnHoldChat(room) { + const { _id: roomId, onHold } = room; + if (!roomId || !onHold) { + return; + } + + await AutoCloseOnHoldScheduler.unscheduleRoom(roomId); + LivechatRooms.unsetAllOnHoldFieldsByRoomId(roomId); + Subscriptions.unsetOnHold(roomId); + }, }; const RACE_TIMEOUT = 1000; From 3126b797498048da54078bf5e5e044f2dc501873 Mon Sep 17 00:00:00 2001 From: Renato Becker Date: Mon, 22 Mar 2021 18:38:27 -0300 Subject: [PATCH 30/30] Fix callback returning promise. --- .../{resumeOnHold.ts => resumeOnHold.js} | 26 +++++++++++-------- .../server/methods/resumeOnHold.ts | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) rename ee/app/livechat-enterprise/server/hooks/{resumeOnHold.ts => resumeOnHold.js} (64%) diff --git a/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts b/ee/app/livechat-enterprise/server/hooks/resumeOnHold.js similarity index 64% rename from ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts rename to ee/app/livechat-enterprise/server/hooks/resumeOnHold.js index 9ea8aab39fa42..750662e3a5cfe 100644 --- a/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/hooks/resumeOnHold.js @@ -1,29 +1,33 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../app/callbacks/server'; +import { LivechatRooms } from '../../../../../app/models/server'; -const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promise => { - const { _id: rid, onHold } = room; - if (!rid) { +const handleAfterSaveMessage = (message, { _id: rid }) => { + // skips this callback if the message was edited + if (message.editedAt) { return message; } - // skips this callback if the message was edited - if (message.editedAt) { + // if the message has a type means it is a special message (like the closing comment), so skips + if (message.t) { return message; } - // message valid only if it is a livechat room - if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.v && room.v.token)) { + + // Need to read the room every time, the room object is not updated + const room = LivechatRooms.findOneById(rid, { t: 1, v: 1, onHold: 1 }); + if (!room) { return message; } - // if the message has a type means it is a special message (like the closing comment), so skips - if (message.t) { + + // message valid only if it is a livechat room + if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.v && room.v.token)) { return message; } // if a visitor sends a message in room which is On Hold - if (message.token && onHold) { - await Meteor.call('livechat:resumeOnHold', room._id, { clientAction: false }); + if (message.token && room.onHold) { + Meteor.call('livechat:resumeOnHold', rid, { clientAction: false }); } return message; diff --git a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index 2fd069cccbc65..77523c52e04cb 100644 --- a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -24,7 +24,7 @@ Meteor.methods({ await RoutingManager.takeInquiry(inquiry, { agentId, username }, options); - const updatedRoom = LivechatRooms.findById(roomId, null); + const updatedRoom = LivechatRooms.findOneById(roomId); updatedRoom && Meteor.defer(() => callbacks.run('livechat:afterOnHoldChatResumed', updatedRoom)); }, });