Getting Started
In any business interaction, it is vital to hold on to records of how you got to where you are. Not only can being able to reference previous transactions and interactions teach you about what you've done, it can help you make informed decisions about what you are going to do. With this being the case, one of the most valuable ways in which you can leverage chat to better your company is to keep records of and review the conversations your employees are having with your clients.
Knowing your users' pain points and what they love about your product and seeing what the most common comments are is a fantastic way to keep your product relevant and your customers coming back for more.
If you’re planning on building a chat application with Stream Chat, you might be wondering whether and how you might retain and process the transcripts from chats you receive, not only because you might want them for the aforementioned reasons, but also because of how frequently users request copies of their conversations. Luckily, when leveraging Stream Chat, retaining chat transcripts and providing them to your users is not only possible, it's incredibly easy to implement.
In this tutorial, we’ll build a demo application where a customer and support agent will interact using Stream Chat features. Then, we'll walk through how to send the transcript of the conversation to an email address using Mailgun (if you prefer another email service, swapping it out shouldn't be difficult).
Prerequisites
Before you proceed with this tutorial, make sure you have Node.js and npm installed on your machine. You'll also need to have a basic familiarity with building Node.js and React applications.
Sign Up for Stream Chat
If you’re new to Stream Chat, click here to create a new account. Otherwise, you can log in to your existing account. Once you’re logged in, you can use the app that's automatically created for you. Or, if you prefer, go to the dashboard, hit the blue “Create App” button at the top right to create a new app and take note of your access keys, which will be presented at the bottom of the page.
Set Up the Chat Application
Clone this GitHub repository to your filesystem by running:
1git clone https://github.com/ayoisaiah/stream-chat-transcript
and cd
into the created directory. Next, run:
1npm install
to install all the dependencies specified in the package.json
file.
Following that, rename the .env.example
file to .env
and paste in the Stream Chat keys you grabbed in the previous section.
// .env STREAM_API_KEY=<your api key> STREAM_APP_SECRET=<your app secret>
You can now start the Node server using
1node server.js
and the React app, in a separate terminal window or tab, with
1npm start
The Client Interface
Now, if you go to http://localhost:3000/
, you should see the following interface, which the customer will use to interact with a support agent:
Here’s the code that renders this view:
// src/Customer.js import React, { useState, useEffect } from "react"; import { Chat, Channel, Window, TypingIndicator, MessageList, MessageCommerce, MessageInput, MessageInputFlat, ChannelHeader } from "stream-chat-react"; import { StreamChat } from "stream-chat"; import axios from "axios"; import "stream-chat-react/dist/css/index.css"; let chatClient; function Customer() { document.title = "Customer service"; const [channel, setChannel] = useState(null); useEffect(() => { const username = "customer"; async function getToken() { try { const response = await axios.post("http://localhost:7000/join", { username }); const token = response.data.token; chatClient = new StreamChat(response.data.api_key); chatClient.setUser( { id: username, name: "Customer" }, token ); const channel = chatClient.channel("messaging", "livechat", { name: "Customer support" }); await channel.watch(); setChannel(channel); } catch (err) { console.log(err); return; } } getToken(); }, []); if (channel) { return ( <Chat client={chatClient} theme="commerce light"> <Channel channel={channel}> <Window> <ChannelHeader /> <MessageList typingIndicator={TypingIndicator} Message={MessageCommerce} /> <MessageInput Input={MessageInputFlat} focus /> </Window> </Channel> </Chat> ); } return <div></div>; } export default Customer;
Once this component is loaded, a token is requested from the server via the /join
route and, once it's received, the channel is initialized, causing the chat interface to become active. We’re using the commerce
theme here since it’s closest to a typical customer support interface from the user’s point of view.
The Agent Interface
The agent interface can be accessed by going to http://localhost:3000/#/admin
. Upon visiting this link, you should be presented with the following interface:
And here’s the code that renders this agent interface:
// src/Admin.js import React, { useState, useEffect } from "react"; import { Chat, Channel, ChannelHeader, Window, MessageList, ChannelList, MessageInput, ChannelListMessenger, MessageTeam } from "stream-chat-react"; import { StreamChat } from "stream-chat"; import axios from "axios"; import "stream-chat-react/dist/css/index.css"; let chatClient; function Admin() { document.title = "Admin"; const [channel, setChannel] = useState(null); useEffect(() => { const username = "admin"; async function getToken() { try { const response = await axios.post("http://localhost:7000/join", { username }); const token = response.data.token; chatClient = new StreamChat(response.data.api_key); chatClient.setUser( { id: username, name: "Admin" }, token ); const channel = chatClient.channel("messaging", "livechat"); await channel.watch(); setChannel(channel); } catch (err) { console.log(err); return; } } getToken(); }, []); if (channel) { return ( <Chat client={chatClient} theme="messaging light"> <ChannelList options={{ subscribe: true, state: true }} filters={{ type: "messaging" }} List={ChannelListMessenger} /> <Channel channel={channel}> <Window> <ChannelHeader /> <MessageList Message={MessageTeam} /> <MessageInput focus /> </Window> </Channel> </Chat> ); } return <div></div>; } export default Admin;
As you can see, the code is pretty similar to that for the Customer component, except that we’re using the messaging
theme here, which affords us the ability to select from different conversations via the sidebar.
You can now simulate a real customer support request by sending messages between the customer and the agent!
In the next section, we’ll take a look at how to generate a transcript of the conversation and send it to an email address...
Sending the Chat Transcript with Mailgun
Setting Up Mailgun
Before you can use Mailgun's service, you need to sign up for a free account here. Once you reach your Mailgun dashboard, you can grab your sandbox domain from the Sending domains
section of the page. Using a sandbox allows you to test Mailgun's features before connecting a real domain.
Once again, you'll need to grab your API Key (from Mailgun, this time!), which can be found here. Copy your Private API Key
and add it to your .env
file as shown below:
STREAM_API_KEY=<your stream api key> STREAM_APP_SECRET=<your stream app secret> MAILGUN_DOMAIN=<your mailgun domain> MAILGUN_API_KEY=<your mailgun private key>
Finally, run the command below to install the packages we’ll be using to send emails from our server:
1npm install nodemailer nodemailer-mailgun-transport
Creating Transcript Request Button
To complete the loop, we'll need to provide a way for the user to request a transcript in the user interface; a simple button will suffice for demo purposes. We'll make it so that, when the button is clicked, the messages from the chatroom are sent to the server. Here’s the code that gives the desired result:
// src/Customer.js import React, { useState, useEffect } from "react"; import { Chat, Channel, Window, TypingIndicator, MessageList, MessageCommerce, MessageInput, MessageInputFlat, withChannelContext } from "stream-chat-react"; import { StreamChat } from "stream-chat"; import axios from "axios"; import "stream-chat-react/dist/css/index.css"; let chatClient; async function sendTranscript(messages) { if (messages.length === 0) return; try { await axios.post("http://localhost:7000/transcript", { messages }); alert("Transcript sent successfully"); } catch (err) { alert("Sending failed"); console.log(err); } } function Customer() { document.title = "Customer service"; const [channel, setChannel] = useState(null); useEffect(() => { const username = "customer"; async function getToken() { try { const response = await axios.post("http://localhost:7000/join", { username }); const token = response.data.token; chatClient = new StreamChat(response.data.api_key); chatClient.setUser( { id: username, name: "Customer" }, token ); const channel = chatClient.channel("messaging", "livechat", { name: "Customer support" }); await channel.watch(); setChannel(channel); } catch (err) { console.log(err); return; } } getToken(); }, []); if (channel) { const CustomChannelHeader = withChannelContext( class CustomChannelHeader extends React.PureComponent { render() { return ( <div className="str-chat__header-livestream"> <div className="str-chat__header-livestream-left"> <p className="str-chat__header-livestream-left--title"> Customer Support Chat </p> </div> <div className="str-chat__header-livestream-right"> <div className="str-chat__header-livestream-right-button-wrapper"> <button className="logout" onClick={() => sendTranscript(this.props.channel.state.messages) } > Transcript </button> </div> </div> </div> ); } } ); return ( <Chat client={chatClient} theme="commerce light"> <Channel channel={channel}> <Window> <CustomChannelHeader /> <MessageList typingIndicator={TypingIndicator} Message={MessageCommerce} /> <MessageInput Input={MessageInputFlat} focus /> </Window> </Channel> </Chat> ); } return <div></div>; } export default Customer;
What we’ve done here is replaced the default ChannelHeader
component with our own, in order to add a transcript button that, when clicked, calls the sendTranscript()
method and passes an array of messages, which can be accessed using this.props.channel.state.messages
. In sendTranscript
, the messages are sent to the /transcript
route on the server, where they will be processed and sent to the user.
Let’s make some changes in server.js
to get everything working as described above:
// server.js require("dotenv").config(); const express = require("express"); const cors = require("cors"); const bodyParser = require("body-parser"); const { StreamChat } = require("stream-chat"); const nodemailer = require('nodemailer'); const mg = require('nodemailer-mailgun-transport'); const app = express(); app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); // initialize Stream Chat SDK const serverSideClient = new StreamChat( process.env.STREAM_API_KEY, process.env.STREAM_APP_SECRET ); const auth = { auth: { api_key: process.env.MAILGUN_API_KEY, domain: process.env.MAILGUN_DOMAIN }, }; const nodemailerMailgun = nodemailer.createTransport(mg(auth)); app.post('/transcript', async (req, res) => { const { messages } = req.body; let html = '<h1>Your Chat Transcript</h1>'; messages.forEach(msg => { html += `<p><strong>${msg.user.name}</strong>: ${msg.text}</p>`; }); nodemailerMailgun.sendMail( { from: 'chat@example.com', to: '<your email address>', subject: 'Chat transcript', html, }, err => { if (err) { console.log(`Error: ${err}`); res.status(500); } else { res.status(200); } } ); }); app.post("/join", async (req, res) => { const { username } = req.body; const token = serverSideClient.createToken(username); try { await serverSideClient.updateUser( { id: username, name: username }, token ); const admin = { id: "admin" }; const channel = serverSideClient.channel("messaging", "livechat", { name: "Customer support", created_by: admin }); await channel.create(); await channel.addMembers([username]); } catch (err) { console.log(err); } return res .status(200) .json({ user: { username }, token, api_key: process.env.STREAM_API_KEY }); }); app.listen(7000, () => { console.log(`Server running on PORT 7000`); });
In the /transcript
route, the messages are processed into an HTML string
and sent to the email address entered in the to
field. Make sure to replace the <your email address>
placeholder before testing the application.
Once you’ve updated the server.js
file, kill the server and start it again with
1node server.js
To see this all come together, click the "transcript" button on the customer view, and check your email inbox. You should see a transcript of the conversation, similar to that in the screenshot below:
Wrap Up
In this article, I’ve described how messages in a Stream channel can be extracted and used to construct a transcript of a conversation that is subsequently sent to an email address using Mailgun. With this knowlege, you can now easily use the concepts discussed here to implement chat transcripts in your own applications!
Don’t forget to check out the full source code used for this tutorial here to do a deeper dive into the nuances of what makes our app tick and how you might better customize it to your use case.
Thanks for reading and Happy Coding!