Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW][Enterprise] Omnichannel On-Hold Queue #20945

Merged
merged 42 commits into from
Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7eef8ff
Add settings for on-hold feature
murtaza98 Feb 23, 2021
3a73b5c
Add On-Hold Section within Sidebar
murtaza98 Feb 24, 2021
f1be061
On hold room UI
murtaza98 Feb 27, 2021
97b8bc4
OnHold - Automatic
murtaza98 Feb 28, 2021
06033c5
Add Manual on-hold button UI
murtaza98 Mar 1, 2021
90f4329
OnHold - manual
murtaza98 Mar 3, 2021
2fbcfaf
Handle manual On-Hold event
murtaza98 Mar 3, 2021
606d6e9
Add permissions to manual on-hold feature
murtaza98 Mar 3, 2021
dda8636
[New] Auto-Close On hold chats
murtaza98 Mar 5, 2021
28f6b50
Routing chat when an on-hold get is resumed
murtaza98 Mar 16, 2021
e77d930
Apply suggestions from code review
murtaza98 Mar 16, 2021
6e8dd1b
Merge branch 'develop' into omnichannel/on-hold
murtaza98 Mar 16, 2021
f22ce72
Apply suggestions from code review
murtaza98 Mar 17, 2021
b6717b5
Add migration
murtaza98 Mar 17, 2021
0160705
Add new endpoint - livechat/placeChatOnHold
murtaza98 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
murtaza98 Mar 19, 2021
2005de7
Remove timeout on Manual On-Hold feature
murtaza98 Mar 19, 2021
1de02d3
Merge branch 'omnichannel/on-hold' of https://github.com/RocketChat/R…
murtaza98 Mar 19, 2021
f38eb4e
Apply suggestions from code review
murtaza98 Mar 19, 2021
bf2d641
Move resume On-Hold chat logic inside Queue Manager
murtaza98 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
murtaza98 Mar 19, 2021
161efac
Merge branch 'omnichannel/on-hold' of https://github.com/RocketChat/R…
murtaza98 Mar 19, 2021
5ca9cba
Use takeInquiry() to resume On-Hold chats
murtaza98 Mar 20, 2021
966dd0d
Fix failing test case
murtaza98 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 …
murtaza98 Mar 22, 2021
16db56a
Fix failing test cases
murtaza98 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
murtaza98 Mar 22, 2021
657a3c0
Move checks to Livechat methods
murtaza98 Mar 22, 2021
fe51c5c
Add releaseOnHoldChat() method
murtaza98 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
15 changes: 11 additions & 4 deletions app/livechat/client/views/app/livechatReadOnly.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@
{{#if isPreparing}}
{{> loading}}
{{else}}
{{#if inquiryOpen}}
{{#if isOnHold}}
<div class="rc-message-box__join">
{{{_ "you_are_in_preview_mode_of_incoming_livechat"}}}
<button class="rc-button rc-button--primary rc-button--small rc-message-box__take-it-button js-take-it">{{_ "Take_it"}}</button>
{{{_ "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>
</div>
{{else}}
{{_ "room_is_read_only"}}
{{#if inquiryOpen}}
<div class="rc-message-box__join">
{{{_ "you_are_in_preview_mode_of_incoming_livechat"}}}
<button class="rc-button rc-button--primary rc-button--small rc-message-box__take-it-button js-take-it">{{_ "Take_it"}}</button>
</div>
{{else}}
{{_ "room_is_read_only"}}
{{/if}}
{{/if}}
{{/if}}
{{/if}}
Expand Down
13 changes: 13 additions & 0 deletions app/livechat/client/views/app/livechatReadOnly.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ Template.livechatReadOnly.helpers({
isPreparing() {
return Template.instance().preparing.get();
},

isOnHold() {
return Template.currentData().onHold;
},
});

Template.livechatReadOnly.events({
Expand All @@ -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() {
Expand Down
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 @@ -89,6 +89,10 @@ <h3>{{_ "Transcript_Request"}}</h3>
{{#if canSendTranscript}}
<button class="button rc-button rc-button--secondary button-block send-transcript"><span><i class="icon-mail"></i> {{_ "Transcript"}}</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}}
</nav>
{{/if}}

Expand Down
30 changes: 30 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.open && !room.onHold && settings.get('Livechat_allow_manual_on_hold') && room.lastMessage && !room.lastMessage?.token;
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
},

roomClosedDateTime() {
const { closedAt } = this;
return DateFormat.formatDateAndTime(closedAt);
Expand Down Expand Up @@ -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() {
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 },
renatobecker marked this conversation as resolved.
Show resolved Hide resolved
);
}

unsetOnHold(roomId) {
return this.update(
{ rid: roomId },
{ $unset: { onHold: 1 } },
{ multi: true },
renatobecker marked this conversation as resolved.
Show resolved Hide 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
4 changes: 4 additions & 0 deletions app/theme/client/imports/components/message-box.css
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,10 @@
margin: 0 0.5rem;
}

&__resume-it-button {
margin: 0 0.5rem;
}

&__cannot-send {
display: flex;
justify-content: space-between;
Expand Down
2 changes: 1 addition & 1 deletion app/ui-message/client/messageBox/messageBox.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
{{#if isBlockedOrBlocker}}
{{_ "room_is_blocked"}}
{{else}}
{{> messageBoxReadOnly rid=rid isSubscribed=isSubscribed}}
{{> messageBoxReadOnly rid=rid isSubscribed=isSubscribed onHold=onHold }}
{{/if}}
</div>
{{/if}}
Expand Down
9 changes: 8 additions & 1 deletion app/ui-message/client/messageBox/messageBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,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 +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);

Expand Down Expand Up @@ -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;
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', []);
renatobecker marked this conversation as resolved.
Show resolved Hide 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
2 changes: 2 additions & 0 deletions definition/IRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export interface IRoom extends IRocketChatRecord {
balance: number;
}[];
};

onHold?: boolean;
}

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
42 changes: 42 additions & 0 deletions ee/app/livechat-enterprise/lib/QueueManager.js
Original file line number Diff line number Diff line change
@@ -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);
},
};
1 change: 1 addition & 0 deletions ee/app/livechat-enterprise/server/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ import './priorities';
import './tags';
import './units';
import './business-hours';
import './rooms';
40 changes: 40 additions & 0 deletions ee/app/livechat-enterprise/server/api/rooms.js
Original file line number Diff line number Diff line change
@@ -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();
},
});
36 changes: 36 additions & 0 deletions ee/app/livechat-enterprise/server/hooks/afterOnHold.ts
Original file line number Diff line number Diff line change
@@ -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<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;
});
Loading