Setting up the Chat channel
Unlike calls, there can be multiple chats at the same time. When the chat channel is active, the agent cannot receive calls or tasks but can still make an outgoing call.
Let's add a ChatContainer.tsx
file
ChatContainer.tsx
import { useRef, useState } from 'react';
import { clearContact, downloadAttachment, endContact } from '@neev/ccp-api';
import {
AfterContactWorkCard,
ChatAttachmentButton,
ChatInput,
ChatMessages,
ContactFooter,
ContactHeader,
EndButton,
TypingIndicator,
useChatMessages,
} from '@neev/ccp-react';
import { Logger } from '@neev/logger';
import ChatLexComponent from './ChatLexComponent';
const TYPING_TIMEOUT = 5_000;
const log = new Logger('ChatContainer');
const ChatContainer = ({ contact }: { contact: connect.Contact }) => {
// Messages coming from onMessage and onTranscript will be populated in this state
const [messages, setMessages] = useState<connect.ChatTranscriptItem[]>([]);
// A state to determine whether the customer is typing or not
const [isTyping, setTyping] = useState(false);
const typingTimeout = useRef(0); // nosemgrep
// A session object is necessary to send the message, add attachments and also to download the attachments
const session = useChatMessages(contact.getContactId(), {
// this gets invoked when a message is received in the channel
onMessage: (e) => {
log.debug('onMessage', e);
setMessages((prevMessages) => [...prevMessages, e.data]);
},
// this gets invoked at the start which lets us know the previous conversation
onTranscript: (e) => {
log.debug('onTranscript', e);
if (e.data) {
setMessages((prevMessages) => {
return e.data ? [...prevMessages, ...e.data.Transcript] : prevMessages;
});
}
},
// Lets us know the customer is typing
onTyping: () => {
setTyping(true);
if (typingTimeout.current) {
window.clearTimeout(typingTimeout.current);
}
typingTimeout.current = window.setTimeout(() => setTyping(false), TYPING_TIMEOUT);
}
});
/**
* Function that downloads the attachment that was added by either party
* @param attachmentId id of the attachment which can be retrieved by the object received in the content
* @param attachmentName Name of the file which will be downloaded
*/
async function getAttachment(attachmentId: string, attachmentName: string) {
const attachmentBlob = await session?.downloadAttachment({
attachmentId
});
if (attachmentBlob === undefined) {
log.warn(`Attachment with id ${attachmentId} is not present, skipping`);
return;
}
downloadAttachment(attachmentBlob, attachmentName);
}
return (
<section>
{/* This shows basic info such as duration and status */}
<ContactHeader contact={contact} />
{/* This component renders all the stuff you see in the conversation - chat bubbles, chat events ('agent has joined') and attachments */}
<ChatMessages
messagesOrEvents={messages}
attachmentsEnabled={true}
onAttachmentClicks={
contact.getState().type === connect.ContactStateType.ENDED ? undefined : getAttachment
}
chatMessageComponent={ChatLexComponent}
/>
{/* If the conversation has ended and contact is in ENDED state */}
{contact.getState().type === connect.ContactStateType.ENDED ? (
<AfterContactWorkCard
contactType={connect.ContactType.CHAT}
onClear={async () => {
try {
await clearContact(contact.getContactId());
} catch (error) {
log.error(error);
}
}}
/>
) : (
// If not, then show the typing indicator and the chat input using which the agent can add messages
<>
<TypingIndicator show={isTyping} />
<ChatInput
onChatSubmit={(message) => {
log.debug('Session - ', session);
session
?.sendMessage({
contentType: 'text/plain',
message
})
.catch((e) => {
log.error(e);
});
}}
extraActions={[
<ChatAttachmentButton
key={1}
onFileSelect={(attachment) => {
session
?.sendAttachment({
attachment
})
.catch((e) => {
log.error(e);
});
}}
/>
]}
/>
<ContactFooter>
<EndButton label="End Chat" onEndClick={() => endContact(contact.getContactId())} />
</ContactFooter>
</>
)}
</section>
);
};
export default ChatContainer;
Next we'll add this file in our ContactList
component
ContactList.tsx
import React, { useState } from 'react';
import { Chat as ChatIcon, Phone as PhoneIcon } from '@mui/icons-material';
import { Tab, Tabs } from '@mui/material';
import { getCustomerName } from '@neev/ccp-api';
import { useContacts } from '@neev/ccp-react';
import CallContainer from './CallContainer';
import ChatContainer from './ChatContainer';
const CALL_TAB = 'call';
const ContactList = () => {
// Index of the tab which is selected
const [selectedTab, setSelectedTab] = useState<string>(CALL_TAB);
const contacts = useContacts(
{
contactStateTypes: [connect.ContactStateType.CONNECTED, connect.ContactStateType.ENDED],
monitorEvents: [
connect.ContactEvents.CONNECTED,
connect.ContactEvents.ACW,
connect.ContactEvents.ENDED,
connect.ContactEvents.DESTROYED
]
},
(newContact) => {
newContact.onConnected(() =>
setSelectedTab(
newContact.getType() === connect.ContactType.VOICE ? CALL_TAB : newContact.getContactId()
)
);
}
);
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
setSelectedTab(newValue);
};
// filtering out all the chat contacts as they will appear side by side
const chatContacts = contacts.filter((contact) => contact.getType() === connect.ContactType.CHAT);
// Contact object pertaining to a call (there can only be 1 call contact object at a time)
const callContact = contacts.find((contact) => contact.getType() === connect.ContactType.VOICE);
return (
<section>
<Tabs
value={selectedTab}
onChange={handleChange}
aria-label="contact tabs"
sx={{ paddingBlock: '0px', minHeight: 'unset', marginBottom: '16px' }}
>
<Tab
sx={{ paddingBlock: '10px', minHeight: 'unset' }}
label="Call"
value={CALL_TAB}
iconPosition="start"
icon={<PhoneIcon />}
/>
{/* Displaying all the active chat contact tab buttons */}
{chatContacts.map((contact) => (
<Tab
sx={{ paddingBlock: '10px', minHeight: 'unset' }}
icon={<ChatIcon />}
iconPosition="start"
label={getCustomerName(contact)}
key={contact.getContactId()}
value={contact.getContactId()}
/>
))}
</Tabs>
{/* since there can only be one Call contact, there is only a single call tab button */}
{/* If it is selected, then show the CallContainer */}
<div style={{ display: selectedTab === CALL_TAB ? 'block' : 'none' }}>
<CallContainer callContact={callContact} />
</div>
{/* Show ChatContainer view for all the chat objects which are available */}
{chatContacts.map((contact) => {
return (
<div
key={contact.getContactId()}
style={{ display: contact.getContactId() === selectedTab ? 'block' : 'none' }}
>
<ChatContainer contact={contact} />
</div>
);
})}
</section>
);
};
export default ContactList;
Done
And we are finished! There are still some things to do though, such as setting up the lex support, or adding the tasks support. All of which you can find in the "How do I?" section.