Chat messaging is everywhere today. We can talk to customer support personnel through a web app that allows them to see our request and respond in real-time. We can interact with our friends and family, no matter where we are, through apps like WhatsApp and Facebook. There are multitudes of instant messaging apps, for many use cases, available today, even some that allow you to customize for a particular community or team (e.g Slack), however, you still may find you need to create your own real-time messaging app in order to reach and interact with a particular audience. This may be a social app for language learners or an app for a school to interact with students and parents. And you might be wondering, โ...how do I do this?โ.
There are many options available for building real-time applications, however, in this post, I'll show you how to use Stream Chat API with its custom React components to build a messenger style app. In addition, we will add authentication to the application using Auth0. Using these managed services helps us focus on building the application, leaving the concern of server management and scaling to the provider. The application we're going to build by the end of this post will support:
- A conversation list where a user can see their chat history.
- A typing indicator to tell who is typing.
- Message delivery status.
- A message thread to keep the discussion organized.
- Online/Offline statuses for users.
- Emoji support.
- File Attachment and Link preview.
And it's going to behave like this:
In the next post, we will add functionality to make phone calls, so stick around ๐. To follow along with this tutorial, you'll need to have knowledge of React.js, Node.js and npm installed (npm is distributed with Node.js โ which means that when you download Node.js, you automatically get npm installed on your machine). Alternatively, you can use yarn with any of the commands.
Getting Started With The React App
To save time on setup and design, we will be using create-react-app to create our React project. Open your command line application and run the following commands:
npx create-react-app react-messenger
cd react-messenger
This will set up the React project and install necessary dependencies. We used npx
, which is a tool that gets installed alongside npm (starting from version 5.2).
Setting Up Auth0
We will be using Auth0 to handle user authentication and user management. Auth0 is an Authentication-as-a-Service (or Identity-as-a-Service) provider that provides an SDK to allow developers to easily add authentication and manage users. Its user management dashboard allows for breach detection and multifactor authentication, and Passwordless login.
You need to create an application on Auth0 as a container for the users of this messenger app. You'll need some API keys to use the SDK. To create an application on Auth0, visit Auth0's home page to log in. Once you have logged in, click on the big button at the upper right-hand corner that says New Application. This should show a modal asking for an application name and a type. Give it the name react-messenger
, select Single Page Web Application, and then click the Create button. This should create an application on Auth0 for you.
Next, we need to set up an API on Auth0. In the side menu, click on APIs to show the API dashboard. At the upper right corner of the page, click the big Create API button. This shows a modal form asking for a name and an identifier. Enter react-messenger-api
as the name, and https://react-messenger-api
as the identifier. This will create an API for us. Click on the Settings tab and it should display the id, name, and identifier of the API. We will need this identifier value later on, as the audience
parameter on authorization calls. To learn more about this parameter, check out the documentation.
Secure the React App With Auth0
Now that we have our application setup in Auth0, we need to integrate it with React. We will create a class that will handle login, log out, and a way for the app to tell if the user is authenticated. In the src
directory, add a new file auth/config.js
with the content below:
export default {
clientId: "your auth0 clientId",
domain: "yourauth0domain.auth0.com",
redirect: "http://localhost:3000/close-popup",
logoutUrl: "http://localhost:3000",
audience: "https://react-messenger-api"
};
Replace the placeholder for domain
and clientId
with the data in your Auth0 application dashboard. In the settings page of the Auth0 application, update the fields Allowed Callback URLs
with http://localhost:3000/close-popup
, and Allowed Logout URLs
with http://localhost:3000
to match what we have in config.js
. The Allowed Callback URLs
setting is the URL that the Auth0 Lock widget will redirect to after the user is signed in. The other setting, Allowed Logout URLs
, is the URL to redirect to after the user is logged out.
Create another file src/auth/service.js
and add the code below to it:
import config from "./config";
import * as Auth0 from "auth0-js";
class Auth {
auth0 = new Auth0.WebAuth({
domain: config.domain,
clientID: config.clientId,
redirectUri: config.redirect,
audience: config.audience,
responseType: "id_token token",
scope: "openid profile email"
});
authFlag = "isLoggedIn";
userProfileFlag = "userProfile";
localLogin(authResult) {
localStorage.setItem(this.authFlag, true);
localStorage.setItem(
this.userProfileFlag,
JSON.stringify(authResult.idTokenPayload)
);
this.loginCallback(authResult.idTokenPayload);
}
login() {
this.auth0.popup.authorize({}, (err, authResult) => {
if (err) this.localLogout();
else {
this.localLogin(authResult);
}
});
}
isAuthenticated() {
return localStorage.getItem(this.authFlag) === "true";
}
getUserProfile() {
return JSON.parse(localStorage.getItem(this.userProfileFlag));
}
}
const auth = new Auth();
export default auth;
In the code above, we used the Auth0 client-side library, which we will add later as a dependency. We initialized it using details from the config.js. We have the login()
function which, when called, will trigger a pop-up window where users can log in or signup. The localLogin()
function stores some data to localStorage so that we can access them on page refresh. The loginCallback
function will be set later in src/App.js
so it can use the authentication result for some other operations. The idTokenPayload
has information such as email, name, and user id.
We are also going to build our logout functionality here. This will clear whatever we stored in localStorage from the previous section, as well as sign the user out of the system. Add the following code to the class we defined in the previous section:
localLogout() {
localStorage.removeItem(this.authFlag);
localStorage.removeItem(this.userProfileFlag);
this.logoutCallback();
}
logout() {
this.localLogout();
this.auth0.logout({
returnTo: config.logoutUrl,
clientID: config.clientId
});
}
Working With Our Auth Service
With the authentication service class complete, we will now use it in the React component. We will install the Auth0 dependency used earlier and add bootstrap, to beautify the UI a little. Open your terminal and run npm install --save bootstrap auth0-js
to install those dependencies. Then, open src/index.js
and add import 'bootstrap/dist/css/bootstrap.css
to include the bootstrap CSS on the page.
Open src/App.js
and update it with the following code:
import React, { Component } from "react";
import authService from "./auth/service";
import Conversations from "./Conversations";
import Users from "./Users";
class App extends Component {
constructor(props) {
super(props);
authService.loginCallback = this.loggedIn;
authService.logoutCallback = this.loggedOut;
const loggedIn = authService.isAuthenticated();
this.state = { loggedIn, page: "conversations" };
}
loggedIn = async ({ email, nickname }) => {
this.setState({ loggedIn: true });
};
loggedOut = () => {
this.setState({ loggedIn: false });
};
switchPage = page => this.setState({ page });
render() {
return (
<div>
<nav className="navbar navbar-dark bg-dark">
<a className="navbar-brand text-light">Messenger</a>
{this.state.loggedIn ? (
<div>
<button
onClick={() => this.setState({ page: "conversations" })}
type="button"
className="btn btn-link text-light"
>
Conversations
</button>
<button
onClick={() => this.setState({ page: "users" })}
type="button"
className="btn btn-link text-light"
>
Users
</button>
<button
onClick={() => authService.logout()}
className="btn btn-light"
>
Log Out
</button>
</div>
) : (
<button
onClick={() => authService.login()}
className="btn btn-light"
>
Log In
</button>
)}
</nav>
<div>{/* content goes here */}</div>
</div>
);
}
}
export default App;
What this component does is render a page with a navigation header. When the user is not logged in we show the login button which, when clicked, calls the login
function from the auth service. If they're logged in, they get two links to switch between the two pages for this application and a logout button. Since it's a small app we'll be using a boolean variable to determine what to display in the main content area below the navigation header. When the login button is clicked, it pops out a new window with a page asking the user to log in or sign up. When they're done with signup or login, it will redirect to the URL we set for Allowed Callback URLs
in the application's settings page in Auth0's dashboard, which is http://localhost:3000/close-popup
. At the moment we don't have that page so we'll set it up.
Add a new file in the root public folder named close-popup/index.html
with the content below:
<!DOCTYPE html>
<html lang="en">
<head>
<meta
charset="utf-8"
content="font-src: 'self' data: img-src 'self' data: default-src 'self'"
/>
<title></title>
<script src="https://cdn.auth0.com/js/auth0/9.8.1/auth0.min.js"></script>
</head>
<body>
<script type="text/javascript">
const webAuth = new auth0.WebAuth({
domain: "yourname.auth0.com",
clientID: "your client id"
});
webAuth.popup.callback();
</script>
</body>
</html>
You should replace the two lines indicating domain
and clientID
with your Auth0 application credentials. This will close the window once the page gets redirected here.
Adding Stream Chat Messaging For Real-Time Conversation
So far we have our app set up to allow users to log in and log out. Now we need to allow them to chat with each other. We're going to build this functionality using Stream Chatโs messaging SDK. The awesomeness of using this is that it provides a Chat SDK with an easy-to-work-with API for building real-time messaging applications. Some of its features include:
- Chat threads to provide a good way to reply to specific messages.
- Emoji chat reactions just like you would on Facebook or Slack.
- Ability to send emojis and file attachments.
- Direct and group chats.
- Search function for messages or conversations.
Another interesting addition is that it provides UI components that you can use in your app to speed up development. At the time of this writing, it's only available for React Native and React. We will be using the React UI component to add messaging functionality to our React application. This is because out of the box it provides components to view a list of existing conversations, send and receive messages in real-time, chat threads and message reactions.
To get started using Stream messaging SDK, you'll need to sign up and sign in to the dashboard. Then, click the Create App button at the upper right corner of the page. Enter the app name react-messenger
, select your preferred server location, and whether it's a production app or in development.
Once created, you should see the secret, key, and region it's hosted on. Copy the app's key as you'll be needing this soon. Open your command line and run npm install --save stream-chat-react
. This package contains the Stream Chat React component which we will use and also install the stream chat SDK stream-chat
. We're going to use the stream-chat
module to create a chat client and connect to the Chat server.
Add a new file src/chat/service.js
and paste the content below in it:
import { StreamChat } from "stream-chat";
const tokenServerUrl = "http://localhost:8080/v1/token";
const chatClient = new StreamChat("API_KEY");
const streamServerFlag = "streamServerInfo";
let isClientReady = localStorage.getItem(streamServerFlag) !== null;
export const initialiseClient = async (email, name) => {
if (isClientReady) return chatClient;
const response = await fetch(tokenServerUrl, {
method: "POST",
mode: "cors",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
email,
name
})
});
const streamServerInfo = await response.json();
localStorage.setItem(streamServerFlag, JSON.stringify(streamServerInfo));
chatClient.setUser(
{
id: streamServerInfo.user.id,
name: streamServerInfo.user.name,
image: streamServerInfo.user.image
},
streamServerInfo.token
);
isClientReady = true;
return { chatClient, user: { ...streamServerInfo.user } };
};
export const getClient = () => {
const streamServerInfo = JSON.parse(localStorage.getItem(streamServerFlag));
chatClient.setUser(
{
id: streamServerInfo.user.id,
name: streamServerInfo.user.name,
image: streamServerInfo.user.image
},
streamServerInfo.token
);
return { chatClient, user: { ...streamServerInfo.user } };
};
export const isClientInitialised = () => isClientReady;
export const resetClient = () => {
localStorage.removeItem(streamServerFlag);
};
The code we added allows us to create a chat client and set the user for the client. It is with this chat client that the application will interact with the stream chat server. To initialize the chat client you need the API key which you copied from the Stream dashboard. Weโll then call chatClient.setUser()
to set the current user. The setUser()
function takes two parameters. An object which contains the user's name and id, and the token needed to authenticate the client. That information will be coming from a server we will add later. We call into that server with the name
, and email
we get from Auth0, and it'll generate and return an id, name, image, and token. Once the user is set, we return the chat client and the user info from the token server we will add later.
Adding The User List Page
With our chat service done, we're going to add a page that will list the users in the application and a user can select who to chat with.
Add a new file src/Users.js
with the content below:
import React, { Component } from "react";
export default class Users extends Component {
constructor(props) {
super(props);
this.state = { users: [] };
}
async componentDidMount() {
const { users } = await this.props.chatClient.queryUsers({
id: { $ne: this.props.user.id }
});
this.setState({ users });
}
startConversation = async (partnerId, partnerName) => {
const userId = this.props.user.id;
const userName = this.props.user.name;
const filter = {
id: { $in: [userId, partnerId] }
};
const channels = await this.props.chatClient.queryChannels(filter);
if (channels.length > 0) {
alert("chat with this user is already in your conversation list");
} else {
const channel = this.props.chatClient.channel("messaging", userId, {
name: `Chat between ${partnerName} & ${userName}`,
members: [userId, partnerId]
});
await channel.create();
this.props.switchPage("conversations");
}
};
render() {
return (
<div>
<div class="list-group">
{this.state.users.map(user => (
<button
onClick={() => this.startConversation(user.id, user.name)}
key={user.id}
type="button"
class="list-group-item list-group-item-action"
>
{user.name}
{": "}
{user.online
? "online"
: `Last seen ${new Date(user.last_active).toString()}`}
</button>
))}
</div>
</div>
);
}
}
We've created a component that'll receive the chat client as props from a parent container. It queries the stream chat server for users using chatClient.queryUsers({ id: { $ne: this.props.user.id } })
. The queryUsers
function allows you to search for users and see if they are online/offline. The filter syntax uses Mongoose style queries and queryUsers
takes in three parameters. The first argument is the filter object, the second is the sorting and the third contains any additional options. Above, we used queryUsers
to query for all users except the currently logged in user. As an aside, because this function doesn't run MongoDB in the background, only a subset of its query syntax is available. You can read more in the docs.
The startConversation
function is called when a user is selected from the rendered user list. It checks if a conversation between those two users exists, and if not, it creates a conversation channel for them. To start the conversation we create a channel by calling chatClient.channel()
and passing it the type of channel and the channel id, as well as an object specifying the channel name and it's members (if it's a private channel), as the third argument. This object can contain any custom properties but the ones we've used, in addition to an image
field are reserved fields for Stream Chat. We used the logged in user's id as the channel id and, because we're building a messenger style app, I've set the channel type (see below) to messaging
.
There are 5 built-in channel types. They are:
- Livestream: Sensible defaults in case you want to build chat like Twitch or football public chat stream.
- Messaging: Configured for apps such as Whatsapp or Messenger.
- Gaming: Configured for in-game chat.
- Commerce: Good defaults for building something like your own version of Intercom or Drift.
- Team: For if you want to build your own version of Slack or something similar.
While those are the custom defined channel types, you can also create your own and customize it to fit your needs. Check the documentation for more info on this.
When we initialize a channel by calling chatClient.channel()
, it returns a channel object. Then, the app creates the channel by calling await channel.create()
, to create it on the server. When that's completed, switchPage("conversations")
is called, to take the user back to the conversation screen where they see a list of their conversations and chats with other users.
Adding The Conversation Page
Next up is to create the conversation page. We're going to make a new React component. We will use the components from the stream-chat-react
library. Add a new file src/Conversations.js
and update it with the content below:
import React from "react";
import {
Chat,
Channel,
ChannelList,
Window,
ChannelHeader,
MessageList,
MessageInput,
Thread
} from "stream-chat-react";
import "stream-chat-react/dist/css/index.css";
const App = props => {
const filters = { type: "messaging", members: { $in: [props.userId] } };
return (
<Chat client={props.chatClient} theme={"messaging dark"}>
<ChannelList filters={filters} />
<Channel>
<Window>
<ChannelHeader />
<MessageList />
<MessageInput />
</Window>
<Thread />
</Channel>
</Chat>
);
};
export default App;
Here we have used eight components from stream-chat-react
library. The <Chat />
component creates a container to hold the chat client and the theme which will be passed down to child components, as needed. The <ChannelList />
component is used to render a list of channels. The <Channel />
component is a wrapper component for a channel. It has two required props which are channel
and client
. The client
prop will be set automatically by the Chat
component while the channel
prop will automatically be set by the <ChannelList />
component when a channel is selected. When a channel is selected, we want to render a view where users can see the list of messages for that conversation/channel, enter messages, and respond to message threads. For this we've used the <ChannelHeader />
, <MessageList />
, <MessageInput />
, and <Thread />
components.
Using these components automatically gives us the following features:
- URL preview (Try sending a link to a Youtube video to see this in action)
- Video Playback
- File uploads & Previews
- Slash commands such as /giphy and /imgur.
- Online Presence โ Who is online
- Typing Indicators
- Message Status Indicators (sending, received)
- Emoticons
- Threads/Replies
- Reactions
- Autocomplete on users, emoticons, and commands
With these components ready, we need to render them in App.js when the user is logged in and navigates pages using the links in the navigation header. Open src/App.js
and import the chat service as follows:
import {
getClient,
initialiseClient,
isClientInitialised,
resetClient
} from "./chat/service";
Then update line 18 (in the constructor) to:
if (loggedIn && isClientInitialised()) {
const { chatClient, user } = getClient();
this.state = { loggedIn, page: "conversations", chatClient, user };
} else this.state = { loggedIn, page: "conversations" };
This will call getClient()
to create a chat client using the info we already have from the token server. We will also update the loggedIn
and loggedOut
function to initialize the chat client and invalidate the chat client respectively.
loggedIn = async ({ email, nickname }) => {
const { chatClient, user } = await initialiseClient(email, nickname);
this.setState({ loggedIn: true, chatClient, user });
};
loggedOut = () => {
resetClient();
this.setState({ loggedIn: false });
};
We will update our render()
function to add new variables used in determining the page to show as follows:
const showConversations =
this.state.loggedIn && this.state.page === "conversations";
const showUsers = this.state.loggedIn && this.state.page !== "conversations";
Then replace the comment {\* content goes here *\}
with the following:
{
showConversations && (
<Conversations
chatClient={this.state.chatClient}
userId={this.state.user.id}
/>
);
}
{
showUsers && (
<Users
chatClient={this.state.chatClient}
user={this.state.user}
switchPage={this.switchPage}
/>
);
}
With all these modifications the App.js file should look exactly like this:
import React, { Component } from "react";
import authService from "./auth/service";
import Conversations from "./Conversations";
import Users from "./Users";
import {
getClient,
initialiseClient,
isClientInitialised,
resetClient
} from "./chat/service";
class App extends Component {
constructor(props) {
super(props);
authService.loginCallback = this.loggedIn;
authService.logoutCallback = this.loggedOut;
const loggedIn = authService.isAuthenticated();
if (loggedIn && isClientInitialised()) {
const { chatClient, user } = getClient();
this.state = { loggedIn, page: "conversations", chatClient, user };
} else this.state = { loggedIn, page: "conversations" };
}
loggedIn = async ({ email, nickname }) => {
const { chatClient, user } = await initialiseClient(email, nickname);
this.setState({ loggedIn: true, chatClient, user });
};
loggedOut = () => {
resetClient();
this.setState({ loggedIn: false });
};
switchPage = page => this.setState({ page });
render() {
const showConversations =
this.state.loggedIn && this.state.page === "conversations";
const showUsers =
this.state.loggedIn && this.state.page !== "conversations";
return (
<div>
<nav className="navbar navbar-dark bg-dark">
<a className="navbar-brand text-light">Messenger</a>
{this.state.loggedIn ? (
<div>
<button
onClick={() => this.setState({ page: "conversations" })}
type="button"
className="btn btn-link text-light"
>
Conversations
</button>
<button
onClick={() => this.setState({ page: "users" })}
type="button"
className="btn btn-link text-light"
>
Users
</button>
<button
onClick={() => authService.logout()}
className="btn btn-light"
>
Log Out
</button>
</div>
) : (
<button
onClick={() => authService.login()}
className="btn btn-light"
>
Log In
</button>
)}
</nav>
<div>
{showConversations && (
<Conversations
chatClient={this.state.chatClient}
userId={this.state.user.id}
/>
)}
{showUsers && (
<Users
chatClient={this.state.chatClient}
user={this.state.user}
switchPage={this.switchPage}
/>
)}
</div>
</div>
);
}
}
export default App;
Adding The Token Server
Now our frontend is done and we're close to completion! Next, we need to add the token server we mentioned earlier, which is needed to generate a user token and other data for use with the stream chat client. We won't build this from scratch but rather clone a project from GitHub which will do this for us. The project repository can be found on GitHub. Follow the instructions below to set it up:
- Open your terminal and run
git clone https://github.com/nparsons08/stream-chat-boilerplate-api.git && cd stream-chat-boilerplate-api
to clone the repository. - Run
npm install
to install the Node.js dependencies. - Once that's done, add a new file
.env
with the content below.
NODE_ENV=development
PORT=8080
STREAM_API_KEY=your_api_key
STREAM_API_SECRET=your_app_secret
Replace the value for STREAM_API_KEY
and STREAM_API_SECRET
with what you find in your Stream Chat dashboard. Then start the token server by running npm start
. This will start the token server and display Running on port 8080 in development mode. ๐
in the console.
Running And Testing The App
We have the token server running. Now we run the React app by running npm start
. This will open the browser and navigate us to localhost:3000
. Then you need to login and try out the app! Try running it from different browsers with different users. Use the /giphy command and freely share videos. Add messages reaction and try out the features I mentioned earlier!
That's A Wrap ๐
Almost everything in today's world happens in real-time. You receive a real-time notification if someone you follow starts a live video on Instagram. You can send messages in real-time through WhatsApp and get the other individuals response within milliseconds. You may have the need to add real-time messaging to your app, build a Slack competitor or some other social app allowing users to communicate in real-time.
In this post, I showed you how to build a messenger style chat application in React using the Stream Chat React SDK, and the Stream Chat React components. You have tested the application and have seen how rich it is with just a few lines of code. We also added security to the app using Auth0. With this knowledge, you can start building a messaging app under few hours and shipping your prototype in a short time. While we focused on text in this post, in the next one we'll add video call feature to the app. So, don't miss the next one!! ๐
Here's the link to the repository for what we built on GitHub.
For more information on https://getstream.io/chat/, youโll enjoy the API tour here.