[NEW] On-Hold chats for Omnichannel (#4051)

* [NEW] Implementing On-Hold Livechat for Omnichannel

* added onHold to database

* list header title open livechats

* update rooms list view

* remove placeOnHold after clicked

* fix mesasgebox reactive to on hold

* navigate to roomslistview

* minor tweaks

* for grouping too

* fix chat on-hold when the agent is fully

* show on hold system messages
This commit is contained in:
Reinaldo Neto 2022-04-20 17:53:11 -03:00 committed by GitHub
parent a4090782f5
commit 8012031cd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 198 additions and 35 deletions

View File

@ -354,7 +354,8 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
blocks, blocks,
autoTranslate: autoTranslateMessage, autoTranslate: autoTranslateMessage,
replies, replies,
md md,
comment
} = item; } = item;
let message = msg; let message = msg;
@ -435,6 +436,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
callJitsi={callJitsi} callJitsi={callJitsi}
blockAction={blockAction} blockAction={blockAction}
highlighted={highlighted} highlighted={highlighted}
comment={comment}
/> />
</MessageContext.Provider> </MessageContext.Provider>
); );

View File

@ -59,6 +59,7 @@ export interface IMessageContent {
useRealName?: boolean; useRealName?: boolean;
isIgnored: boolean; isIgnored: boolean;
type: string; type: string;
comment?: string;
} }
export interface IMessageEmoji { export interface IMessageEmoji {

View File

@ -55,7 +55,9 @@ export const SYSTEM_MESSAGES = [
'user-converted-to-team', 'user-converted-to-team',
'user-converted-to-channel', 'user-converted-to-channel',
'user-deleted-room-from-team', 'user-deleted-room-from-team',
'user-removed-room-from-team' 'user-removed-room-from-team',
'omnichannel_placed_chat_on_hold',
'omnichannel_on_hold_chat_resumed'
]; ];
export const SYSTEM_MESSAGE_TYPES = { export const SYSTEM_MESSAGE_TYPES = {
@ -73,7 +75,9 @@ export const SYSTEM_MESSAGE_TYPES = {
CONVERTED_TO_TEAM: 'user-converted-to-team', CONVERTED_TO_TEAM: 'user-converted-to-team',
CONVERTED_TO_CHANNEL: 'user-converted-to-channel', CONVERTED_TO_CHANNEL: 'user-converted-to-channel',
DELETED_ROOM_FROM_TEAM: 'user-deleted-room-from-team', DELETED_ROOM_FROM_TEAM: 'user-deleted-room-from-team',
REMOVED_ROOM_FROM_TEAM: 'user-removed-room-from-team' REMOVED_ROOM_FROM_TEAM: 'user-removed-room-from-team',
OMNICHANNEL_PLACED_CHAT_ON_HOLD: 'omnichannel_placed_chat_on_hold',
OMNICHANNEL_ON_HOLD_CHAT_RESUMED: 'omnichannel_on_hold_chat_resumed'
}; };
export const SYSTEM_MESSAGE_TYPES_WITH_AUTHOR_NAME = [ export const SYSTEM_MESSAGE_TYPES_WITH_AUTHOR_NAME = [
@ -99,9 +103,10 @@ type TInfoMessage = {
role: string; role: string;
msg: string; msg: string;
author: { username: string }; author: { username: string };
comment?: string;
}; };
export const getInfoMessage = ({ type, role, msg, author }: TInfoMessage): string => { export const getInfoMessage = ({ type, role, msg, author, comment }: TInfoMessage): string => {
const { username } = author; const { username } = author;
if (type === 'rm') { if (type === 'rm') {
return I18n.t('Message_removed'); return I18n.t('Message_removed');
@ -193,6 +198,12 @@ export const getInfoMessage = ({ type, role, msg, author }: TInfoMessage): strin
if (type === 'user-removed-room-from-team') { if (type === 'user-removed-room-from-team') {
return I18n.t('Removed__roomName__from_this_team', { roomName: msg }); return I18n.t('Removed__roomName__from_this_team', { roomName: msg });
} }
if (type === 'omnichannel_placed_chat_on_hold') {
return I18n.t('Omnichannel_placed_chat_on_hold', { comment });
}
if (type === 'omnichannel_on_hold_chat_resumed') {
return I18n.t('Omnichannel_on_hold_chat_resumed', { comment });
}
return ''; return '';
}; };

View File

@ -60,6 +60,7 @@ export interface ILastMessage {
reactions?: IReaction[]; reactions?: IReaction[];
unread?: boolean; unread?: boolean;
status?: number; status?: number;
token?: string;
} }
interface IMessageFile { interface IMessageFile {
@ -140,6 +141,7 @@ export interface IMessage extends IMessageFromServer {
blocks?: any; blocks?: any;
e2e?: E2EType; e2e?: E2EType;
tshow?: boolean; tshow?: boolean;
comment?: string;
subscription?: { id: string }; subscription?: { id: string };
} }

View File

@ -55,6 +55,8 @@ export interface IRoom {
uids: Array<string>; uids: Array<string>;
lm?: Date; lm?: Date;
sysMes?: string[]; sysMes?: string[];
onHold?: boolean;
waitingResponse?: boolean;
} }
export enum OmnichannelSourceType { export enum OmnichannelSourceType {

View File

@ -98,6 +98,7 @@ export interface ISubscription {
teamMain?: boolean; teamMain?: boolean;
unsubscribe: () => Promise<any>; unsubscribe: () => Promise<any>;
separator?: boolean; separator?: boolean;
onHold?: boolean;
source?: IOmnichannelSource; source?: IOmnichannelSource;
// https://nozbe.github.io/WatermelonDB/Relation.html#relation-api // https://nozbe.github.io/WatermelonDB/Relation.html#relation-api
messages: RelationModified<TMessageModel>; messages: RelationModified<TMessageModel>;

View File

@ -17,6 +17,9 @@ export const getInquiriesQueued = () => sdk.get('livechat/inquiries.queued');
// RC 2.4.0 // RC 2.4.0
export const takeInquiry = (inquiryId: string) => sdk.methodCallWrapper('livechat:takeInquiry', inquiryId); export const takeInquiry = (inquiryId: string) => sdk.methodCallWrapper('livechat:takeInquiry', inquiryId);
// RC 4.26
export const takeResume = (roomId: string) => sdk.methodCallWrapper('livechat:resumeOnHold', roomId);
class Omnichannel { class Omnichannel {
private inquirySub: { stop: () => void } | null; private inquirySub: { stop: () => void } | null;
constructor() { constructor() {

View File

@ -358,7 +358,6 @@
"Offline": "غير متصل", "Offline": "غير متصل",
"Oops": "عفوًا!", "Oops": "عفوًا!",
"Omnichannel": "القنوات الموحدة", "Omnichannel": "القنوات الموحدة",
"Open_Livechats": "محادثات مباشرة جارية",
"Omnichannel_enable_alert": "أنت غير متاح ", "Omnichannel_enable_alert": "أنت غير متاح ",
"Onboarding_description": "مساحة عمل هي مساحة لفريقك ومنظمتك للتعاون. اطلب من المشرف العنوان للانضمام أو أنشئ لفريقك", "Onboarding_description": "مساحة عمل هي مساحة لفريقك ومنظمتك للتعاون. اطلب من المشرف العنوان للانضمام أو أنشئ لفريقك",
"Onboarding_join_workspace": "انضم لمساحة عمل", "Onboarding_join_workspace": "انضم لمساحة عمل",

View File

@ -363,7 +363,6 @@
"Offline": "Offline", "Offline": "Offline",
"Oops": "Hoppla!", "Oops": "Hoppla!",
"Omnichannel": "Omnichannel", "Omnichannel": "Omnichannel",
"Open_Livechats": "Offene Chats",
"Omnichannel_enable_alert": "Sie sind in Omnichannel nicht verfügbar. Möchten Sie erreichbar sein?", "Omnichannel_enable_alert": "Sie sind in Omnichannel nicht verfügbar. Möchten Sie erreichbar sein?",
"Onboarding_description": "Ein Arbeitsbereich ist der Ort für die Zusammenarbeit deines Teams oder Organisation. Bitten Sie den Admin des Arbeitsbereichs um eine Adresse, um ihm beizutreten, oder erstellen Sie einen Arbeitsbereich für Ihr Team.", "Onboarding_description": "Ein Arbeitsbereich ist der Ort für die Zusammenarbeit deines Teams oder Organisation. Bitten Sie den Admin des Arbeitsbereichs um eine Adresse, um ihm beizutreten, oder erstellen Sie einen Arbeitsbereich für Ihr Team.",
"Onboarding_join_workspace": "Einem Arbeitsbereich beiteten", "Onboarding_join_workspace": "Einem Arbeitsbereich beiteten",

View File

@ -363,7 +363,6 @@
"Offline": "Offline", "Offline": "Offline",
"Oops": "Oops!", "Oops": "Oops!",
"Omnichannel": "Omnichannel", "Omnichannel": "Omnichannel",
"Open_Livechats": "Chats in Progress",
"Omnichannel_enable_alert": "You're not available on Omnichannel. Would you like to be available?", "Omnichannel_enable_alert": "You're not available on Omnichannel. Would you like to be available?",
"Onboarding_description": "A workspace is your team or organizations space to collaborate. Ask the workspace admin for address to join or create one for your team.", "Onboarding_description": "A workspace is your team or organizations space to collaborate. Ask the workspace admin for address to join or create one for your team.",
"Onboarding_join_workspace": "Join a workspace", "Onboarding_join_workspace": "Join a workspace",
@ -808,6 +807,14 @@
"Removed__username__from_team": "removed @{{user_removed}} from this Team", "Removed__username__from_team": "removed @{{user_removed}} from this Team",
"User_joined_team": "joined this Team", "User_joined_team": "joined this Team",
"User_left_team": "left this Team", "User_left_team": "left this Team",
"Place_chat_on_hold": "Place chat on-hold",
"Would_like_to_place_on_hold": "Would you like to place this chat On-Hold?",
"Open_Livechats": "Omnichannel chats in progress",
"On_hold_Livechats": "Omnichannel chats on hold",
"Chat_is_on_hold": "This chat is on-hold due to inactivity",
"Resume": "Resume",
"Omnichannel_placed_chat_on_hold": "Chat On Hold: {{comment}}",
"Omnichannel_on_hold_chat_resumed": "On Hold Chat Resumed: {{comment}}",
"Omnichannel_queue": "Omnichannel queue", "Omnichannel_queue": "Omnichannel queue",
"Empty": "Empty" "Empty": "Empty"
} }

View File

@ -363,7 +363,6 @@
"Offline": "Hors ligne", "Offline": "Hors ligne",
"Oops": "Oups !", "Oops": "Oups !",
"Omnichannel": "Omnicanal", "Omnichannel": "Omnicanal",
"Open_Livechats": "Discussions en cours",
"Omnichannel_enable_alert": "Vous n'êtes pas disponible sur Omnicanal. Souhaitez-vous être disponible ?", "Omnichannel_enable_alert": "Vous n'êtes pas disponible sur Omnicanal. Souhaitez-vous être disponible ?",
"Onboarding_description": "Un espace de travail est l'espace de collaboration de votre équipe ou organisation. Demandez à l'administrateur de l'espace de travail l'adresse pour rejoindre ou créez-en une pour votre équipe.", "Onboarding_description": "Un espace de travail est l'espace de collaboration de votre équipe ou organisation. Demandez à l'administrateur de l'espace de travail l'adresse pour rejoindre ou créez-en une pour votre équipe.",
"Onboarding_join_workspace": "Rejoindre un espace de travail", "Onboarding_join_workspace": "Rejoindre un espace de travail",

View File

@ -352,7 +352,6 @@
"Offline": "Offline", "Offline": "Offline",
"Oops": "Oops!", "Oops": "Oops!",
"Omnichannel": "Omnichannel", "Omnichannel": "Omnichannel",
"Open_Livechats": "Chat in corso",
"Omnichannel_enable_alert": "Non sei ancora su Onmichannel. Vuoi attivarlo?", "Omnichannel_enable_alert": "Non sei ancora su Onmichannel. Vuoi attivarlo?",
"Onboarding_description": "Un workspace è lo spazio dove il tuo team o la tua organizzazione possono collaborare. Chiedi l'indirizzo all'amministratore del tuo workspace oppure creane uno per il tuo team.", "Onboarding_description": "Un workspace è lo spazio dove il tuo team o la tua organizzazione possono collaborare. Chiedi l'indirizzo all'amministratore del tuo workspace oppure creane uno per il tuo team.",
"Onboarding_join_workspace": "Unisciti", "Onboarding_join_workspace": "Unisciti",

View File

@ -363,7 +363,6 @@
"Offline": "Offline", "Offline": "Offline",
"Oops": "Oeps!", "Oops": "Oeps!",
"Omnichannel": "Omnichannel", "Omnichannel": "Omnichannel",
"Open_Livechats": "Bezig met chatten",
"Omnichannel_enable_alert": "Je bent niet beschikbaar op Omnichannel. Wil je beschikbaar zijn?", "Omnichannel_enable_alert": "Je bent niet beschikbaar op Omnichannel. Wil je beschikbaar zijn?",
"Onboarding_description": "Een werkruimte is de ruimte van jouw team of organisatie om samen te werken. Vraag aan de beheerder van de werkruimte een adres om lid te worden of maak er een aan voor jouw team.", "Onboarding_description": "Een werkruimte is de ruimte van jouw team of organisatie om samen te werken. Vraag aan de beheerder van de werkruimte een adres om lid te worden of maak er een aan voor jouw team.",
"Onboarding_join_workspace": "Word lid van een werkruimte", "Onboarding_join_workspace": "Word lid van een werkruimte",

View File

@ -339,7 +339,6 @@
"Offline": "Offline", "Offline": "Offline",
"Oops": "Ops!", "Oops": "Ops!",
"Omnichannel": "Omnichannel", "Omnichannel": "Omnichannel",
"Open_Livechats": "Bate-papos em Andamento",
"Omnichannel_enable_alert": "Você não está disponível no Omnichannel. Você quer ficar disponível?", "Omnichannel_enable_alert": "Você não está disponível no Omnichannel. Você quer ficar disponível?",
"Onboarding_description": "Workspace é o espaço de colaboração do seu time ou organização. Peça um convite ou o endereço ao seu administrador ou crie uma workspace para o seu time.", "Onboarding_description": "Workspace é o espaço de colaboração do seu time ou organização. Peça um convite ou o endereço ao seu administrador ou crie uma workspace para o seu time.",
"Onboarding_join_workspace": "Entre numa workspace", "Onboarding_join_workspace": "Entre numa workspace",

View File

@ -359,7 +359,6 @@
"Offline": "Desligado", "Offline": "Desligado",
"Oops": "Oops!", "Oops": "Oops!",
"Omnichannel": "Omnichannel", "Omnichannel": "Omnichannel",
"Open_Livechats": "Chats em andamento",
"Omnichannel_enable_alert": "Você não está disponível no Omnichannel. Você gostaria de estar disponível?", "Omnichannel_enable_alert": "Você não está disponível no Omnichannel. Você gostaria de estar disponível?",
"Onboarding_description": "Um espaço de trabalho é o espaço da sua equipa ou organização para colaborar. Peça ao administrador do espaço de trabalho um endereço para se juntar ou criar um para a sua equipa.", "Onboarding_description": "Um espaço de trabalho é o espaço da sua equipa ou organização para colaborar. Peça ao administrador do espaço de trabalho um endereço para se juntar ou criar um para a sua equipa.",
"Onboarding_join_workspace": "Junte-se a um espaço de trabalho", "Onboarding_join_workspace": "Junte-se a um espaço de trabalho",

View File

@ -363,7 +363,6 @@
"Offline": "Офлайн", "Offline": "Офлайн",
"Oops": "Упс!", "Oops": "Упс!",
"Omnichannel": "Omnichannel", "Omnichannel": "Omnichannel",
"Open_Livechats": "Чаты в Работе",
"Omnichannel_enable_alert": "Вы не доступны в Omnichannel. Хотите стать доступными?", "Omnichannel_enable_alert": "Вы не доступны в Omnichannel. Хотите стать доступными?",
"Onboarding_description": "Сервер это пространство для взаимодействия вашей команды или организации. Уточните адрес сервера у вашего администратора или создайте свой сервер для команды.", "Onboarding_description": "Сервер это пространство для взаимодействия вашей команды или организации. Уточните адрес сервера у вашего администратора или создайте свой сервер для команды.",
"Onboarding_join_workspace": "Присоединиться к серверу", "Onboarding_join_workspace": "Присоединиться к серверу",

View File

@ -353,7 +353,6 @@
"Offline": "Çevrimdışı", "Offline": "Çevrimdışı",
"Oops": "Ahh!", "Oops": "Ahh!",
"Omnichannel": "Çoklu Kanal", "Omnichannel": "Çoklu Kanal",
"Open_Livechats": "Devam Eden Sohbetler",
"Omnichannel_enable_alert": "Çoklu Kanal'da mevcut değilsiniz. Erişilebilir olmak ister misiniz?", "Omnichannel_enable_alert": "Çoklu Kanal'da mevcut değilsiniz. Erişilebilir olmak ister misiniz?",
"Onboarding_description": "Çalışma alanı, ekibinizin veya kuruluşunuzun işbirliği alanıdır. Çalışma alanı yöneticisinden bir ekibe katılmak veya bir ekip oluşturmak için yardım isteyin.", "Onboarding_description": "Çalışma alanı, ekibinizin veya kuruluşunuzun işbirliği alanıdır. Çalışma alanı yöneticisinden bir ekibe katılmak veya bir ekip oluşturmak için yardım isteyin.",
"Onboarding_join_workspace": "Bir çalışma alanına katılın", "Onboarding_join_workspace": "Bir çalışma alanına katılın",

View File

@ -350,7 +350,6 @@
"Offline": "离线", "Offline": "离线",
"Oops": "哎呀!", "Oops": "哎呀!",
"Omnichannel": "Omnichannel", "Omnichannel": "Omnichannel",
"Open_Livechats": "打开即时聊天",
"Omnichannel_enable_alert": "您尚未启用 Omnichannel是否想要启用", "Omnichannel_enable_alert": "您尚未启用 Omnichannel是否想要启用",
"Onboarding_description": "工作区是团队或组织协作的空间。向工作区管理员询问要加入的地址或为您的团队创建一个。", "Onboarding_description": "工作区是团队或组织协作的空间。向工作区管理员询问要加入的地址或为您的团队创建一个。",
"Onboarding_join_workspace": "加入一个工作区", "Onboarding_join_workspace": "加入一个工作区",

View File

@ -352,7 +352,6 @@
"Offline": "離線", "Offline": "離線",
"Oops": "哎呀!", "Oops": "哎呀!",
"Omnichannel": "Omnichannel", "Omnichannel": "Omnichannel",
"Open_Livechats": "打開即時聊天",
"Omnichannel_enable_alert": "您尚未啟用 Omnichannel是否想要啟用", "Omnichannel_enable_alert": "您尚未啟用 Omnichannel是否想要啟用",
"Onboarding_description": "工作區是團隊或組織協作的空間。向工作區管理員詢問要加入的地址或為您的團隊創建一個。", "Onboarding_description": "工作區是團隊或組織協作的空間。向工作區管理員詢問要加入的地址或為您的團隊創建一個。",
"Onboarding_join_workspace": "加入一個工作區", "Onboarding_join_workspace": "加入一個工作區",

View File

@ -206,6 +206,9 @@ export const defaultSettings = {
Canned_Responses_Enable: { Canned_Responses_Enable: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
}, },
Livechat_allow_manual_on_hold: {
type: 'valueAsBoolean'
},
Accounts_AvatarExternalProviderUrl: { Accounts_AvatarExternalProviderUrl: {
type: 'valueAsString' type: 'valueAsString'
} }

View File

@ -83,4 +83,6 @@ export default class Message extends Model {
@field('tshow') tshow; @field('tshow') tshow;
@json('md', sanitizer) md; @json('md', sanitizer) md;
@field('comment') comment;
} }

View File

@ -131,5 +131,7 @@ export default class Subscription extends Model {
@field('team_main') teamMain; @field('team_main') teamMain;
@field('on_hold') onHold;
@json('source', sanitizer) source; @json('source', sanitizer) source;
} }

View File

@ -217,6 +217,19 @@ export default schemaMigrations({
columns: [{ name: 'source', type: 'string', isOptional: true }] columns: [{ name: 'source', type: 'string', isOptional: true }]
}) })
] ]
},
{
toVersion: 17,
steps: [
addColumns({
table: 'subscriptions',
columns: [{ name: 'on_hold', type: 'boolean', isOptional: true }]
}),
addColumns({
table: 'messages',
columns: [{ name: 'comment', type: 'string', isOptional: true }]
})
]
} }
] ]
}); });

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb'; import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({ export default appSchema({
version: 16, version: 17,
tables: [ tables: [
tableSchema({ tableSchema({
name: 'subscriptions', name: 'subscriptions',
@ -60,6 +60,7 @@ export default appSchema({
{ name: 'avatar_etag', type: 'string', isOptional: true }, { name: 'avatar_etag', type: 'string', isOptional: true },
{ name: 'team_id', type: 'string', isIndexed: true }, { name: 'team_id', type: 'string', isIndexed: true },
{ name: 'team_main', type: 'boolean', isOptional: true }, // Use `Q.notEq(true)` to get false or null { name: 'team_main', type: 'boolean', isOptional: true }, // Use `Q.notEq(true)` to get false or null
{ name: 'on_hold', type: 'boolean', isOptional: true },
{ name: 'source', type: 'string', isOptional: true } { name: 'source', type: 'string', isOptional: true }
] ]
}), }),
@ -117,7 +118,8 @@ export default appSchema({
{ name: 'blocks', type: 'string', isOptional: true }, { name: 'blocks', type: 'string', isOptional: true },
{ name: 'e2e', type: 'string', isOptional: true }, { name: 'e2e', type: 'string', isOptional: true },
{ name: 'tshow', type: 'boolean', isOptional: true }, { name: 'tshow', type: 'boolean', isOptional: true },
{ name: 'md', type: 'string', isOptional: true } { name: 'md', type: 'string', isOptional: true },
{ name: 'comment', type: 'string', isOptional: true }
] ]
}), }),
tableSchema({ tableSchema({

View File

@ -26,7 +26,8 @@ import {
TMessageModel, TMessageModel,
TRoomModel, TRoomModel,
TThreadMessageModel, TThreadMessageModel,
TThreadModel TThreadModel,
SubscriptionType
} from '../../../definitions'; } from '../../../definitions';
import sdk from '../../services/sdk'; import sdk from '../../services/sdk';
import { IDDPMessage } from '../../../definitions/IDDPMessage'; import { IDDPMessage } from '../../../definitions/IDDPMessage';
@ -99,7 +100,8 @@ const createOrUpdateSubscription = async (subscription: ISubscription, room: ISe
encrypted: s.encrypted, encrypted: s.encrypted,
e2eKeyId: s.e2eKeyId, e2eKeyId: s.e2eKeyId,
E2EKey: s.E2EKey, E2EKey: s.E2EKey,
avatarETag: s.avatarETag avatarETag: s.avatarETag,
onHold: s.onHold
} as ISubscription; } as ISubscription;
} catch (error) { } catch (error) {
try { try {
@ -251,6 +253,11 @@ const debouncedUpdate = (subscription: ISubscription) => {
createOrUpdateSubscription(sub, room); createOrUpdateSubscription(sub, room);
} else { } else {
const room = batch[key] as IRoom; const room = batch[key] as IRoom;
// If the omnichannel's chat is onHold and waitingResponse we shouldn't create or update the chat,
// because it should go to Queue
if (room.t === SubscriptionType.OMNICHANNEL && room.onHold && room.waitingResponse) {
return null;
}
const subQueueId = getSubQueueId(room._id); const subQueueId = getSubQueueId(room._id);
const sub = batch[subQueueId] as ISubscription; const sub = batch[subQueueId] as ISubscription;
delete batch[subQueueId]; delete batch[subQueueId];

View File

@ -360,6 +360,8 @@ export const returnLivechat = (rid: string): Promise<boolean> =>
// RC 0.72.0 // RC 0.72.0
sdk.methodCallWrapper('livechat:returnAsInquiry', rid); sdk.methodCallWrapper('livechat:returnAsInquiry', rid);
export const onHoldLivechat = (roomId: string) => sdk.post('livechat/room.onHold', { roomId });
export const forwardLivechat = (transferData: any) => export const forwardLivechat = (transferData: any) =>
// RC 0.36.0 // RC 0.36.0
sdk.methodCallWrapper('livechat:transfer', transferData); sdk.methodCallWrapper('livechat:transfer', transferData);

View File

@ -226,6 +226,7 @@ export default {
ROOM_MSG_ACTION_REPORT: 'room_msg_action_report', ROOM_MSG_ACTION_REPORT: 'room_msg_action_report',
ROOM_MSG_ACTION_REPORT_F: 'room_msg_action_report_f', ROOM_MSG_ACTION_REPORT_F: 'room_msg_action_report_f',
ROOM_JOIN: 'room_join', ROOM_JOIN: 'room_join',
ROOM_RESUME: 'room_resume',
ROOM_GO_RA: 'room_go_ra', ROOM_GO_RA: 'room_go_ra',
ROOM_TOGGLE_FOLLOW_THREADS: 'room_toggle_follow_threads', ROOM_TOGGLE_FOLLOW_THREADS: 'room_toggle_follow_threads',
ROOM_GO_TEAM_CHANNELS: 'room_go_team_channels', ROOM_GO_TEAM_CHANNELS: 'room_go_team_channels',

View File

@ -63,6 +63,7 @@ interface IRoomActionsViewProps extends IBaseScreen<ChatsStackParamList, 'RoomAc
addTeamChannelPermission?: string[]; addTeamChannelPermission?: string[];
convertTeamPermission?: string[]; convertTeamPermission?: string[];
viewCannedResponsesPermission?: string[]; viewCannedResponsesPermission?: string[];
livechatAllowManualOnHold?: boolean;
} }
interface IRoomActionsViewState { interface IRoomActionsViewState {
@ -82,6 +83,8 @@ interface IRoomActionsViewState {
canAddChannelToTeam: boolean; canAddChannelToTeam: boolean;
canConvertTeam: boolean; canConvertTeam: boolean;
canViewCannedResponse: boolean; canViewCannedResponse: boolean;
canPlaceLivechatOnHold: boolean;
isOnHold: boolean;
} }
class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomActionsViewState> { class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomActionsViewState> {
@ -129,13 +132,15 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
canCreateTeam: false, canCreateTeam: false,
canAddChannelToTeam: false, canAddChannelToTeam: false,
canConvertTeam: false, canConvertTeam: false,
canViewCannedResponse: false canViewCannedResponse: false,
canPlaceLivechatOnHold: false,
isOnHold: false
}; };
if (room && room.observe && room.rid) { if (room && room.observe && room.rid) {
this.roomObservable = room.observe(); this.roomObservable = room.observe();
this.subscription = this.roomObservable.subscribe(changes => { this.subscription = this.roomObservable.subscribe(changes => {
if (this.mounted) { if (this.mounted) {
this.setState({ room: changes }); this.setState({ room: changes, isOnHold: !!changes?.onHold });
} else { } else {
// @ts-ignore // @ts-ignore
this.state.room = changes; this.state.room = changes;
@ -209,11 +214,25 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
const canForwardGuest = await this.canForwardGuest(); const canForwardGuest = await this.canForwardGuest();
const canReturnQueue = await this.canReturnQueue(); const canReturnQueue = await this.canReturnQueue();
const canViewCannedResponse = await this.canViewCannedResponse(); const canViewCannedResponse = await this.canViewCannedResponse();
this.setState({ canForwardGuest, canReturnQueue, canViewCannedResponse }); const canPlaceLivechatOnHold = this.canPlaceLivechatOnHold();
this.setState({ canForwardGuest, canReturnQueue, canViewCannedResponse, canPlaceLivechatOnHold });
} }
} }
} }
componentDidUpdate(prevProps: IRoomActionsViewProps, prevState: IRoomActionsViewState) {
const { livechatAllowManualOnHold } = this.props;
const { room, isOnHold } = this.state;
if (
room.t === 'l' &&
(isOnHold !== prevState.isOnHold || prevProps.livechatAllowManualOnHold !== livechatAllowManualOnHold)
) {
const canPlaceLivechatOnHold = this.canPlaceLivechatOnHold();
this.setState({ canPlaceLivechatOnHold });
}
}
componentWillUnmount() { componentWillUnmount() {
if (this.subscription && this.subscription.unsubscribe) { if (this.subscription && this.subscription.unsubscribe) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
@ -360,6 +379,13 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
return permissions[0]; return permissions[0];
}; };
canPlaceLivechatOnHold = (): boolean => {
const { livechatAllowManualOnHold } = this.props;
const { room } = this.state;
return !!(livechatAllowManualOnHold && !room?.lastMessage?.token && room?.lastMessage?.u && !room.onHold);
};
canReturnQueue = async () => { canReturnQueue = async () => {
try { try {
const { returnQueue } = await RocketChat.getRoutingConfig(); const { returnQueue } = await RocketChat.getRoutingConfig();
@ -393,6 +419,24 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
dispatch(closeRoom(rid)); dispatch(closeRoom(rid));
}; };
placeOnHoldLivechat = () => {
const { navigation } = this.props;
const { room } = this.state;
showConfirmationAlert({
title: I18n.t('Are_you_sure_question_mark'),
message: I18n.t('Would_like_to_place_on_hold'),
confirmationText: I18n.t('Yes'),
onPress: async () => {
try {
await RocketChat.onHoldLivechat(room.rid);
navigation.navigate('RoomsListView');
} catch (e: any) {
showErrorAlert(e.data?.error, I18n.t('Oops'));
}
}
});
};
returnLivechat = () => { returnLivechat = () => {
const { const {
room: { rid } room: { rid }
@ -1005,7 +1049,8 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
canAutoTranslate, canAutoTranslate,
canForwardGuest, canForwardGuest,
canReturnQueue, canReturnQueue,
canViewCannedResponse canViewCannedResponse,
canPlaceLivechatOnHold
} = this.state; } = this.state;
const { rid, t, prid } = room; const { rid, t, prid } = room;
const isGroupChat = RocketChat.isGroupChat(room); const isGroupChat = RocketChat.isGroupChat(room);
@ -1269,6 +1314,22 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
</> </>
) : null} ) : null}
{['l'].includes(t) && !this.isOmnichannelPreview && canPlaceLivechatOnHold ? (
<>
<List.Item
title='Place_chat_on_hold'
onPress={() =>
this.onPressTouchable({
event: this.placeOnHoldLivechat
})
}
left={() => <List.Icon name='pause' />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
{['l'].includes(t) && !this.isOmnichannelPreview && canReturnQueue ? ( {['l'].includes(t) && !this.isOmnichannelPreview && canReturnQueue ? (
<> <>
<List.Item <List.Item
@ -1312,7 +1373,8 @@ const mapStateToProps = (state: IApplicationState) => ({
createTeamPermission: state.permissions['create-team'], createTeamPermission: state.permissions['create-team'],
addTeamChannelPermission: state.permissions['add-team-channel'], addTeamChannelPermission: state.permissions['add-team-channel'],
convertTeamPermission: state.permissions['convert-team'], convertTeamPermission: state.permissions['convert-team'],
viewCannedResponsesPermission: state.permissions['view-canned-responses'] viewCannedResponsesPermission: state.permissions['view-canned-responses'],
livechatAllowManualOnHold: state.settings.Livechat_allow_manual_on_hold as boolean
}); });
export default connect(mapStateToProps)(withTheme(withDimensions(RoomActionsView))); export default connect(mapStateToProps)(withTheme(withDimensions(RoomActionsView)));

View File

@ -46,7 +46,7 @@ import Navigation from '../../lib/navigation/appNavigation';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
import { withDimensions } from '../../dimensions'; import { withDimensions } from '../../dimensions';
import { getHeaderTitlePosition } from '../../containers/Header'; import { getHeaderTitlePosition } from '../../containers/Header';
import { takeInquiry } from '../../ee/omnichannel/lib'; import { takeInquiry, takeResume } from '../../ee/omnichannel/lib';
import Loading from '../../containers/Loading'; import Loading from '../../containers/Loading';
import { goRoom, TGoRoomItem } from '../../utils/goRoom'; import { goRoom, TGoRoomItem } from '../../utils/goRoom';
import getThreadName from '../../lib/methods/getThreadName'; import getThreadName from '../../lib/methods/getThreadName';
@ -116,7 +116,8 @@ const roomAttrsUpdate = [
'visitor', 'visitor',
'joinCodeRequired', 'joinCodeRequired',
'teamMain', 'teamMain',
'teamId' 'teamId',
'onHold'
] as const; ] as const;
interface IRoomViewProps extends IBaseScreen<ChatsStackParamList, 'RoomView'> { interface IRoomViewProps extends IBaseScreen<ChatsStackParamList, 'RoomView'> {
@ -958,6 +959,22 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
} }
}; };
resumeRoom = async () => {
logEvent(events.ROOM_RESUME);
try {
const { room } = this.state;
if (this.isOmnichannel) {
if ('rid' in room) {
await takeResume(room.rid);
}
this.onJoin();
}
} catch (e) {
log(e);
}
};
getThreadName = (tmid: string, messageId: string) => { getThreadName = (tmid: string, messageId: string) => {
const { rid } = this.state.room; const { rid } = this.state.room;
return getThreadName(rid, tmid, messageId); return getThreadName(rid, tmid, messageId);
@ -1243,6 +1260,24 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
if (!this.rid) { if (!this.rid) {
return null; return null;
} }
if ('onHold' in room && room.onHold) {
return (
<View style={styles.joinRoomContainer} key='room-view-chat-on-hold' testID='room-view-chat-on-hold'>
<Text accessibilityLabel={I18n.t('Chat_is_on_hold')} style={[styles.previewMode, { color: themes[theme].titleText }]}>
{I18n.t('Chat_is_on_hold')}
</Text>
<Touch
onPress={this.resumeRoom}
style={[styles.joinRoomButton, { backgroundColor: themes[theme].actionTintColor }]}
enabled={!loading}
theme={theme}>
<Text style={[styles.joinRoomText, { color: themes[theme].buttonText }]} testID='room-view-chat-on-hold-button'>
{I18n.t('Resume')}
</Text>
</Touch>
</View>
);
}
if (!joined && !this.tmid) { if (!joined && !this.tmid) {
return ( return (
<View style={styles.joinRoomContainer} key='room-view-join' testID='room-view-join'> <View style={styles.joinRoomContainer} key='room-view-join' testID='room-view-join'>

View File

@ -88,7 +88,8 @@ interface IRoomsListViewState {
searching: boolean; searching: boolean;
search: ISubscription[]; search: ISubscription[];
loading: boolean; loading: boolean;
chatsUpdate: []; chatsUpdate: string[];
omnichannelsUpdate: string[];
chats: ISubscription[]; chats: ISubscription[];
item: ISubscription; item: ISubscription;
canCreateRoom: boolean; canCreateRoom: boolean;
@ -107,7 +108,8 @@ const DISCUSSIONS_HEADER = 'Discussions';
const TEAMS_HEADER = 'Teams'; const TEAMS_HEADER = 'Teams';
const CHANNELS_HEADER = 'Channels'; const CHANNELS_HEADER = 'Channels';
const DM_HEADER = 'Direct_Messages'; const DM_HEADER = 'Direct_Messages';
const OMNICHANNEL_HEADER = 'Open_Livechats'; const OMNICHANNEL_HEADER_IN_PROGRESS = 'Open_Livechats';
const OMNICHANNEL_HEADER_ON_HOLD = 'On_hold_Livechats';
const QUERY_SIZE = 20; const QUERY_SIZE = 20;
const filterIsUnread = (s: TSubscriptionModel) => (s.unread > 0 || s.tunread?.length > 0 || s.alert) && !s.hideUnreadStatus; const filterIsUnread = (s: TSubscriptionModel) => (s.unread > 0 || s.tunread?.length > 0 || s.alert) && !s.hideUnreadStatus;
@ -172,6 +174,7 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
search: [], search: [],
loading: true, loading: true,
chatsUpdate: [], chatsUpdate: [],
omnichannelsUpdate: [],
chats: [], chats: [],
item: {} as ISubscription, item: {} as ISubscription,
canCreateRoom: false canCreateRoom: false
@ -231,7 +234,7 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
} }
shouldComponentUpdate(nextProps: IRoomsListViewProps, nextState: IRoomsListViewState) { shouldComponentUpdate(nextProps: IRoomsListViewProps, nextState: IRoomsListViewState) {
const { chatsUpdate, searching, item, canCreateRoom } = this.state; const { chatsUpdate, searching, item, canCreateRoom, omnichannelsUpdate } = this.state;
// eslint-disable-next-line react/destructuring-assignment // eslint-disable-next-line react/destructuring-assignment
const propsUpdated = shouldUpdateProps.some(key => nextProps[key] !== this.props[key]); const propsUpdated = shouldUpdateProps.some(key => nextProps[key] !== this.props[key]);
if (propsUpdated) { if (propsUpdated) {
@ -260,6 +263,12 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
this.shouldUpdate = true; this.shouldUpdate = true;
} }
const omnichannelsNotEqual = !dequal(nextState.omnichannelsUpdate, omnichannelsUpdate);
if (omnichannelsNotEqual) {
this.shouldUpdate = true;
}
if (nextState.searching !== searching) { if (nextState.searching !== searching) {
return true; return true;
} }
@ -295,7 +304,7 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
return true; return true;
} }
// If it's focused and there are changes, update // If it's focused and there are changes, update
if (chatsNotEqual) { if (chatsNotEqual || omnichannelsNotEqual) {
this.shouldUpdate = false; this.shouldUpdate = false;
return true; return true;
} }
@ -480,21 +489,21 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
observable = await db observable = await db
.get('subscriptions') .get('subscriptions')
.query(...defaultWhereClause) .query(...defaultWhereClause)
.observeWithColumns(['alert']); .observeWithColumns(['alert', 'on_hold']);
// When we're NOT grouping // When we're NOT grouping
} else { } else {
this.count += QUERY_SIZE; this.count += QUERY_SIZE;
observable = await db observable = await db
.get('subscriptions') .get('subscriptions')
.query(...defaultWhereClause, Q.experimentalSkip(0), Q.experimentalTake(this.count)) .query(...defaultWhereClause, Q.experimentalSkip(0), Q.experimentalTake(this.count))
.observe(); .observeWithColumns(['on_hold']);
} }
this.querySubscription = observable.subscribe(data => { this.querySubscription = observable.subscribe(data => {
let tempChats = [] as TSubscriptionModel[]; let tempChats = [] as TSubscriptionModel[];
let chats = data; let chats = data;
let omnichannelsUpdate: string[] = [];
let chatsUpdate = []; let chatsUpdate = [];
if (showUnread) { if (showUnread) {
/** /**
@ -513,8 +522,12 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
const isOmnichannelAgent = user?.roles?.includes('livechat-agent'); const isOmnichannelAgent = user?.roles?.includes('livechat-agent');
if (isOmnichannelAgent) { if (isOmnichannelAgent) {
const omnichannel = chats.filter(s => filterIsOmnichannel(s)); const omnichannel = chats.filter(s => filterIsOmnichannel(s));
const omnichannelInProgress = omnichannel.filter(s => !s.onHold);
const omnichannelOnHold = omnichannel.filter(s => s.onHold);
chats = chats.filter(s => !filterIsOmnichannel(s)); chats = chats.filter(s => !filterIsOmnichannel(s));
tempChats = this.addRoomsGroup(omnichannel, OMNICHANNEL_HEADER, tempChats); omnichannelsUpdate = omnichannelInProgress.map(s => s.rid);
tempChats = this.addRoomsGroup(omnichannelInProgress, OMNICHANNEL_HEADER_IN_PROGRESS, tempChats);
tempChats = this.addRoomsGroup(omnichannelOnHold, OMNICHANNEL_HEADER_ON_HOLD, tempChats);
} }
// unread // unread
@ -551,6 +564,7 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
this.internalSetState({ this.internalSetState({
chats: tempChats, chats: tempChats,
chatsUpdate, chatsUpdate,
omnichannelsUpdate,
loading: false loading: false
}); });
} else { } else {
@ -559,6 +573,8 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
// @ts-ignore // @ts-ignore
this.state.chatsUpdate = chatsUpdate; this.state.chatsUpdate = chatsUpdate;
// @ts-ignore // @ts-ignore
this.state.omnichannelsUpdate = omnichannelsUpdate;
// @ts-ignore
this.state.loading = false; this.state.loading = false;
} }
}); });