Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7eef8ff
Add settings for on-hold feature
Feb 23, 2021
3a73b5c
Add On-Hold Section within Sidebar
Feb 24, 2021
f1be061
On hold room UI
Feb 27, 2021
97b8bc4
OnHold - Automatic
Feb 28, 2021
06033c5
Add Manual on-hold button UI
Mar 1, 2021
90f4329
OnHold - manual
Mar 3, 2021
2fbcfaf
Handle manual On-Hold event
Mar 3, 2021
606d6e9
Add permissions to manual on-hold feature
Mar 3, 2021
dda8636
[New] Auto-Close On hold chats
Mar 5, 2021
28f6b50
Routing chat when an on-hold get is resumed
Mar 16, 2021
e77d930
Apply suggestions from code review
Mar 16, 2021
6e8dd1b
Merge branch 'develop' into omnichannel/on-hold
murtaza98 Mar 16, 2021
f22ce72
Apply suggestions from code review
Mar 17, 2021
b6717b5
Add migration
Mar 17, 2021
0160705
Add new endpoint - livechat/placeChatOnHold
Mar 17, 2021
d725098
Merge branch 'develop' into omnichannel/on-hold
renatobecker Mar 18, 2021
77f7d01
Merge branch 'develop' into omnichannel/on-hold
renatobecker Mar 18, 2021
5eead6b
Move Resume Button login within livechatReadOnly file
Mar 19, 2021
2005de7
Remove timeout on Manual On-Hold feature
Mar 19, 2021
1de02d3
Merge branch 'omnichannel/on-hold' of https://github.com/RocketChat/R…
Mar 19, 2021
f38eb4e
Apply suggestions from code review
Mar 19, 2021
bf2d641
Move resume On-Hold chat logic inside Queue Manager
Mar 19, 2021
0fef8ae
Merge branch 'develop' into omnichannel/on-hold
renatobecker Mar 19, 2021
077161d
Apply suggestions from code review
murtaza98 Mar 19, 2021
8e3e713
Apply suggestions from code review
Mar 19, 2021
161efac
Merge branch 'omnichannel/on-hold' of https://github.com/RocketChat/R…
Mar 19, 2021
5ca9cba
Use takeInquiry() to resume On-Hold chats
Mar 20, 2021
966dd0d
Fix failing test case
Mar 20, 2021
4979234
Merge branch 'develop' into omnichannel/on-hold
murtaza98 Mar 20, 2021
c4f60be
Merge branch 'develop' into omnichannel/on-hold
renatobecker Mar 20, 2021
2983087
Minor improvements.
renatobecker Mar 20, 2021
85dbe77
[Regression] Omnichannel On Hold feature - handle following impacted …
Mar 22, 2021
16db56a
Fix failing test cases
Mar 22, 2021
8b8b9b9
Merge pull request #21240 from RocketChat/omnichannel/on-hold-regression
murtaza98 Mar 22, 2021
c1c634e
Revert "[Regression] Omnichannel On hold Queue"
murtaza98 Mar 22, 2021
6233fc6
Merge pull request #21242 from RocketChat/revert-21240-omnichannel/on…
murtaza98 Mar 22, 2021
7c1765f
Merge branch 'develop' into omnichannel/on-hold
renatobecker Mar 22, 2021
740e14f
Prevent on hold chat from being returned or forwarded
Mar 22, 2021
657a3c0
Move checks to Livechat methods
Mar 22, 2021
fe51c5c
Add releaseOnHoldChat() method
Mar 22, 2021
3126b79
Fix callback returning promise.
renatobecker Mar 22, 2021
7bc2b4f
Merge branch 'develop' into omnichannel/on-hold
renatobecker Mar 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/authorization/server/startup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'] },
Expand Down
14 changes: 14 additions & 0 deletions app/livechat/client/views/app/livechatOnHold.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template name="livechatOnHold">
{{#if roomOpen}}
{{#if isPreparing}}
{{> loading}}
{{else}}
<div class="rc-message-box__join">
{{{_ "chat_on_hold_due_to_inactivity"}}}
<button class="rc-button rc-button--primary rc-button--small rc-message-box__resume-it-button js-resume-it">{{_ "Resume"}}</button>
Comment thread
murtaza98 marked this conversation as resolved.
Outdated
</div>
{{/if}}
{{else}}
<p>{{_ "This_conversation_is_already_closed"}}</p>
{{/if}}
</template>
40 changes: 40 additions & 0 deletions app/livechat/client/views/app/livechatOnHold.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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);
});
});
4 changes: 4 additions & 0 deletions app/livechat/client/views/app/tabbar/visitorInfo.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ <h3>{{_ "Transcript_Request"}}</h3>
<button class='button rc-button rc-button--secondary button-block edit-livechat'><span><i class='icon-edit'></i> {{_ "Edit"}}</span></button>
{{/if}}

{{#if canPlaceChatOnHold}}
<button class="button rc-button rc-button--secondary button-block on-hold"><span><i class="icon-pause"></i> {{_ "On_Hold_Chats"}}</span></button>
{{/if}}

{{#if roomOpen}}
{{#if canCloseRoom}}
<button class='button rc-button rc-button--secondary button-block close-livechat'><span><i class='icon-cancel'></i> {{_ "Close"}}</span></button>
Expand Down
29 changes: 29 additions & 0 deletions app/livechat/client/views/app/tabbar/visitorInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ Template.visitorInfo.helpers({
return !room.email && hasPermission('send-omnichannel-chat-transcript');
},

canPlaceChatOnHold() {
const room = Template.instance().room.get();
return !!room.canPlaceChatOnHold;
},

roomClosedDateTime() {
const { closedAt } = this;
return DateFormat.formatDateAndTime(closedAt);
Expand Down Expand Up @@ -324,6 +329,30 @@ 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'),
}, () => 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() {
Expand Down
2 changes: 1 addition & 1 deletion app/livechat/server/lib/QueueManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Comment thread
murtaza98 marked this conversation as resolved.
const inquiryAgent = RoutingManager.delegateAgent(defaultAgent, inquiry);
await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent);
inquiry = LivechatInquiry.findOneById(inquiry._id);
Expand Down
16 changes: 16 additions & 0 deletions app/models/server/models/Subscriptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Comment thread
renatobecker marked this conversation as resolved.
);
}

unsetOnHold(roomId) {
return this.update(
{ rid: roomId },
{ $unset: { onHold: 1 } },
{ multi: true },
Comment thread
renatobecker marked this conversation as resolved.
);
}
}

export default new Subscriptions('subscription', true);
2 changes: 1 addition & 1 deletion app/models/server/raw/Users.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
];

Expand Down
6 changes: 5 additions & 1 deletion app/ui-message/client/messageBox/messageBox.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@
{{#if isBlockedOrBlocker}}
{{_ "room_is_blocked"}}
{{else}}
{{> messageBoxReadOnly rid=rid isSubscribed=isSubscribed}}
{{#if onHold}}
{{> livechatOnHold rid=rid isSubscribed=isSubscribed}}
{{else}}
{{> messageBoxReadOnly rid=rid isSubscribed=isSubscribed}}
{{/if}}
{{/if}}
</div>
{{/if}}
Expand Down
13 changes: 12 additions & 1 deletion app/ui-message/client/messageBox/messageBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import './messageBoxAudioMessage';
import './messageBoxNotSubscribed';
import './messageBox.html';
import './messageBoxReadOnly';
import '../../../livechat/client/views/app/livechatOnHold';
Comment thread
murtaza98 marked this conversation as resolved.
Outdated

Template.messageBox.onCreated(function() {
this.state = new ReactiveDict();
Expand Down Expand Up @@ -120,7 +121,6 @@ Template.messageBox.onRendered(function() {
}
$input.on('dataChange', () => {
const messages = $input.data('reply') || [];
console.log('dataChange', messages);
this.replyMessageData.set(messages);
});
}
Expand Down Expand Up @@ -214,6 +214,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);

Expand Down Expand Up @@ -256,6 +260,13 @@ Template.messageBox.helpers({
isBlockedOrBlocker() {
return Template.instance().state.get('isBlockedOrBlocker');
},
onHold() {
const { rid, subscription } = Template.currentData();
if (!rid) {
return false;
}
return !!subscription?.onHold;
Comment thread
murtaza98 marked this conversation as resolved.
Outdated
},
isSubscribed() {
const { subscription } = Template.currentData();
return !!subscription;
Expand Down
13 changes: 10 additions & 3 deletions client/sidebar/hooks/useRoomList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const useRoomList = (): Array<ISubscription> => {
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) {
Expand All @@ -55,6 +56,10 @@ export const useRoomList = (): Array<ISubscription> => {
_private.add(room);
}

if (room.t === 'l' && room.onHold) {
return showOmnichannel && onHold.add(room);
}

if (room.t === 'l') {
return showOmnichannel && omnichannel.add(room);
}
Expand All @@ -66,11 +71,13 @@ export const useRoomList = (): Array<ISubscription> => {
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', []);
Comment thread
renatobecker marked this conversation as resolved.
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);
Expand Down
3 changes: 3 additions & 0 deletions definition/IRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export interface IRoom extends IRocketChatRecord {
balance: number;
}[];
};

onHold?: boolean;
canPlaceChatOnHold?: boolean;
Comment thread
murtaza98 marked this conversation as resolved.
Outdated
}

export interface IDirectMessageRoom extends Omit<IRoom, 'default' | 'featured' | 'u' | 'name'> {
Expand Down
2 changes: 2 additions & 0 deletions definition/ISubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface ISubscription extends IRocketChatRecord {
prid?: RoomID;

roles?: string[];

onHold?: boolean;
}

export interface ISubscriptionDirectMessage extends Omit<ISubscription, 'name'> {
Expand Down
33 changes: 33 additions & 0 deletions ee/app/livechat-enterprise/server/hooks/afterOnHold.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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';
Comment thread
murtaza98 marked this conversation as resolved.
Outdated

let autoCloseOnHoldChatTimeout = 0;
let customCloseMessage = DEFAULT_CLOSED_MESSAGE;

const handleAfterOnHold = async (room: any = {}): Promise<any> => {
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;
});
60 changes: 60 additions & 0 deletions ee/app/livechat-enterprise/server/hooks/scheduleManualOnHold.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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<any> => {
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 (!manualOnHoldTimeout) {
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;
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions ee/app/livechat-enterprise/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ 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';
import LivechatUnitMonitors from '../../models/server/models/LivechatUnitMonitors';
Expand All @@ -26,7 +28,11 @@ import './hooks/onLoadConfigApi';
import './hooks/onCloseLivechat';
import './hooks/onSaveVisitorInfo';
import './hooks/scheduleAutoTransfer';
import './hooks/scheduleManualOnHold';
import './hooks/afterOnHold';
import './lib/routing/LoadBalancing';
import './lib/AutoCloseOnHoldScheduler';
import './lib/ManualOnHoldScheduler';
import { onLicense } from '../../license/server';
import './business-hour';

Expand Down
Loading