diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js
index 7d5efbdca1679..759c363bb3d50 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: '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/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..fcfaed1111298 100644
--- a/app/livechat/client/views/app/livechatReadOnly.js
+++ b/app/livechat/client/views/app/livechatReadOnly.js
@@ -22,12 +22,16 @@ Template.livechatReadOnly.helpers({
showPreview() {
const config = Template.instance().routingConfig.get();
- return config.previewRoom;
+ return config.previewRoom || Template.currentData().onHold;
},
isPreparing() {
return Template.instance().preparing.get();
},
+
+ isOnHold() {
+ return Template.currentData().onHold;
+ },
});
Template.livechatReadOnly.events({
@@ -37,9 +41,18 @@ 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);
},
+
+ 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/livechat/client/views/app/tabbar/visitorInfo.html b/app/livechat/client/views/app/tabbar/visitorInfo.html
index c46ed0b1ec51d..233bd77ccd360 100644
--- a/app/livechat/client/views/app/tabbar/visitorInfo.html
+++ b/app/livechat/client/views/app/tabbar/visitorInfo.html
@@ -89,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 f061207a35af6..56de7d9275c0b 100644
--- a/app/livechat/client/views/app/tabbar/visitorInfo.js
+++ b/app/livechat/client/views/app/tabbar/visitorInfo.js
@@ -206,6 +206,11 @@ Template.visitorInfo.helpers({
return !room.email && hasPermission('send-omnichannel-chat-transcript');
},
+ 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');
+ },
+
roomClosedDateTime() {
const { closedAt } = this;
return DateFormat.formatDateAndTime(closedAt);
@@ -324,6 +329,31 @@ Template.visitorInfo.events({
instance.action.set('transcript');
},
+
+ 'click .on-hold'(event) {
+ event.preventDefault();
+
+ modal.open({
+ title: t('Would_you_like_to_place_chat_on_hold'),
+ type: 'warning',
+ showCancelButton: true,
+ confirmButtonColor: '#3085d6',
+ cancelButtonColor: '#d33',
+ confirmButtonText: t('Yes'),
+ },
+ async () => {
+ const { success } = await APIClient.v1.post('livechat/room.onHold', { roomId: this.rid });
+ if (success) {
+ 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/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/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/app/models/server/models/Subscriptions.js b/app/models/server/models/Subscriptions.js
index 049e6a05cda9d..fcd24f02c1dcb 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 });
}
+
+ setOnHold(roomId) {
+ return this.update(
+ { rid: roomId },
+ { $set: { onHold: true } },
+ { multi: true },
+ );
+ }
+
+ unsetOnHold(roomId) {
+ return this.update(
+ { rid: roomId },
+ { $unset: { onHold: 1 } },
+ { multi: true },
+ );
+ }
}
export default new Subscriptions('subscription', true);
diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js
index c89ecd9068230..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: { $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.onHold', true] }] } } } } } },
{ $sort: { 'queueInfo.chats': 1, lastAssignTime: 1, lastRoutingTime: 1, username: 1 } },
];
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.html b/app/ui-message/client/messageBox/messageBox.html
index 12def35badffa..a5fb8327baace 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}}
- {{> messageBoxReadOnly rid=rid isSubscribed=isSubscribed}}
+ {{> 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 46856eeecea10..7fcf12f371a90 100644
--- a/app/ui-message/client/messageBox/messageBox.js
+++ b/app/ui-message/client/messageBox/messageBox.js
@@ -120,7 +120,6 @@ Template.messageBox.onRendered(function() {
}
$input.on('dataChange', () => {
const messages = $input.data('reply') || [];
- console.log('dataChange', messages);
this.replyMessageData.set(messages);
});
}
@@ -214,6 +213,10 @@ Template.messageBox.helpers({
return false;
}
+ if (subscription?.onHold) {
+ 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 +259,10 @@ Template.messageBox.helpers({
isBlockedOrBlocker() {
return Template.instance().state.get('isBlockedOrBlocker');
},
+ onHold() {
+ const { rid, subscription } = Template.currentData();
+ return rid && !!subscription?.onHold;
+ },
isSubscribed() {
const { subscription } = Template.currentData();
return !!subscription;
diff --git a/client/sidebar/hooks/useRoomList.ts b/client/sidebar/hooks/useRoomList.ts
index 96087832288c7..2efe703a46a94 100644
--- a/client/sidebar/hooks/useRoomList.ts
+++ b/client/sidebar/hooks/useRoomList.ts
@@ -33,6 +33,7 @@ export const useRoomList = (): Array => {
const direct = new Set();
const discussion = new Set();
const conversation = new Set();
+ const onHold = new Set();
rooms.forEach((room) => {
if (sidebarShowUnread && (room.alert || room.unread) && !room.hideUnreadStatus) {
@@ -55,6 +56,10 @@ export const useRoomList = (): Array => {
_private.add(room);
}
+ if (room.t === 'l' && room.onHold) {
+ return showOmnichannel && onHold.add(room);
+ }
+
if (room.t === 'l') {
return showOmnichannel && omnichannel.add(room);
}
@@ -66,11 +71,13 @@ export const useRoomList = (): Array => {
conversation.add(room);
});
+
const groups = new Map();
- showOmnichannel && inquiries.enabled && groups.set('Omnichannel', []);
- showOmnichannel && !inquiries.enabled && 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 && omnichannel.size && groups.set('Open_Livechats', omnichannel);
+ 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 ae83e23360bdc..a6504dedc7d6c 100644
--- a/definition/IRoom.ts
+++ b/definition/IRoom.ts
@@ -38,6 +38,8 @@ export interface IRoom extends IRocketChatRecord {
balance: number;
}[];
};
+
+ onHold?: boolean;
}
export interface IDirectMessageRoom extends Omit {
diff --git a/definition/ISubscription.ts b/definition/ISubscription.ts
index 268475735fae9..69b5a154d89d8 100644
--- a/definition/ISubscription.ts
+++ b/definition/ISubscription.ts
@@ -31,6 +31,8 @@ export interface ISubscription extends IRocketChatRecord {
prid?: RoomID;
roles?: string[];
+
+ onHold?: boolean;
}
export interface ISubscriptionDirectMessage extends Omit {
diff --git a/ee/app/livechat-enterprise/server/api/index.js b/ee/app/livechat-enterprise/server/api/index.js
index da303b6e498ab..1906206a69b1b 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 './rooms';
diff --git a/ee/app/livechat-enterprise/server/api/rooms.js b/ee/app/livechat-enterprise/server/api/rooms.js
new file mode 100644
index 0000000000000..4cec68f48ddb4
--- /dev/null
+++ b/ee/app/livechat-enterprise/server/api/rooms.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/room.onHold', { authRequired: true }, {
+ post() {
+ const { roomId } = this.bodyParams;
+ 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/afterOnHold.ts b/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts
new file mode 100644
index 0000000000000..6a7a4063f28bc
--- /dev/null
+++ b/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts
@@ -0,0 +1,36 @@
+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 = TAPi18n.__('Closed_automatically');
+
+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);
+};
+
+settings.get('Livechat_auto_close_on_hold_chats_timeout', (_, value) => {
+ autoCloseOnHoldChatTimeout = value as number;
+ if (!value || value <= 0) {
+ callbacks.remove('livechat:afterOnHold', 'livechat-auto-close-on-hold');
+ }
+ callbacks.add('livechat:afterOnHold', handleAfterOnHold, callbacks.priority.HIGH, 'livechat-auto-close-on-hold');
+});
+
+settings.get('Livechat_auto_close_on_hold_chats_custom_message', (_, value) => {
+ customCloseMessage = value as string || DEFAULT_CLOSED_MESSAGE;
+});
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..82875932ccbfa
--- /dev/null
+++ b/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts
@@ -0,0 +1,12 @@
+import { callbacks } from '../../../../../app/callbacks/server';
+import { LivechatEnterprise } from '../lib/LivechatEnterprise';
+
+const handleAfterOnHoldChatResumed = async (room: any): Promise => {
+ if (!room || !room._id || !room.onHold) {
+ return;
+ }
+
+ 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/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..99824636a152c 100644
--- a/ee/app/livechat-enterprise/server/hooks/index.js
+++ b/ee/app/livechat-enterprise/server/hooks/index.js
@@ -15,3 +15,5 @@ import './onLoadConfigApi';
import './onCloseLivechat';
import './onSaveVisitorInfo';
import './onBusinessHourStart';
+import './onAgentAssignmentFailed';
+import './afterOnHoldChatResumed';
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..b8592fff228d7
--- /dev/null
+++ b/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts
@@ -0,0 +1,25 @@
+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 || !room.onHold) {
+ return;
+ }
+
+ const { _id: roomId, servedBy } = room;
+
+ const { _id: inquiryId } = inquiry;
+ LivechatInquiry.readyInquiry(inquiryId);
+ LivechatInquiry.removeDefaultAgentById(inquiryId);
+ LivechatRooms.removeAgentByRoomId(roomId);
+ if (servedBy?._id) {
+ Subscriptions.removeByRoomIdAndUserId(roomId, servedBy._id);
+ }
+
+ const newInquiry = LivechatInquiry.findOneById(inquiryId);
+
+ await queueInquiry(room, newInquiry);
+};
+
+callbacks.add('livechat.onAgentAssignmentFailed', handleOnAgentAssignmentFailed, callbacks.priority.HIGH, 'livechat-agent-assignment-failed');
diff --git a/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js b/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js
index c180849ed9759..e8a802b8181b2 100644
--- a/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js
+++ b/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js
@@ -2,8 +2,11 @@ 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) => {
+ Promise.await(LivechatEnterprise.releaseOnHoldChat(room));
+
if (!settings.get('Livechat_waiting_queue')) {
return room;
}
diff --git a/ee/app/livechat-enterprise/server/hooks/resumeOnHold.js b/ee/app/livechat-enterprise/server/hooks/resumeOnHold.js
new file mode 100644
index 0000000000000..750662e3a5cfe
--- /dev/null
+++ b/ee/app/livechat-enterprise/server/hooks/resumeOnHold.js
@@ -0,0 +1,36 @@
+import { Meteor } from 'meteor/meteor';
+
+import { callbacks } from '../../../../../app/callbacks/server';
+import { LivechatRooms } from '../../../../../app/models/server';
+
+const handleAfterSaveMessage = (message, { _id: rid }) => {
+ // skips this callback if the message was edited
+ if (message.editedAt) {
+ 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;
+ }
+
+ // 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;
+ }
+
+ // 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 && room.onHold) {
+ Meteor.call('livechat:resumeOnHold', rid, { clientAction: false });
+ }
+
+ return message;
+};
+
+callbacks.add('afterSaveMessage', handleAfterSaveMessage, callbacks.priority.HIGH, 'livechat-resume-on-hold');
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/index.js b/ee/app/livechat-enterprise/server/index.js
index 0f6006affbed4..3763b924c2195 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/resumeOnHold';
import LivechatUnit from '../../models/server/models/LivechatUnit';
import LivechatTag from '../../models/server/models/LivechatTag';
import LivechatUnitMonitors from '../../models/server/models/LivechatUnitMonitors';
@@ -26,7 +27,10 @@ import './hooks/onLoadConfigApi';
import './hooks/onCloseLivechat';
import './hooks/onSaveVisitorInfo';
import './hooks/scheduleAutoTransfer';
+import './hooks/resumeOnHold';
+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..9042f96b8e17c
--- /dev/null
+++ b/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts
@@ -0,0 +1,67 @@
+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 {
+ 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 }`;
+ const when = moment(new Date()).add(timeout, 's').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 {
+ const { roomId, comment } = data;
+
+ const payload = {
+ user: schedulerUser,
+ room: LivechatRooms.findOneById(roomId),
+ comment,
+ options: {},
+ visitor: undefined,
+ };
+
+ Livechat.closeRoom(payload);
+ }
+}
+
+export const AutoCloseOnHoldScheduler = new AutoCloseOnHoldSchedulerClass();
+
+Meteor.startup(() => {
+ AutoCloseOnHoldScheduler.init();
+});
diff --git a/ee/app/livechat-enterprise/server/lib/Helper.js b/ee/app/livechat-enterprise/server/lib/Helper.js
index b3f8f675f32da..ed11b351334b4 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,10 +144,10 @@ export const setPredictedVisitorAbandonmentTime = (room) => {
};
export const updatePredictedVisitorAbandonment = () => {
- if (settings.get('Livechat_auto_close_abandoned_rooms')) {
- 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));
}
};
diff --git a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js
index 77608e6c53066..6ce8b448ce5f4 100644
--- a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js
+++ b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js
@@ -5,11 +5,14 @@ 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';
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) {
@@ -163,6 +166,32 @@ export const LivechatEnterprise = {
updateInquiryQueuePriority(roomId, priority);
updateRoomPriorityHistory(roomId, user, priority);
},
+
+ placeRoomOnHold(room) {
+ const { _id: roomId, onHold } = room;
+ if (!roomId || onHold) {
+ return false;
+ }
+ LivechatRooms.setOnHold(roomId);
+ Subscriptions.setOnHold(roomId);
+
+ Meteor.defer(() => {
+ callbacks.run('livechat:afterOnHold', room);
+ });
+
+ 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;
diff --git a/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js b/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js
index 11879836e1829..3f32e99126cdc 100644
--- a/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js
+++ b/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js
@@ -4,6 +4,7 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { settings } from '../../../../../app/settings/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() {
@@ -77,11 +78,27 @@ export class VisitorInactivityMonitor {
});
}
+ placeRoomOnHold(room) {
+ LivechatEnterprise.placeRoomOnHold(room) && LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id);
+ }
+
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/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts
new file mode 100644
index 0000000000000..77523c52e04cb
--- /dev/null
+++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts
@@ -0,0 +1,30 @@
+import { Meteor } from 'meteor/meteor';
+
+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 }) {
+ const room = await LivechatRooms.findOneById(roomId);
+ if (!room || room.t !== 'l') {
+ throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:resumeOnHold' });
+ }
+
+ if (!room.onHold) {
+ throw new Meteor.Error('room-closed', 'Room is not OnHold', { method: 'livechat:resumeOnHold' });
+ }
+
+ const { servedBy: { _id: agentId, username } } = room;
+
+ 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 RoutingManager.takeInquiry(inquiry, { agentId, username }, options);
+
+ const updatedRoom = LivechatRooms.findOneById(roomId);
+ updatedRoom && Meteor.defer(() => callbacks.run('livechat:afterOnHoldChatResumed', updatedRoom));
+ },
+});
diff --git a/ee/app/livechat-enterprise/server/permissions.js b/ee/app/livechat-enterprise/server/permissions.js
index ba09adda5ecb4..2e934be9cea44 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',
+ '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/livechat-enterprise/server/settings.js b/ee/app/livechat-enterprise/server/settings.js
index 89c15ea1890f4..4963cc256981a 100644
--- a/ee/app/livechat-enterprise/server/settings.js
+++ b/ee/app/livechat-enterprise/server/settings.js
@@ -53,24 +53,30 @@ export const createSettings = () => {
],
});
- settings.add('Livechat_auto_close_abandoned_rooms', false, {
- type: 'boolean',
+ settings.add('Livechat_abandoned_rooms_action', 'none', {
+ 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' },
+ ],
enterprise: true,
- invalidValue: false,
+ public: true,
+ invalidValue: 'none',
modules: [
'livechat-enterprise',
],
});
+
settings.add('Livechat_abandoned_rooms_closed_custom_message', '', {
type: 'string',
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 +95,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 +128,52 @@ export const createSettings = () => {
],
});
+ settings.add('Livechat_auto_close_on_hold_chats_timeout', 3600, {
+ type: 'int',
+ group: 'Omnichannel',
+ section: 'Sessions',
+ enterprise: true,
+ invalidValue: 0,
+ modules: [
+ 'livechat-enterprise',
+ ],
+ });
+
+ 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', false, {
+ type: 'boolean',
+ group: 'Omnichannel',
+ section: 'Sessions',
+ enterprise: true,
+ invalidValue: false,
+ public: true,
+ 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/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..69144374584e0 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.setOnHold = function(roomId) {
+ return this.update(
+ { _id: roomId },
+ { $set: { onHold: true } },
+ );
+};
+
+LivechatRooms.prototype.unsetOnHold = function(roomId) {
+ return this.update(
+ { _id: roomId },
+ { $unset: { onHold: 1 } },
+ );
+};
+
LivechatRooms.prototype.unsetPredictedVisitorAbandonment = function() {
return this.update({
open: true,
@@ -57,6 +71,25 @@ LivechatRooms.prototype.unsetPredictedVisitorAbandonment = function() {
});
};
+LivechatRooms.prototype.unsetPredictedVisitorAbandonmentByRoomId = function(roomId) {
+ return this.update({
+ _id: roomId,
+ }, {
+ $unset: { 'omnichannel.predictedVisitorAbandonmentAt': 1 },
+ });
+};
+
+LivechatRooms.prototype.unsetAllOnHoldFieldsByRoomId = function(roomId) {
+ return this.update({
+ _id: roomId,
+ }, {
+ $unset: {
+ 'omnichannel.predictedVisitorAbandonmentAt': 1,
+ onHold: 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 034a84489cf7f..bdb4544869d4c 100644
--- a/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -724,8 +724,12 @@
"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_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_resumed": "Chat Resumed",
"Chat_start": "Chat Start",
"Chat_started": "Chat started",
"Chat_taken": "Chat Taken",
@@ -1372,6 +1376,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",
@@ -1644,6 +1649,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",
@@ -2401,15 +2407,23 @@
"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_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_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_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",
"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",
@@ -2434,6 +2448,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 chat On-Hold",
"Livechat_online": "Omnichannel on-line",
"Livechat_Queue": "Omnichannel Queue",
"Livechat_registration_form": "Registration Form",
@@ -2933,6 +2948,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",
@@ -4233,6 +4249,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!",
diff --git a/server/modules/watchers/publishFields.ts b/server/modules/watchers/publishFields.ts
index 63f1cd8183858..4b05d89372e7d 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,
+ onHold: 1,
};
export const roomFields = {
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';
diff --git a/server/startup/migrations/v219.js b/server/startup/migrations/v219.js
new file mode 100644
index 0000000000000..b155c086d2552
--- /dev/null
+++ b/server/startup/migrations/v219.js
@@ -0,0 +1,34 @@
+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,
+ });
+ },
+});