Implementing In-App Chat Using Pusher

Implementing In-App Chat Using Pusher

In this article, learn how to implement an in-app chat functionality for React applications by integrating Pusher

In this tutorial, I will be showing you how to build a chat application using React and Pusher. In this article, we are going to create a very simple application which comes with functionalities that shows some of rich features of Pusher while also exploring how you can easily combine it with a modern library like React.

What is Pusher?

Pusher is a platform that allows developers to easily build an application with real-time features quickly. It specializes with building real-time and scalable infrastructures for developers and is packaged with powerful features like client events, queryable APIs and Pub/Sub messaging amongst other features.

Why should we use Pusher channels for chat applications?

There are multiple advantages of integrating Pusher channels for chat apps. Some of them are:

  • It is easy and fast to configure and to understand.

  • We cab add presence indicators, typing indicators and read receipts to keep everyone up to speed.

  • It provides instant updates to users by connecting them directly with the person they need through in-app messaging.

  • Allows your app to connect in a convenient and meaningful way with 1:1 or multi-user chat rooms.

  • You have full control over the database of customer conversations.

Pusher Channels for chat also comes with a publish/subscribe model and they essentially use channels as a medium for users to communicate wherein a user can create a channel and the other user can subscribe to that channel. Your system will publish the changes to that channel whenever an update is made and update is sent to all the subscribers present on the channel. Now that we are familiar with the introduction to the technologies, lets get started with the implementation.

Pricing Model

Here is a pictorial depiction of what a pricing model will look like: price model.png

Get the API keys

Firstly Create an account and then create a Channels app. Go to the “Keys” page of that app and keep note of your app_id, key, secret and cluster.

Let's create our Node Server

  • Install the following dependencies for onto the server:

      npm install --save body-parser cors express pusher dotenv
    
  • Next, we will provide an endpoint to send messages to the chat app so that users can interact with each other on the server-side. Use the following code:

const Pusher = require("pusher");
const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");

const app = express();

app.use(cors());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
require("dotenv").config();

const pusher = new Pusher({
  appId: process.env.appId,
  key: process.env.key,
  secret: process.env.secret,
  cluster: process.env.cluster,
  encrypted: true,
});

app.post("/message", (req, res) => {
  const payload = req.body;
  pusher.trigger(req.query.channel, "message", payload);
  res.send(payload);
});
  • In the process given above, we have initialized Pusher with the required credentials and then created an endpoint to process messages from the Frontend of our application.

  • To publish the events, we will be using the trigger function which sends the event to the channel as an argument to the function using the code given below:

pusher.trigger(channel_name, event,  {message => 'hello world'});

Let's create the Frontend

  • Let's start off by installing the following dependencies to our client:
    npm install --save axios pusher-js
  • This component contains two input fields for accepting the channel name and username which are then sent to ChatScreen.js as props. The channel name can either be the user input or we can optionally keep it hidden from the user by giving a random name to the channel. In this case, I have given the user an option to enter the channel name as shown below:
const UserInput = ({ handleJoinBtn }) => {
  const [channelName, setChannelName] = useState("");
  const [userName, setUserName] = useState("");

  const [showError, setShowError] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (channelName.trim().length !== 0 && userName.trim().length !== 0) {
      handleJoinBtn(channelName, userName);
      setShowError(false);
      setChannelName("");
      setUserName("");
    } else {
      setShowError(true);
    }
  };
  return (
    <div className="wrapper">
      <h1 className="header">Pusher chat demo</h1>
      <form onSubmit={(e) => handleSubmit(e)} className="Container">
        {showError && (
          <div className="errorText">Both fields are Required </div>
        )}
        <div className="input-container">
          <span className="label">Enter Channel Name</span>
          <input
            type="text"
            className="input"
            value={channelName}
            onChange={(e) => setChannelName(e.target.value)}
          />
        </div>
        <div className="input-container">
          <span className="label">Enter your Name</span>
          <input
            type="text"
            value={userName}
            className="input"
            onChange={(e) => setUserName(e.target.value)}
          />
        </div>
        <div className="btn-container">
          <button type="submit" className="btn">
            Join
          </button>
        </div>
      </form>
    </div>
  );
};

export default UserInput;
  • The next component we will be handling is ChatScreen.js. In this component, we will creating a functionality by connecting and subscribing to the user(client-side) using Pusher. Use the code given below:

import Pusher from "pusher-js";

const ChatScreen = ({ channelName, userName }) => {
  const [chats, setChats] = useState([]);
  const [msg, setMsg] = useState();

  useEffect(() => {
    const pusher = new Pusher(process.env.REACT_APP_KEY, {
      cluster: process.env.REACT_APP_CLUSTER,
      encrypted: true,
    });
    const channel = pusher.subscribe(channelName);
    channel.bind("message", (data) => {
      setMsg(data);
    });
    return () => {
      pusher.unsubscribe(channelName);
    };
  }, []);

  useEffect(() => {
    if (msg) setChats([...chats, msg]);
  }, [msg]);

  return (
    <div className="wrapper">
      <div className="container">
        <div className="userProfile">Hello, {userName}</div>
        <ChatList chats={chats} username={userName} />
        <ChatInput channelName={channelName} username={userName} />
      </div>
    </div>
  );
};

export default ChatScreen;
  • In the next step, we will be opening the connection to the channel using the code given below:
const pusher = new Pusher(process.env.REACT_APP_KEY, {
      cluster: process.env.REACT_APP_CLUSTER,
      encrypted: true,
    });
  • As we already know, Pusher follows a publish/subscribe model because of which we will be subscribing to a particular channel using the following code:
    const channel = pusher.subscribe(channelName);
  • We also have to unsubscribe from the channel on unmounting the component. Use the code given below:
      pusher.unsubscribe(channelName);
  • By binding the particular event to our channel, we can listen to every published event and perform actions on the basis of the same. For example, we are binding our channel with a message event so that we can listen to a new event whenever it is published:
channel.bind("message", (data) => {
      setMsg(data);
    });
  • As you can see in the below image, the user is first connected to the channel and then subscribed, after which the channel is occupied by the user:

Screenshot 2021-06-14 at 6.40.48 PM.png

  • The next step is to integrate ChatList.js to our app example. This functionality is responsible for displaying the list of messages and the sender's name. Use the code given below to achieve this:
const ChatList = ({ chats, username }) => {
  return (
    <div className="chatsContainer">
      {chats.map((chat) => {
        return (
          <div className={chat.username === username ? "divRight" : "divLeft"}>
            <div
              className={
                chat.username === username
                  ? " commonStyle myChatContainer "
                  : "commonStyle chatContainer"
              }
              key={Math.random()}
            >
              {chat.username !== username && (
                <div className="msgAuthor">{chat.username}</div>
              )}
              <div>{chat.message}</div>
            </div>

            <div
              className={
                chat.username === username
                  ? "arrowRight arrow"
                  : "arrowLeft arrow"
              }
            ></div>
          </div>
        );
      })}
    </div>
  );
};

export default ChatList;
  • Next, we have to integrate ChatInput.js to our app. This component is responsible for storing the user's message in a state and on hitting send/enter, it will make an HTTP request to the local node server(mentioned above) along with username and message as a payload. This request will then trigger the message event after integrating the following code:
import "./styles.css";

const ChatInput = ({ channelName, username }) => {
  const [message, setMessage] = useState("");
  const [showErr, setShowErr] = useState(false);

  const sendMessage = (e) => {
    e.preventDefault();
    if (message.trim().length > 0) {
      let data = {
        username,
        message,
      };
      setShowErr(false);
      axios
        .post(`http://localhost:5000/message?channel=${channelName}`, data)
        .then(() => {
          setMessage("");
        });
    } else setShowErr(true);
  };

  return (
    <form className="inputContainer" onSubmit={(e) => sendMessage(e)}>
      <input
        type="text"
        className="inputElement"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
      />
      <button className="inputBtn" type="submit">
        Send
      </button>
      {showErr && <div className="errorText">Enter your message</div>}
    </form>
  );
};

export default ChatInput;

Adding additional features

The above code is an example of basic implementation of chat features. Using it as a base, we can now add a new feature where a user can know when the other user is typing. Refer to the code given below:

app.post("/userTyping", async function (req, res) {
  const username = req.body.username;
  const channelName = req.query.channelName;
  pusher.trigger(channelName, "user_typing", { username: username });
  res.status(200).send();
});
  • Let's start by adding a new endpoint on the server-side of our application. Here, we have also used the trigger function to publish the user-typing events.
  • Now, we will bind our application with a user-typing event which will listen whenever any user starts typing and return a result. We have used setTimeout so that this event will not show unnecessary typing events as shown below:
var clearInterval1 = 900; //0.9 seconds
var clearTimerId1;
useEffect(() => {
.....
  channel.bind("user_typing", function (data) {
      if (data.username !== userName) {
        var typingText = data.username + " is typing...";
        setTyping(typingText);
        clearTimeout(clearTimerId1);
        clearTimerId1 = setTimeout(function () {
          setTyping("");
        }, clearInterval1);
      }
    });
 return () => {
      pusher.unsubscribe(channelName);
    };
  }, []);

...
return (
  ...
        <ChatList chats={chats} username={userName} />
        {typing && <div className="typingContainer">{typing}</div>}
        <ChatInput channelName={channelName} username={userName} />
      ...
  );

Next, we will have to implement certain functionalities into ChatInput.js. Here, we will make an HTTP request which will trigger the user-typing event. We have set our throttle time to 200ms so that continuous HTTP calls will not be made on any character input and then we shall start passing the username of the user to the body of the HTTP request so that we can get know know which user is currently typing. Refer to the code given below:

...
var canPublish = true;
var throttleTime = 200;
const handleTextChange = (e) => {
    const payload = {
      username,
    };
    setMessage(e);
    if (canPublish) {
      axios
        .post(
          `http://localhost:5000/usertyping?channelName=${channelName}`,
          payload
        )
        .then((response) => {
          return response;
        })
        .catch(function (error) {
          console.log("error ----", error);
        });
      canPublish = false;
      setTimeout(function () {
        canPublish = true;
      }, throttleTime);
    }
  };
...
<input
        type="text"
        className="inputElement"
        value={message}
        onChange={(e) => handleTextChange(e.target.value)}
      />
...

Screenshot 2021-07-19 at 8.19.09 PM.png

Screenshot 2021-07-19 at 8.22.36 PM.png

  • As you can see in the above screenshot, User 2 is getting notified whenever other user (User 1 in this case) types something.

The Result

client1.png Screenshot 2021-06-16 at 3.51.02 PM.png

Conclusion

This article has covered the basic steps necessary to create a real-time chat application along with the user-typing feature using React and Pusher. I hope you have found this tutorial helpful.