Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Exploring the Conversations JavaScript Quickstart


What does the Conversations JavaScript Quickstart do? How does it work? How would you add something similar to your own project? We'll cover all of these questions and more in this behind-the-scenes look at the example application code.

If you haven't had a chance to try out the Conversations demo application, follow the instructions in the Twilio Conversations Quickstart guide to get it up and running.


Quickstart Overview

quickstart-overview page anchor

The example application code has two pieces - a large front-end Single Page Application (SPA) written with JavaScript and React, and a small back end written with JavaScript and Node.js. We created the project using the create-react-app(link takes you to an external page) command line tool.

Within the quickstart application, you will find examples of the following:

When you build an application that uses Conversations, you may be able to use several of the React components from the quickstart, or you may customize them to fit your use case. You also do not have to use React with Conversations - it works with vanilla JS, Angular(link takes you to an external page), Vue(link takes you to an external page), or any other Javascript framework for the web browser.


Adding Twilio Conversations to your Application

adding-twilio-conversations-to-your-application page anchor

When you build your solutions with Twilio Conversations, you need a Conversations Client Javascript SDK that runs in your end user's web browser. You can install this library using Node Package Manager (NPM), Yarn, or with an HTML Script tag that points to the Twilio CDN.

You also need to integrate a Twilio helper library into your back-end application. These libraries exist for Java, C#, PHP, Node.js/Javascript, Ruby, and Python. You can use these libraries with almost any server framework that works with these languages.

Conversations JavaScript Client SDK

conversations-javascript-client-sdk page anchor

To install the JavaScript Conversations Client library in your web application's front end, use npm (or yarn):


_10
npm install --save @twilio/conversations

There is also a CDN installation, if you prefer:


_10
<script src="https://media.twiliocdn.com/sdk/js/conversations/releases/2.1.0/twilio-conversations.min.js"
_10
integrity="sha256-v2SFLWujVq0wnwHpcxct7bzTP8wII7sumEhAKMEqgHQ="
_10
crossorigin="anonymous"></script>

You would typically start by adding the Conversations.Client from this SDK to your project, and then work with Conversation objects to send and retrieve Message objects for a given Conversation. Other important classes are User, Participant, and Media.

While we cover some of the basics of the Conversations JS SDK in this Quickstart, you can find reference documentation for each class as JSDocs(link takes you to an external page). We also consider some of these topics in more detail on other pages in our docs, which we will link to in each section that has a corresponding guide.

These Javascript libraries are not the libraries you need for a back-end, Node.js application. If you are building a web application with Node.js, you need the JavaScript Twilio Helper Library.

For your chosen language and/or platform, pick the appropriate Twilio Helper Library:

On each of these pages, you will find instructions for setting up the Twilio helper library (also called a "server-side SDK"). We recommend using dependency management for the Twilio libraries, and you'll find directions for the most common build tools for your platform.

(information)

Info

If you don't already have a Twilio account, sign up for a Twilio trial account(link takes you to an external page), and then create a new project. You'll also need to create an API Key and API Secret pair to call Twilio's REST API, whether you use one of the Twilio helper libraries, or make the API calls yourself.


Understanding Identity, Access Tokens, and Chat Grants

understanding-identity-access-tokens-and-chat-grants page anchor

Each chat user in your Conversations project needs an identity - this could be their user id, their username, or some kind of other identifier. You could certainly have anonymous users in your Conversations - for instance, a web chat popup with a customer service agent on an e-commerce website - but in that case, you would still want to issue some kind of identifier from your application.

Once you build Twilio Conversations into your project, you should generate an access token with a ChatGrant for end users, along with the identity value.

With the Conversations JS Quickstart, the easiest way to get started is to create an access token from the Twilio Command Line Interface (CLI).


Difference between Access Tokens, Auth Tokens and API Keys

difference-between-access-tokens-auth-tokens-and-api-keys page anchor

As part of this project, you will see that there are three different ways of providing credentials for Twilio - access tokens, auth tokens, and API keys. What is the difference between all of these different styles?

Access tokens provide short-lived credentials for a single end user to work with your Twilio service from a Javascript application running in a web browser, or from a native iOS or Android mobile application. Use the Twilio helper libraries in your back-end web services to create access tokens for your front-end applications to consume. Alternatively, use the Twilio CLI to create access tokens for testing. These access tokens have a built-in expiration, and need to be refreshed from your server if your users have long-running connections. The Conversations client will update your application when access tokens are about to expire, or if they have expired, so that you can refresh the token.

Although the names are similar, authentication (or auth) tokens are not the same as access tokens, and cannot be used in the same way. The auth token pairs with your Twilio account identifier (also called the account SID) to provide authentication for the Twilio REST API. Your auth token should be treated with the same care that you would use to secure your Twilio password, and should never be included directly in source code, made available to a client application, or checked into a file in source control.

Similar to auth tokens, API key/secret pairs secure access to the Twilio REST API for your account. When you create an API key and secret pair from the Twilio console, the secret will only be shown once, and then it won't be recoverable. In your back-end application, you would authenticate to Twilio with a combination of your account identifier (also known as the "Account SID"), an API key, and an API secret.

The advantage of API keys over auth tokens is that it is easy to rotate API keys on your server application, especially if you use one API key and secret pair for each application cluster or instance. This way, you can have multiple credentials under your Twilio account, and if you need to swap out a key pair and then deactivate it, you can do it on an application basis, not on an account basis.

Storing Credentials Securely

storing-credentials-securely page anchor

Whether you use auth tokens or API keys, we suggest that you store those credentials securely, and do not check them into source control. There are many different options for managing secure credentials that depend on how and where you run your development, staging, and production environments.

When you develop locally, look into using a .env file with your project, usually in conjunction with a library named dotenv. For .NET Core, read our article on Setting Twilio Environment Variables in Windows 10 with PowerShell and .NET Core 3.0(link takes you to an external page) to learn a lot more about this topic!


Retrieving a Conversations Access Token

retrieving-a-conversations-access-token page anchor

In the Conversations JS Quickstart, you can simply generate an access token using the Twilio Command Line Interface (CLI), and then paste that into the ConversationsApp.js file. While this works for getting the quickstart up and running, you will want to replace this with your own function that retrieves an access token.

You can use fetch, axios, or another client-side JS library to make an authenticated HTTP request to your server, where you would provide an access token with a ChatGrant that sets the identity for the user based on your own authentication mechanism (such as a session cookie).

Ideally, this method would be usable for three different scenarios:

  1. Initializing the Conversations JS Client when the React component mounts
  2. Refreshing the access token when the Conversations JS Client notifies your application that the token is about to expire
  3. Refreshing the access token when the Conversations JS Client notifies your application that the token did expire

Initializing the JS Conversations Client

initializing-the-js-conversations-client page anchor

The first step is to get an access token. Once you have an access token (It is a string value.), you can initialize a Twilio Conversations Client. This client is the central class in the Conversations JS SDK, and you need to keep it around after initialization. The client is designed to be long-lived, and it will fire events off that your project can subscribe to.

Initializing the Conversations Client

initializing-the-conversations-client page anchor

_218
import React from "react";
_218
import { Badge, Icon, Layout, Spin, Typography } from "antd";
_218
import { Client as ConversationsClient } from "@twilio/conversations";
_218
_218
import "./assets/Conversation.css";
_218
import "./assets/ConversationSection.css";
_218
import { ReactComponent as Logo } from "./assets/twilio-mark-red.svg";
_218
_218
import Conversation from "./Conversation";
_218
import LoginPage from "./LoginPage";
_218
import { ConversationsList } from "./ConversationsList";
_218
import { HeaderItem } from "./HeaderItem";
_218
_218
const { Content, Sider, Header } = Layout;
_218
const { Text } = Typography;
_218
_218
class ConversationsApp extends React.Component {
_218
constructor(props) {
_218
super(props);
_218
_218
const name = localStorage.getItem("name") || "";
_218
const loggedIn = name !== "";
_218
_218
this.state = {
_218
name,
_218
loggedIn,
_218
token: null,
_218
statusString: null,
_218
conversationsReady: false,
_218
conversations: [],
_218
selectedConversationSid: null,
_218
newMessage: ""
_218
};
_218
}
_218
_218
componentDidMount = () => {
_218
if (this.state.loggedIn) {
_218
this.getToken();
_218
this.setState({ statusString: "Fetching credentials…" });
_218
}
_218
};
_218
_218
logIn = (name) => {
_218
if (name !== "") {
_218
localStorage.setItem("name", name);
_218
this.setState({ name, loggedIn: true }, this.getToken);
_218
}
_218
};
_218
_218
logOut = (event) => {
_218
if (event) {
_218
event.preventDefault();
_218
}
_218
_218
this.setState({
_218
name: "",
_218
loggedIn: false,
_218
token: "",
_218
conversationsReady: false,
_218
messages: [],
_218
newMessage: "",
_218
conversations: []
_218
});
_218
_218
localStorage.removeItem("name");
_218
this.conversationsClient.shutdown();
_218
};
_218
_218
getToken = () => {
_218
// Paste your unique Chat token function
_218
const myToken = "<Your token here>";
_218
this.setState({ token: myToken }, this.initConversations);
_218
};
_218
_218
initConversations = async () => {
_218
window.conversationsClient = ConversationsClient;
_218
this.conversationsClient = new ConversationsClient(this.state.token);
_218
this.setState({ statusString: "Connecting to Twilio…" });
_218
_218
this.conversationsClient.on("connectionStateChanged", (state) => {
_218
if (state === "connecting")
_218
this.setState({
_218
statusString: "Connecting to Twilio…",
_218
status: "default"
_218
});
_218
if (state === "connected") {
_218
this.setState({
_218
statusString: "You are connected.",
_218
status: "success"
_218
});
_218
}
_218
if (state === "disconnecting")
_218
this.setState({
_218
statusString: "Disconnecting from Twilio…",
_218
conversationsReady: false,
_218
status: "default"
_218
});
_218
if (state === "disconnected")
_218
this.setState({
_218
statusString: "Disconnected.",
_218
conversationsReady: false,
_218
status: "warning"
_218
});
_218
if (state === "denied")
_218
this.setState({
_218
statusString: "Failed to connect.",
_218
conversationsReady: false,
_218
status: "error"
_218
});
_218
});
_218
this.conversationsClient.on("conversationJoined", (conversation) => {
_218
this.setState({ conversations: [...this.state.conversations, conversation] });
_218
});
_218
this.conversationsClient.on("conversationLeft", (thisConversation) => {
_218
this.setState({
_218
conversations: [...this.state.conversations.filter((it) => it !== thisConversation)]
_218
});
_218
});
_218
};
_218
_218
render() {
_218
const { conversations, selectedConversationSid, status } = this.state;
_218
const selectedConversation = conversations.find(
_218
(it) => it.sid === selectedConversationSid
_218
);
_218
_218
let conversationContent;
_218
if (selectedConversation) {
_218
conversationContent = (
_218
<Conversation
_218
conversationProxy={selectedConversation}
_218
myIdentity={this.state.name}
_218
/>
_218
);
_218
} else if (status !== "success") {
_218
conversationContent = "Loading your conversation!";
_218
} else {
_218
conversationContent = "";
_218
}
_218
_218
if (this.state.loggedIn) {
_218
return (
_218
<div className="conversations-window-wrapper">
_218
<Layout className="conversations-window-container">
_218
<Header
_218
style={{ display: "flex", alignItems: "center", padding: 0 }}
_218
>
_218
<div
_218
style={{
_218
maxWidth: "250px",
_218
width: "100%",
_218
display: "flex",
_218
alignItems: "center"
_218
}}
_218
>
_218
<HeaderItem style={{ paddingRight: "0", display: "flex" }}>
_218
<Logo />
_218
</HeaderItem>
_218
<HeaderItem>
_218
<Text strong style={{ color: "white" }}>
_218
Conversations
_218
</Text>
_218
</HeaderItem>
_218
</div>
_218
<div style={{ display: "flex", width: "100%" }}>
_218
<HeaderItem>
_218
<Text strong style={{ color: "white" }}>
_218
{selectedConversation &&
_218
(selectedConversation.friendlyName || selectedConversation.sid)}
_218
</Text>
_218
</HeaderItem>
_218
<HeaderItem style={{ float: "right", marginLeft: "auto" }}>
_218
<span
_218
style={{ color: "white" }}
_218
>{` ${this.state.statusString}`}</span>
_218
<Badge
_218
dot={true}
_218
status={this.state.status}
_218
style={{ marginLeft: "1em" }}
_218
/>
_218
</HeaderItem>
_218
<HeaderItem>
_218
<Icon
_218
type="poweroff"
_218
onClick={this.logOut}
_218
style={{
_218
color: "white",
_218
fontSize: "20px",
_218
marginLeft: "auto"
_218
}}
_218
/>
_218
</HeaderItem>
_218
</div>
_218
</Header>
_218
<Layout>
_218
<Sider theme={"light"} width={250}>
_218
<ConversationsList
_218
conversations={conversations}
_218
selectedConversationSid={selectedConversationSid}
_218
onConversationClick={(item) => {
_218
this.setState({ selectedConversationSid: item.sid });
_218
}}
_218
/>
_218
</Sider>
_218
<Content className="conversation-section">
_218
<div id="SelectedConversation">{conversationContent}</div>
_218
</Content>
_218
</Layout>
_218
</Layout>
_218
</div>
_218
);
_218
}
_218
_218
return <LoginPage onSubmit={this.logIn} />;
_218
}
_218
}
_218
_218
export default ConversationsApp;

After you initialize the Conversations client, the connectionStateChanged event will fire any time the user's connection changes. The possible states handled in the Conversations JS Quickstart are:

  • connecting
  • connected
  • disconnecting
  • disconnected
  • denied

Once the user is connected, they are able to chat with others in conversations.

Managing the Connection State

managing-the-connection-state page anchor

_218
import React from "react";
_218
import { Badge, Icon, Layout, Spin, Typography } from "antd";
_218
import { Client as ConversationsClient } from "@twilio/conversations";
_218
_218
import "./assets/Conversation.css";
_218
import "./assets/ConversationSection.css";
_218
import { ReactComponent as Logo } from "./assets/twilio-mark-red.svg";
_218
_218
import Conversation from "./Conversation";
_218
import LoginPage from "./LoginPage";
_218
import { ConversationsList } from "./ConversationsList";
_218
import { HeaderItem } from "./HeaderItem";
_218
_218
const { Content, Sider, Header } = Layout;
_218
const { Text } = Typography;
_218
_218
class ConversationsApp extends React.Component {
_218
constructor(props) {
_218
super(props);
_218
_218
const name = localStorage.getItem("name") || "";
_218
const loggedIn = name !== "";
_218
_218
this.state = {
_218
name,
_218
loggedIn,
_218
token: null,
_218
statusString: null,
_218
conversationsReady: false,
_218
conversations: [],
_218
selectedConversationSid: null,
_218
newMessage: ""
_218
};
_218
}
_218
_218
componentDidMount = () => {
_218
if (this.state.loggedIn) {
_218
this.getToken();
_218
this.setState({ statusString: "Fetching credentials…" });
_218
}
_218
};
_218
_218
logIn = (name) => {
_218
if (name !== "") {
_218
localStorage.setItem("name", name);
_218
this.setState({ name, loggedIn: true }, this.getToken);
_218
}
_218
};
_218
_218
logOut = (event) => {
_218
if (event) {
_218
event.preventDefault();
_218
}
_218
_218
this.setState({
_218
name: "",
_218
loggedIn: false,
_218
token: "",
_218
conversationsReady: false,
_218
messages: [],
_218
newMessage: "",
_218
conversations: []
_218
});
_218
_218
localStorage.removeItem("name");
_218
this.conversationsClient.shutdown();
_218
};
_218
_218
getToken = () => {
_218
// Paste your unique Chat token function
_218
const myToken = "<Your token here>";
_218
this.setState({ token: myToken }, this.initConversations);
_218
};
_218
_218
initConversations = async () => {
_218
window.conversationsClient = ConversationsClient;
_218
this.conversationsClient = new ConversationsClient(this.state.token);
_218
this.setState({ statusString: "Connecting to Twilio…" });
_218
_218
this.conversationsClient.on("connectionStateChanged", (state) => {
_218
if (state === "connecting")
_218
this.setState({
_218
statusString: "Connecting to Twilio…",
_218
status: "default"
_218
});
_218
if (state === "connected") {
_218
this.setState({
_218
statusString: "You are connected.",
_218
status: "success"
_218
});
_218
}
_218
if (state === "disconnecting")
_218
this.setState({
_218
statusString: "Disconnecting from Twilio…",
_218
conversationsReady: false,
_218
status: "default"
_218
});
_218
if (state === "disconnected")
_218
this.setState({
_218
statusString: "Disconnected.",
_218
conversationsReady: false,
_218
status: "warning"
_218
});
_218
if (state === "denied")
_218
this.setState({
_218
statusString: "Failed to connect.",
_218
conversationsReady: false,
_218
status: "error"
_218
});
_218
});
_218
this.conversationsClient.on("conversationJoined", (conversation) => {
_218
this.setState({ conversations: [...this.state.conversations, conversation] });
_218
});
_218
this.conversationsClient.on("conversationLeft", (thisConversation) => {
_218
this.setState({
_218
conversations: [...this.state.conversations.filter((it) => it !== thisConversation)]
_218
});
_218
});
_218
};
_218
_218
render() {
_218
const { conversations, selectedConversationSid, status } = this.state;
_218
const selectedConversation = conversations.find(
_218
(it) => it.sid === selectedConversationSid
_218
);
_218
_218
let conversationContent;
_218
if (selectedConversation) {
_218
conversationContent = (
_218
<Conversation
_218
conversationProxy={selectedConversation}
_218
myIdentity={this.state.name}
_218
/>
_218
);
_218
} else if (status !== "success") {
_218
conversationContent = "Loading your conversation!";
_218
} else {
_218
conversationContent = "";
_218
}
_218
_218
if (this.state.loggedIn) {
_218
return (
_218
<div className="conversations-window-wrapper">
_218
<Layout className="conversations-window-container">
_218
<Header
_218
style={{ display: "flex", alignItems: "center", padding: 0 }}
_218
>
_218
<div
_218
style={{
_218
maxWidth: "250px",
_218
width: "100%",
_218
display: "flex",
_218
alignItems: "center"
_218
}}
_218
>
_218
<HeaderItem style={{ paddingRight: "0", display: "flex" }}>
_218
<Logo />
_218
</HeaderItem>
_218
<HeaderItem>
_218
<Text strong style={{ color: "white" }}>
_218
Conversations
_218
</Text>
_218
</HeaderItem>
_218
</div>
_218
<div style={{ display: "flex", width: "100%" }}>
_218
<HeaderItem>
_218
<Text strong style={{ color: "white" }}>
_218
{selectedConversation &&
_218
(selectedConversation.friendlyName || selectedConversation.sid)}
_218
</Text>
_218
</HeaderItem>
_218
<HeaderItem style={{ float: "right", marginLeft: "auto" }}>
_218
<span
_218
style={{ color: "white" }}
_218
>{` ${this.state.statusString}`}</span>
_218
<Badge
_218
dot={true}
_218
status={this.state.status}
_218
style={{ marginLeft: "1em" }}
_218
/>
_218
</HeaderItem>
_218
<HeaderItem>
_218
<Icon
_218
type="poweroff"
_218
onClick={this.logOut}
_218
style={{
_218
color: "white",
_218
fontSize: "20px",
_218
marginLeft: "auto"
_218
}}
_218
/>
_218
</HeaderItem>
_218
</div>
_218
</Header>
_218
<Layout>
_218
<Sider theme={"light"} width={250}>
_218
<ConversationsList
_218
conversations={conversations}
_218
selectedConversationSid={selectedConversationSid}
_218
onConversationClick={(item) => {
_218
this.setState({ selectedConversationSid: item.sid });
_218
}}
_218
/>
_218
</Sider>
_218
<Content className="conversation-section">
_218
<div id="SelectedConversation">{conversationContent}</div>
_218
</Content>
_218
</Layout>
_218
</Layout>
_218
</div>
_218
);
_218
}
_218
_218
return <LoginPage onSubmit={this.logIn} />;
_218
}
_218
}
_218
_218
export default ConversationsApp;


Joining and Leaving a Conversation

joining-and-leaving-a-conversation page anchor

The Conversation class is the building block of your Conversations application. In the JS Quickstart, as the user joins or leaves conversations, conversationJoined and conversationLeft events from the ConversationsClient get fired with the Conversation object as an argument. The React application maintains the list of conversations in its state, and then displays those conversations to the user in the ConversationsList.js(link takes you to an external page) component.

Joining and Leaving Conversations

joining-and-leaving-conversations page anchor

_218
import React from "react";
_218
import { Badge, Icon, Layout, Spin, Typography } from "antd";
_218
import { Client as ConversationsClient } from "@twilio/conversations";
_218
_218
import "./assets/Conversation.css";
_218
import "./assets/ConversationSection.css";
_218
import { ReactComponent as Logo } from "./assets/twilio-mark-red.svg";
_218
_218
import Conversation from "./Conversation";
_218
import LoginPage from "./LoginPage";
_218
import { ConversationsList } from "./ConversationsList";
_218
import { HeaderItem } from "./HeaderItem";
_218
_218
const { Content, Sider, Header } = Layout;
_218
const { Text } = Typography;
_218
_218
class ConversationsApp extends React.Component {
_218
constructor(props) {
_218
super(props);
_218
_218
const name = localStorage.getItem("name") || "";
_218
const loggedIn = name !== "";
_218
_218
this.state = {
_218
name,
_218
loggedIn,
_218
token: null,
_218
statusString: null,
_218
conversationsReady: false,
_218
conversations: [],
_218
selectedConversationSid: null,
_218
newMessage: ""
_218
};
_218
}
_218
_218
componentDidMount = () => {
_218
if (this.state.loggedIn) {
_218
this.getToken();
_218
this.setState({ statusString: "Fetching credentials…" });
_218
}
_218
};
_218
_218
logIn = (name) => {
_218
if (name !== "") {
_218
localStorage.setItem("name", name);
_218
this.setState({ name, loggedIn: true }, this.getToken);
_218
}
_218
};
_218
_218
logOut = (event) => {
_218
if (event) {
_218
event.preventDefault();
_218
}
_218
_218
this.setState({
_218
name: "",
_218
loggedIn: false,
_218
token: "",
_218
conversationsReady: false,
_218
messages: [],
_218
newMessage: "",
_218
conversations: []
_218
});
_218
_218
localStorage.removeItem("name");
_218
this.conversationsClient.shutdown();
_218
};
_218
_218
getToken = () => {
_218
// Paste your unique Chat token function
_218
const myToken = "<Your token here>";
_218
this.setState({ token: myToken }, this.initConversations);
_218
};
_218
_218
initConversations = async () => {
_218
window.conversationsClient = ConversationsClient;
_218
this.conversationsClient = new ConversationsClient(this.state.token);
_218
this.setState({ statusString: "Connecting to Twilio…" });
_218
_218
this.conversationsClient.on("connectionStateChanged", (state) => {
_218
if (state === "connecting")
_218
this.setState({
_218
statusString: "Connecting to Twilio…",
_218
status: "default"
_218
});
_218
if (state === "connected") {
_218
this.setState({
_218
statusString: "You are connected.",
_218
status: "success"
_218
});
_218
}
_218
if (state === "disconnecting")
_218
this.setState({
_218
statusString: "Disconnecting from Twilio…",
_218
conversationsReady: false,
_218
status: "default"
_218
});
_218
if (state === "disconnected")
_218
this.setState({
_218
statusString: "Disconnected.",
_218
conversationsReady: false,
_218
status: "warning"
_218
});
_218
if (state === "denied")
_218
this.setState({
_218
statusString: "Failed to connect.",
_218
conversationsReady: false,
_218
status: "error"
_218
});
_218
});
_218
this.conversationsClient.on("conversationJoined", (conversation) => {
_218
this.setState({ conversations: [...this.state.conversations, conversation] });
_218
});
_218
this.conversationsClient.on("conversationLeft", (thisConversation) => {
_218
this.setState({
_218
conversations: [...this.state.conversations.filter((it) => it !== thisConversation)]
_218
});
_218
});
_218
};
_218
_218
render() {
_218
const { conversations, selectedConversationSid, status } = this.state;
_218
const selectedConversation = conversations.find(
_218
(it) => it.sid === selectedConversationSid
_218
);
_218
_218
let conversationContent;
_218
if (selectedConversation) {
_218
conversationContent = (
_218
<Conversation
_218
conversationProxy={selectedConversation}
_218
myIdentity={this.state.name}
_218
/>
_218
);
_218
} else if (status !== "success") {
_218
conversationContent = "Loading your conversation!";
_218
} else {
_218
conversationContent = "";
_218
}
_218
_218
if (this.state.loggedIn) {
_218
return (
_218
<div className="conversations-window-wrapper">
_218
<Layout className="conversations-window-container">
_218
<Header
_218
style={{ display: "flex", alignItems: "center", padding: 0 }}
_218
>
_218
<div
_218
style={{
_218
maxWidth: "250px",
_218
width: "100%",
_218
display: "flex",
_218
alignItems: "center"
_218
}}
_218
>
_218
<HeaderItem style={{ paddingRight: "0", display: "flex" }}>
_218
<Logo />
_218
</HeaderItem>
_218
<HeaderItem>
_218
<Text strong style={{ color: "white" }}>
_218
Conversations
_218
</Text>
_218
</HeaderItem>
_218
</div>
_218
<div style={{ display: "flex", width: "100%" }}>
_218
<HeaderItem>
_218
<Text strong style={{ color: "white" }}>
_218
{selectedConversation &&
_218
(selectedConversation.friendlyName || selectedConversation.sid)}
_218
</Text>
_218
</HeaderItem>
_218
<HeaderItem style={{ float: "right", marginLeft: "auto" }}>
_218
<span
_218
style={{ color: "white" }}
_218
>{` ${this.state.statusString}`}</span>
_218
<Badge
_218
dot={true}
_218
status={this.state.status}
_218
style={{ marginLeft: "1em" }}
_218
/>
_218
</HeaderItem>
_218
<HeaderItem>
_218
<Icon
_218
type="poweroff"
_218
onClick={this.logOut}
_218
style={{
_218
color: "white",
_218
fontSize: "20px",
_218
marginLeft: "auto"
_218
}}
_218
/>
_218
</HeaderItem>
_218
</div>
_218
</Header>
_218
<Layout>
_218
<Sider theme={"light"} width={250}>
_218
<ConversationsList
_218
conversations={conversations}
_218
selectedConversationSid={selectedConversationSid}
_218
onConversationClick={(item) => {
_218
this.setState({ selectedConversationSid: item.sid });
_218
}}
_218
/>
_218
</Sider>
_218
<Content className="conversation-section">
_218
<div id="SelectedConversation">{conversationContent}</div>
_218
</Content>
_218
</Layout>
_218
</Layout>
_218
</div>
_218
);
_218
}
_218
_218
return <LoginPage onSubmit={this.logIn} />;
_218
}
_218
}
_218
_218
export default ConversationsApp;


Sending Messages to a Conversation

sending-messages-to-a-conversation page anchor

To send a message (with text content) to a conversation that a user has joined, you need to call the sendMessage() method on the Conversation instance. In the quickstart, we update the React component's state for the newMessage variable to be empty, leaving the text input field open for another message.

While the Conversations JS Quickstart does not implement them, you can find a list of webhooks that you can enable for your Conversations project. These webhooks include onMessageAdd, which is a pre-action webhook that could filter the text in the message, and onMessageAdded, which is a post-action webhook that could take action based on the contents of a message (such as updating a CRM).

The Conversations JS Quickstart also demonstrates how to send media, such as images, through the web browser interface using drag and drop. This functionality is in the Conversation.js(link takes you to an external page) file.

Sending a Message to a Conversation

sending-a-message-to-a-conversation page anchor

_159
import React, { Component } from 'react';
_159
import './assets/Conversation.css';
_159
import MessageBubble from './MessageBubble'
_159
import Dropzone from 'react-dropzone';
_159
import styles from './assets/Conversation.module.css'
_159
import {Button, Form, Icon, Input} from "antd";
_159
import ConversationsMessages from "./ConversationsMessages";
_159
import PropTypes from "prop-types";
_159
_159
class Conversation extends Component {
_159
constructor(props) {
_159
super(props);
_159
this.state = {
_159
newMessage: '',
_159
conversationProxy: props.conversationProxy,
_159
messages: [],
_159
loadingState: 'initializing',
_159
boundConversations: new Set()
_159
};
_159
}
_159
_159
loadMessagesFor = (thisConversation) => {
_159
if (this.state.conversationProxy === thisConversation) {
_159
thisConversation.getMessages()
_159
.then(messagePaginator => {
_159
if (this.state.conversationProxy === thisConversation) {
_159
this.setState({ messages: messagePaginator.items, loadingState: 'ready' });
_159
}
_159
})
_159
.catch(err => {
_159
console.error("Couldn't fetch messages IMPLEMENT RETRY", err);
_159
this.setState({ loadingState: "failed" });
_159
});
_159
}
_159
};
_159
_159
componentDidMount = () => {
_159
if (this.state.conversationProxy) {
_159
this.loadMessagesFor(this.state.conversationProxy);
_159
_159
if (!this.state.boundConversations.has(this.state.conversationProxy)) {
_159
let newConversation = this.state.conversationProxy;
_159
newConversation.on('messageAdded', m => this.messageAdded(m, newConversation));
_159
this.setState({boundConversations: new Set([...this.state.boundConversations, newConversation])});
_159
}
_159
}
_159
}
_159
_159
componentDidUpdate = (oldProps, oldState) => {
_159
if (this.state.conversationProxy !== oldState.conversationProxy) {
_159
this.loadMessagesFor(this.state.conversationProxy);
_159
_159
if (!this.state.boundConversations.has(this.state.conversationProxy)) {
_159
let newConversation = this.state.conversationProxy;
_159
newConversation.on('messageAdded', m => this.messageAdded(m, newConversation));
_159
this.setState({boundConversations: new Set([...this.state.boundConversations, newConversation])});
_159
}
_159
}
_159
};
_159
_159
static getDerivedStateFromProps(newProps, oldState) {
_159
let logic = (oldState.loadingState === 'initializing') || oldState.conversationProxy !== newProps.conversationProxy;
_159
if (logic) {
_159
return { loadingState: 'loading messages', conversationProxy: newProps.conversationProxy };
_159
} else {
_159
return null;
_159
}
_159
}
_159
_159
messageAdded = (message, targetConversation) => {
_159
if (targetConversation === this.state.conversationProxy)
_159
this.setState((prevState, props) => ({
_159
messages: [...prevState.messages, message]
_159
}));
_159
};
_159
_159
onMessageChanged = event => {
_159
this.setState({ newMessage: event.target.value });
_159
};
_159
_159
sendMessage = event => {
_159
event.preventDefault();
_159
const message = this.state.newMessage;
_159
this.setState({ newMessage: '' });
_159
this.state.conversationProxy.sendMessage(message);
_159
};
_159
_159
onDrop = acceptedFiles => {
_159
this.state.conversationProxy.sendMessage({contentType: acceptedFiles[0].type, media: acceptedFiles[0]});
_159
};
_159
_159
render = () => {
_159
return (
_159
<Dropzone
_159
onDrop={this.onDrop}
_159
accept="image/*">
_159
{({getRootProps, getInputProps, isDragActive}) => (
_159
<div
_159
{...getRootProps()}
_159
onClick={() => {
_159
}}
_159
id="OpenChannel"
_159
style={{position: "relative", top: 0}}>
_159
_159
{isDragActive &&
_159
<div className={styles.drop}>
_159
<Icon type={"cloud-upload"}
_159
style={{fontSize: "5em", color: "#fefefe"}}/>
_159
<h3 style={{color: "#fefefe"}}>Release to Upload</h3>
_159
</div>
_159
}
_159
<div
_159
className={styles.messages}
_159
style={{
_159
filter: `blur(${isDragActive ? 4 : 0}px)`,
_159
}}
_159
>
_159
<input id="files" {...getInputProps()} />
_159
<div style={{flexBasis: "100%", flexGrow: 2, flexShrink: 1, overflowY: "scroll"}}>
_159
<ConversationsMessages
_159
identity={this.props.myIdentity}
_159
messages={this.state.messages}/>
_159
</div>
_159
<div>
_159
<Form onSubmit={this.sendMessage}>
_159
<Input.Group compact={true} style={{
_159
width: "100%",
_159
display: "flex",
_159
flexDirection: "row"
_159
}}>
_159
<Input
_159
style={{flexBasis: "100%"}}
_159
placeholder={"Type your message here..."}
_159
type={"text"}
_159
name={"message"}
_159
id={styles['type-a-message']}
_159
autoComplete={"off"}
_159
disabled={this.state.loadingState !== 'ready'}
_159
onChange={this.onMessageChanged}
_159
value={this.state.newMessage}
_159
/>
_159
<Button icon="enter" htmlType="submit" type={"submit"}/>
_159
</Input.Group>
_159
</Form>
_159
</div>
_159
</div>
_159
</div>
_159
)}
_159
_159
</Dropzone>
_159
);
_159
}
_159
}
_159
_159
Conversation.propTypes = {
_159
myIdentity: PropTypes.string.isRequired
_159
};
_159
_159
export default Conversation;


Receiving and Displaying Messages

receiving-and-displaying-messages page anchor

In the React Conversations demo, we created a Conversation React component, which you can find in the src/Conversation.js(link takes you to an external page) file in the GitHub repo. As part of that component, we simply listen to the messageAdded event on the SDK's Conversation object. To distinguish between the React component and the representation of a conversation in the Twilio SDK, we will call the SDK version a conversation proxy here. This conversation proxy gets passed into the React component as a property, and then the React component interacts with the SDK by calling methods on it, or adding listeners.

Displaying Existing Messages

displaying-existing-messages page anchor

The React Conversation component loads the existing messages from the conversation proxy, using the getMessages() method on the Twilio SDK Conversation class. This returns a paginator, and we load the messages from the first page of results up to display to the user when they join a conversation.

Conversations JS Quickstart - Conversation.js

conversations-js-quickstart---conversationjs page anchor

_159
import React, { Component } from 'react';
_159
import './assets/Conversation.css';
_159
import MessageBubble from './MessageBubble'
_159
import Dropzone from 'react-dropzone';
_159
import styles from './assets/Conversation.module.css'
_159
import {Button, Form, Icon, Input} from "antd";
_159
import ConversationsMessages from "./ConversationsMessages";
_159
import PropTypes from "prop-types";
_159
_159
class Conversation extends Component {
_159
constructor(props) {
_159
super(props);
_159
this.state = {
_159
newMessage: '',
_159
conversationProxy: props.conversationProxy,
_159
messages: [],
_159
loadingState: 'initializing',
_159
boundConversations: new Set()
_159
};
_159
}
_159
_159
loadMessagesFor = (thisConversation) => {
_159
if (this.state.conversationProxy === thisConversation) {
_159
thisConversation.getMessages()
_159
.then(messagePaginator => {
_159
if (this.state.conversationProxy === thisConversation) {
_159
this.setState({ messages: messagePaginator.items, loadingState: 'ready' });
_159
}
_159
})
_159
.catch(err => {
_159
console.error("Couldn't fetch messages IMPLEMENT RETRY", err);
_159
this.setState({ loadingState: "failed" });
_159
});
_159
}
_159
};
_159
_159
componentDidMount = () => {
_159
if (this.state.conversationProxy) {
_159
this.loadMessagesFor(this.state.conversationProxy);
_159
_159
if (!this.state.boundConversations.has(this.state.conversationProxy)) {
_159
let newConversation = this.state.conversationProxy;
_159
newConversation.on('messageAdded', m => this.messageAdded(m, newConversation));
_159
this.setState({boundConversations: new Set([...this.state.boundConversations, newConversation])});
_159
}
_159
}
_159
}
_159
_159
componentDidUpdate = (oldProps, oldState) => {
_159
if (this.state.conversationProxy !== oldState.conversationProxy) {
_159
this.loadMessagesFor(this.state.conversationProxy);
_159
_159
if (!this.state.boundConversations.has(this.state.conversationProxy)) {
_159
let newConversation = this.state.conversationProxy;
_159
newConversation.on('messageAdded', m => this.messageAdded(m, newConversation));
_159
this.setState({boundConversations: new Set([...this.state.boundConversations, newConversation])});
_159
}
_159
}
_159
};
_159
_159
static getDerivedStateFromProps(newProps, oldState) {
_159
let logic = (oldState.loadingState === 'initializing') || oldState.conversationProxy !== newProps.conversationProxy;
_159
if (logic) {
_159
return { loadingState: 'loading messages', conversationProxy: newProps.conversationProxy };
_159
} else {
_159
return null;
_159
}
_159
}
_159
_159
messageAdded = (message, targetConversation) => {
_159
if (targetConversation === this.state.conversationProxy)
_159
this.setState((prevState, props) => ({
_159
messages: [...prevState.messages, message]
_159
}));
_159
};
_159
_159
onMessageChanged = event => {
_159
this.setState({ newMessage: event.target.value });
_159
};
_159
_159
sendMessage = event => {
_159
event.preventDefault();
_159
const message = this.state.newMessage;
_159
this.setState({ newMessage: '' });
_159
this.state.conversationProxy.sendMessage(message);
_159
};
_159
_159
onDrop = acceptedFiles => {
_159
this.state.conversationProxy.sendMessage({contentType: acceptedFiles[0].type, media: acceptedFiles[0]});
_159
};
_159
_159
render = () => {
_159
return (
_159
<Dropzone
_159
onDrop={this.onDrop}
_159
accept="image/*">
_159
{({getRootProps, getInputProps, isDragActive}) => (
_159
<div
_159
{...getRootProps()}
_159
onClick={() => {
_159
}}
_159
id="OpenChannel"
_159
style={{position: "relative", top: 0}}>
_159
_159
{isDragActive &&
_159
<div className={styles.drop}>
_159
<Icon type={"cloud-upload"}
_159
style={{fontSize: "5em", color: "#fefefe"}}/>
_159
<h3 style={{color: "#fefefe"}}>Release to Upload</h3>
_159
</div>
_159
}
_159
<div
_159
className={styles.messages}
_159
style={{
_159
filter: `blur(${isDragActive ? 4 : 0}px)`,
_159
}}
_159
>
_159
<input id="files" {...getInputProps()} />
_159
<div style={{flexBasis: "100%", flexGrow: 2, flexShrink: 1, overflowY: "scroll"}}>
_159
<ConversationsMessages
_159
identity={this.props.myIdentity}
_159
messages={this.state.messages}/>
_159
</div>
_159
<div>
_159
<Form onSubmit={this.sendMessage}>
_159
<Input.Group compact={true} style={{
_159
width: "100%",
_159
display: "flex",
_159
flexDirection: "row"
_159
}}>
_159
<Input
_159
style={{flexBasis: "100%"}}
_159
placeholder={"Type your message here..."}
_159
type={"text"}
_159
name={"message"}
_159
id={styles['type-a-message']}
_159
autoComplete={"off"}
_159
disabled={this.state.loadingState !== 'ready'}
_159
onChange={this.onMessageChanged}
_159
value={this.state.newMessage}
_159
/>
_159
<Button icon="enter" htmlType="submit" type={"submit"}/>
_159
</Input.Group>
_159
</Form>
_159
</div>
_159
</div>
_159
</div>
_159
)}
_159
_159
</Dropzone>
_159
);
_159
}
_159
}
_159
_159
Conversation.propTypes = {
_159
myIdentity: PropTypes.string.isRequired
_159
};
_159
_159
export default Conversation;

Displaying New Messages Added to the Conversation

displaying-new-messages-added-to-the-conversation page anchor

Using React also lets us easily handle the case where a new message gets added to the conversation. We listen to the messageAdded event from the Twilio Conversations SDK Conversation object, and then append that message to the messages we already have, and then set the state for the React component.

React handles the rendering for us as the messages list changes, which is much easier than trying to keep the DOM in sync with the message list manually.

Conversations JS Quickstart - Conversation.js

conversations-js-quickstart---conversationjs-1 page anchor

_159
import React, { Component } from 'react';
_159
import './assets/Conversation.css';
_159
import MessageBubble from './MessageBubble'
_159
import Dropzone from 'react-dropzone';
_159
import styles from './assets/Conversation.module.css'
_159
import {Button, Form, Icon, Input} from "antd";
_159
import ConversationsMessages from "./ConversationsMessages";
_159
import PropTypes from "prop-types";
_159
_159
class Conversation extends Component {
_159
constructor(props) {
_159
super(props);
_159
this.state = {
_159
newMessage: '',
_159
conversationProxy: props.conversationProxy,
_159
messages: [],
_159
loadingState: 'initializing',
_159
boundConversations: new Set()
_159
};
_159
}
_159
_159
loadMessagesFor = (thisConversation) => {
_159
if (this.state.conversationProxy === thisConversation) {
_159
thisConversation.getMessages()
_159
.then(messagePaginator => {
_159
if (this.state.conversationProxy === thisConversation) {
_159
this.setState({ messages: messagePaginator.items, loadingState: 'ready' });
_159
}
_159
})
_159
.catch(err => {
_159
console.error("Couldn't fetch messages IMPLEMENT RETRY", err);
_159
this.setState({ loadingState: "failed" });
_159
});
_159
}
_159
};
_159
_159
componentDidMount = () => {
_159
if (this.state.conversationProxy) {
_159
this.loadMessagesFor(this.state.conversationProxy);
_159
_159
if (!this.state.boundConversations.has(this.state.conversationProxy)) {
_159
let newConversation = this.state.conversationProxy;
_159
newConversation.on('messageAdded', m => this.messageAdded(m, newConversation));
_159
this.setState({boundConversations: new Set([...this.state.boundConversations, newConversation])});
_159
}
_159
}
_159
}
_159
_159
componentDidUpdate = (oldProps, oldState) => {
_159
if (this.state.conversationProxy !== oldState.conversationProxy) {
_159
this.loadMessagesFor(this.state.conversationProxy);
_159
_159
if (!this.state.boundConversations.has(this.state.conversationProxy)) {
_159
let newConversation = this.state.conversationProxy;
_159
newConversation.on('messageAdded', m => this.messageAdded(m, newConversation));
_159
this.setState({boundConversations: new Set([...this.state.boundConversations, newConversation])});
_159
}
_159
}
_159
};
_159
_159
static getDerivedStateFromProps(newProps, oldState) {
_159
let logic = (oldState.loadingState === 'initializing') || oldState.conversationProxy !== newProps.conversationProxy;
_159
if (logic) {
_159
return { loadingState: 'loading messages', conversationProxy: newProps.conversationProxy };
_159
} else {
_159
return null;
_159
}
_159
}
_159
_159
messageAdded = (message, targetConversation) => {
_159
if (targetConversation === this.state.conversationProxy)
_159
this.setState((prevState, props) => ({
_159
messages: [...prevState.messages, message]
_159
}));
_159
};
_159
_159
onMessageChanged = event => {
_159
this.setState({ newMessage: event.target.value });
_159
};
_159
_159
sendMessage = event => {
_159
event.preventDefault();
_159
const message = this.state.newMessage;
_159
this.setState({ newMessage: '' });
_159
this.state.conversationProxy.sendMessage(message);
_159
};
_159
_159
onDrop = acceptedFiles => {
_159
this.state.conversationProxy.sendMessage({contentType: acceptedFiles[0].type, media: acceptedFiles[0]});
_159
};
_159
_159
render = () => {
_159
return (
_159
<Dropzone
_159
onDrop={this.onDrop}
_159
accept="image/*">
_159
{({getRootProps, getInputProps, isDragActive}) => (
_159
<div
_159
{...getRootProps()}
_159
onClick={() => {
_159
}}
_159
id="OpenChannel"
_159
style={{position: "relative", top: 0}}>
_159
_159
{isDragActive &&
_159
<div className={styles.drop}>
_159
<Icon type={"cloud-upload"}
_159
style={{fontSize: "5em", color: "#fefefe"}}/>
_159
<h3 style={{color: "#fefefe"}}>Release to Upload</h3>
_159
</div>
_159
}
_159
<div
_159
className={styles.messages}
_159
style={{
_159
filter: `blur(${isDragActive ? 4 : 0}px)`,
_159
}}
_159
>
_159
<input id="files" {...getInputProps()} />
_159
<div style={{flexBasis: "100%", flexGrow: 2, flexShrink: 1, overflowY: "scroll"}}>
_159
<ConversationsMessages
_159
identity={this.props.myIdentity}
_159
messages={this.state.messages}/>
_159
</div>
_159
<div>
_159
<Form onSubmit={this.sendMessage}>
_159
<Input.Group compact={true} style={{
_159
width: "100%",
_159
display: "flex",
_159
flexDirection: "row"
_159
}}>
_159
<Input
_159
style={{flexBasis: "100%"}}
_159
placeholder={"Type your message here..."}
_159
type={"text"}
_159
name={"message"}
_159
id={styles['type-a-message']}
_159
autoComplete={"off"}
_159
disabled={this.state.loadingState !== 'ready'}
_159
onChange={this.onMessageChanged}
_159
value={this.state.newMessage}
_159
/>
_159
<Button icon="enter" htmlType="submit" type={"submit"}/>
_159
</Input.Group>
_159
</Form>
_159
</div>
_159
</div>
_159
</div>
_159
)}
_159
_159
</Dropzone>
_159
);
_159
}
_159
}
_159
_159
Conversation.propTypes = {
_159
myIdentity: PropTypes.string.isRequired
_159
};
_159
_159
export default Conversation;


Now that you've seen how the Conversations JavaScript Quickstart implements several key pieces of functionality, you can see how to add the Conversations SDK to your React or JavaScript project. You can re-use these React components within your own web application's front end. If you're using Angular or Vue, some of the patterns in this React project should be applicable to your solution.

For more information, check out these helpful links:


Rate this page: