Tutorials

How To Build a Real-Time Chat app with MERN Stack and SuprSend Javascript SDK?

Anjali Arya
June 23, 2023
Build powerful chat apps with real-time push notifications. Learn how to integrate SuprSend for seamless communication and enhanced user engagement.
TABLE OF CONTENTS

In this comprehensive tutorial, we will guide you through the process of building a real-time chat application using the powerful MERN stack and integrating SuprSend for seamless notifications. By combining the robust features of MongoDB as our database, Express.js for server-side development, React.js for building a dynamic user interface, and Node.js as our runtime environment, we'll create a highly scalable and efficient chat app. 

Additionally, we'll leverage the capabilities of SuprSend, a versatile notification platform, to enhance user engagement and provide real-time updates. Some of the business use cases for a real-time chat app are as follows: 

  • Instant customer support
  • Efficient team collaboration
  • Quick decision-making
  • Real-time project updates
  • Streamlined internal communication
  • Enhanced customer engagement
  • Seamless sales and lead generation
  • Effective remote team coordination
  • Interactive live events and webinars
  • Personalized user experiences.
Github Repo: SuprSend-NotificationAPI/Chatting-App (github.com)
Deployed Link: Chirpy Chat (mychttingapp.netlify.app)

Setting up the backend:

Let's start Setting up the backend. We install the necessary dependencies in the server directory.

Copied ✔

cd server
npm init
npm i axios cors express socket.io dotenv bcryptjs
    


Install Nodemon. Nodemon is a Node.js tool that automatically restarts the server after detecting file changes, and Socket.io allows us to configure a real-time connection on the server.

Configure Nodemon by adding the start command to the list of scripts in the package.json file. The code snippet below starts the server using Nodemon.

Copied ✔

 "scripts": {
				"test": "echo \"Error: no test specified\" && exit 1",
			 	"start": "nodemon index.js"
			 },
    


We can now run the server with Nodemon by using the command below.

npm start

If you are not using nodemon, either run the server with the command "node index.js" or modify the script by adding the start command to the list of scripts in the package.json file as shown below

Copied ✔

 "scripts": {
				"test": "echo \"Error: no test specified\" && exit 1",
			 	"start": "node index.js"
			 },
    


Modify package.json to make index.js an ECMAScript Module by adding "type": "module".

Copied ✔

{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon server.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^1.4.0",
    "bcryptjs": "^2.4.3",
    "cors": "^2.8.5",
    "dotenv": "^16.1.4",
    "express": "^4.18.2",
    }
}
    


Enter the following code in index.js:

Copied ✔

import express from "express";
import dotenv from "dotenv";
const app = express()
import { createServer } from "http";
import cors from "cors";
const server = createServer(app);


        dotenv.config();
        const PORT = process.env.PORT || 4000;

        app.use(express.urlencoded({ extended: true }));
        app.use(express.json());
        app.use(cors());

        app.get("/", (req, res) => {
        res.send("Hey!! This is a sign that the server is running");
        });

        server.listen(PORT, () => console.log("Server is running on port", PORT));
    

Setting up the frontend:

Navigate into the frontend folder via your terminal and create a new React.js project.

Copied ✔

cd frontend
npx create-react-app ./
    

Installing dependencies: 

Install Socket.io client API and React Router. React Router is a popular routing library for React applications. It allows us to handle navigation and routing in a declarative manner within the React components.

npm install socket.io-client react-router-dom axios


We'll also use react toastify and emoji-mart here. Emoji mart is a npm package used to include emojis in chats.

 npm install react-toastify emoji-mart @emoji-mart/data @emoji-mart/react

Cleaning up unnecessary data: 

Delete the redundant files such as the logo and the test files from the React app, and update the App.js file to display Hello World as below. Run npm start to verify the app is working fine.

Copied ✔

function App() {
  return (
		

Hello World!

); } export default App;

Adding Styles:

The following is the CSS code for index.css:

Copied ✔

* {
  font-family: "Space Grotesk", sans-serif;

  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
    monospace;
}

.input {
  border: 2px solid #008cba;

  border-radius: 4px;

  background-color: #f8f8f8;

  color: #000;

  transition: 0.3s;

  box-sizing: border-box;

  outline: none;

  margin: 0 5px;

  width: 50%;

  padding: 10px 15px;
}

.input:focus {
  border-color: #66afe9;

  outline: none;
}

.form {
  width: 100%;

  height: 100vh;

  display: flex;

  flex-direction: column;

  align-items: center;

  justify-content: center;
}

.form > label {
  margin-bottom: 15px;
}

.form > input {
  width: 70%;

  padding: 10px 15px;

  margin-bottom: 15px;
}

.welcome-container {
  width: 100%;

  min-height: 100%;

  display: flex;

  align-items: center;

  justify-content: space-between;

  padding: 10px;
}
.btn {
  padding: 15px;

  cursor: pointer;

  margin: 10px;

  font-size: 16px;

  outline: none;

  width: 200px;

  background-color: #007bff;

  color: white;

  text-align: center;

  text-decoration: none;
 
  border-radius: 4px;

  border: none;
 
  transition: all 0.4s ease;

}

.btn:hover {
  transform: translateY(-4px);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
    

Building the frontend:

Register & Login page:

Let's create Register & Login page:

Copied ✔

// This is for creating a SignUp page

import React  from "react";

const Register = () => {
 
  const handleRegister = async (e) => {
   
  };

  return (
    
); }; export default Register; // This is for creating a Login page import React from "react"; const Login = () => { const handleLogin = async (e) => { }; return (
); }; export default Login;

Chat Page:

Also, create a folder named pages in src and inside it, create chatpage.js. This page, as the name suggests, is incharge of handling the appearance and functionalities of chatting in app.

        mkdir pages cd pages        touch chatpage.js


We’ll write code in it after a while,

Lets's add the routes and react-toastify imports in app.js. We'll add the routes using Browser Router.

Copied ✔

import "./App.css";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { useContext } from "react";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import Register from "./components/register"; // Add this
import Chatpage from "./pages/chatpage";
import Login from "./components/login"; // Add this


function App() {
  return (

      
        } />
        } />
        
          }
        />
      
      
    
  );
}

export default App;

// Also modify index.js file inside src as shown below:

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  // 
  // 

  
    
  
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

    

Using Context API: 

We will use context api for keeping authentication and user information. Create a folder named context in src, and create context.js inside it.

Copied ✔

import React, { createContext, useState, useEffect } from "react";

const AuthContext = createContext();

const AuthProvider = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [user, setUser] = useState();
  const [chats, setChats] = useState();
  const [selectedChat, setSelectedChat] = useState();
 

  useEffect(() => {
    const loggedInUser = JSON.parse(localStorage.getItem("user"));
    setUser(loggedInUser);
  }, []);

  return (
    
      {children}
    
  );
};

export { AuthContext, AuthProvider };
    

Protecting the routes:

 Now to protect our chatpage route from unauthorized access, we will create a protected route component. 

Copied ✔

import { useContext } from "react";
import { AuthContext } from "../context/context";
import {Navigate} from "react-router-dom"
function ProtectedRoute({ children, ...props }) {
  const { isAuthenticated } = useContext(AuthContext);

  if (!isAuthenticated) {
    return 
  }
  return children;
}
export default ProtectedRoute;
    


Above part ensures that only a logged in user can navigate to chat page.

Now the App.js will be modified to redirect to chatpage when a user is logged in. If an unauthorised user tries to access the chatpage route, he will be redirected to login page.

Copied ✔

import "./App.css";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { useContext } from "react";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import Register from "./components/register"; 
import Chatpage from "./pages/chatpage";
import Login from "./components/login"; 

import { AuthProvider } from "./context/context"; // Add this
import ProtectedRoute from "./components/protectedroute"; //Add this


function App() {
  return (
    
      
        } />
        } />
        
              
            
          }
        />
      
      
    
  );
}

export default App;
    


Now, we’ll complete login and register components. Let’s modify login and register components to setup the useNavigate hook and handle login and register functionalities:

Register.js:

Copied ✔

import React, { useState, useEffect, useContext } from "react";
import { AuthContext } from "../context/context";
import axios from "axios";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";

const Register = () => {
  const { isAuthenticated,setIsAuthenticated} = useContext(AuthContext);
  const [name, setName] = useState(""); // Add this
  const [email, setEmail] = useState(""); // Add this
  const [password, setPassword] = useState(""); // Add this
  const navigate = useNavigate();

  const getUserFromLocalStorage = () => {
    const result = localStorage.getItem("user");
    const user = result ? JSON.parse(result) : null;
    return user;
  };
  useEffect(() => {
    if (getUserFromLocalStorage("user")) {
      setIsAuthenticated(true)
        navigate("/");
    
    }
  }, [navigate]);
  const handleRegister = async (e) => {
    e.preventDefault();

    try {
      const { data } = await axios.post(
        `${process.env.REACT_APP_URL}/auth/register`,
        {
          name,
          email,
          password,
        },
        {
          headers: {
            "Content-Type": "application/json",
          },
        }
      );
        
      console.log(data);
      toast.success("Success!", {
        duration: 5000,
        isClosable: true,
        position: toast.POSITION.TOP_CENTER,
      });

      localStorage.setItem("user", JSON.stringify(data));
      navigate("/login");
    } catch (error) {
      console.log(error.response.data.error);
      toast.error(error.response.data.error, {
        duration: 5000,
        isClosable: true,
        position: toast.POSITION.TOP_CENTER,
      });
    }
  };

  return (
    
setName(e.target.value)} value={name} /> setEmail(e.target.value)} value={email} /> setPassword(e.target.value)} />
); }; export default Register;


Login.js:

Copied ✔

import React, { useContext, useState } from "react";
import { toast } from "react-toastify";
import axios from "axios";
import { useNavigate } from "react-router-dom";
import { AuthContext } from "../context/context"; // Add this

const Login = () => {
  const { setIsAuthenticated } = useContext(AuthContext);
  const [email, setEmail] = useState(""); // Add this
  const [password, setPassword] = useState(""); // Add this

  const navigate = useNavigate();

  const handleLogin = async (e) => {
    e.preventDefault();
    try {
      const { data } = await axios.post(
        `${process.env.REACT_APP_URL}/auth/login`,
        {
          email,
          password,
        },
        {
          headers: {
            "Content-Type": "application/json",
          },
        }
      );
      console.log(data);
      setIsAuthenticated(true)
      toast.success(`Welcome! ${data.name}. Please wait...`, {
        duration: 5000,
        isClosable: true,
        position: toast.POSITION.TOP_CENTER,
      });

      localStorage.setItem("user", JSON.stringify(data));
      
      setTimeout(() => {
        navigate("/");
        
      }, 4000);
      
    } catch (error) {
      console.log(error.response.data.error);
      setIsAuthenticated(false)
      toast.error(error.response.data.error, {
        duration: 5000,
        isClosable: true,
        position: toast.POSITION.TOP_CENTER,
      });
    }
  };

  return (
    
setEmail(e.target.value)} value={email} /> setPassword(e.target.value)} />
); }; export default Login;

Completing the ChatPage:    

Let's create chatpage for the app. Put the following code in pages/chatpage.js:

Copied ✔

import { useContext, useState } from "react";
import ChatContainer from "../components/ChatContainer";
import MyChats from "../components/MyChats";
import { AuthContext } from "../context/context";
import SideBar from "../components/sidebar";
const Chatpage = () => {
  const { user } = useContext(AuthContext);
  const [fetchAgain, setFetchAgain] = useState(false);

  return (
    
{user && }
{user && } {user && }
); }; export default Chatpage;


Now, create Chatcontainer.js, Mychats.js,Groupchatmodal.js, Scrollablechat.js, singlechat.js, sidebar.js, updategroupchatmodel.js, userbadgeitem.js and userlistitem.js files inside components folder.

Also create a .env file in the root directory( the parent directory containing public,src etc ) and add the following content

REACT_APP_URL=http://localhost:4000

Backend Development

Since our basic frontend part is ready, let's move on to the backend.

In this app, we’ll use jwt for authentication and authorization. 

Including Mongodb, Mongoose, JWT:

 Install mongodb driver, mongoose and jsonwebtoken.

  npm install mongoose mongodb jsonwebtoken 


* Add the following code in index.js of backend.

Copied ✔

import express from "express";
import dotenv from "dotenv";
const app = express();
import mongoose from "mongoose";

        //... Rest of the imports

        dotenv.config();
        const PORT = process.env.PORT || 4000;

        app.use(cors());
        app.use(express.urlencoded({ extended: true }));
        app.use(express.json());
        // Add this
        mongoose
        .connect(process.env.MONGODB_URI, {
            useNewUrlParser: true,
            useUnifiedTopology: true,
        })
        .then(() => console.log("Database connected!"))
        .catch((err) => console.error(err));

        //... Rest of the code
    

Creating database schemas and models:

Let's create the models now. Create a folder models. Inside models, create user.js, chat.js, message.js to create schemas for user details, chat details and message and their respective models.

Firstly install validator package. Validator package provides many functions to validate different types of data, such as URLs,emails,, dates, and mobile phone numbers, among others. You can also use it to sanitize data.

npm install validator


Now put the following code in models/user.js:

Copied ✔

import mongoose from "mongoose";
        import validator from "validator";

        const userSchema = new mongoose.Schema({
        name: {
            type: String,
            required: [true, "Please Provide a Username"],
            trim: true,
            minlength: 4,
        },
        email: {
            type: String,
            required: [true, "Please provide an email"],
            unique: true,
            trim: true,
            validate: {
            validator: validator.isEmail,
            message: "Please Provide Email",
            },
        },
        password: {
            type: String,
            required: [true, "Please Provide Password"],
            minlength: 8,
            trim: true,
        },
        });

        export default mongoose.model("User", userSchema);
        
//  In models/chat.js, enter the below code.

import mongoose from "mongoose";

        const chatSchema = new mongoose.Schema(
        {
            chatName: {
            type: String,
            trim: true,
            },
            isGroupChat: {
            type: Boolean,
            default: false,
            },
            users: [
            {
                type: mongoose.Types.ObjectId,
                ref: "User",
            },
            ],
            latestMessage: {
            type: mongoose.Types.ObjectId,
            ref: "Message",
            },
            groupAdmin: {
            type: mongoose.Types.ObjectId,
            ref: "User",
            },
        },
        { timestamps: true }
        );

        export default mongoose.model("Chat", chatSchema);
        
// Finally for the message model in models/message.js, the following code will be used

import mongoose from "mongoose";

        const messageSchema = new mongoose.Schema(
        {
            sender: {
            type: mongoose.Types.ObjectId,
            ref: "User",
            },
            message: {
            type: String,
            trim: true,
            },
            chat: {
            type: mongoose.Types.ObjectId,
            ref: "Chat",
            },
        },
        {
            timestamps: true,
        }
        );

        export default mongoose.model("Message", messageSchema);
    

Creating the controllers:

Create a folder named controllers in backend. This folder will contain three files:

  1. auth.js: this file has the code for login,register and search functions.
  2. chat.js : this file has the code for various functions pertaining to chats, group chats etc.
  3. message.js: This file has the code for message related functions like sending a  message and getting all messages

Put the following code in controllers/auth.js. We are also including the code for creating a jwt.

Copied ✔

import jwt from "jsonwebtoken";
import User from "../models/user.js";
import bcrypt from "bcryptjs";

const register = async (req, res) => {
  const { name, email, password } = req.body;

  if (!name || !email || !password) {
    return res.status(404).json({ error: "Enter all the values" });
  }

  const isUserExists = await User.findOne({ email: email });

  if (isUserExists) {
    return res
      .status(404)
      .json({ error: "User with this email already exists" });
  }

  const hashPassword = await bcrypt.hash(password, 10);

  const user = await User.create({
    name,
    email,
    password: hashPassword,
  });

  const token = jwt.sign(
    {
      id: user._id,
    },
    process.env.JWT_SECRET,
    {
      expiresIn: process.env.JWT_LIFETIME,
    }
  );

  res.status(201).json({
    _id: user._id,
    name: user.name,
    email: user.email,
    token,
  });
};

const login = async (req, res) => {
  const { email, password } = req.body;

  if (!email || !password) {
    return res.status(400).json({ error: "Please Provide All the Values" });
  }

  const isUser = await User.findOne({ email: email });

  if (!isUser) {
    return res.status(400).json({ error: "Invalid credentials" });
  }

  const comparePassword = await bcrypt.compare(password, isUser.password);

  if (!comparePassword) {
    return res.status(400).json({ error: "Invalid credentials" });
  }

  const token = jwt.sign(
    {
      id: isUser._id,
    },
    process.env.JWT_SECRET,
    {
      expiresIn: process.env.JWT_LIFETIME,
    }
  );

  res.status(200).json({
    success: true,
    _id: isUser._id,
    name: isUser.name,
    email: isUser.email,
    token,
  });
};

const searchUser = async (req, res) => {
  const { search } = req.query;

  const user = await User.find({
    name: { $regex: search, $options: "i" },
  }).select("name _id email");

  res.status(200).json(user);
};

export { register, login, searchUser };
    


Add the details like JWT_SECRET and JWT_LIFETIME in the .env file. JWT_SECRET is a secret message which can be a string used for encryption and JWT_LIFETIME denotes the time for which jwt is valid. It can have values like 30m,10d etc for 30 minutes, 10 days.

JWT_SECRET= somesecretmessage JWT_LIFETIME=40m


Enter the following code in the controllers/chat.js file:

Copied ✔
    
import Chat from "../models/chat.js";
import User from "../models/user.js";

const getChat = async (req, res) => {
  const { userId } = req.body;

  if (!userId) {
    return res.send("No User Exists!");
  }

  let chat = await Chat.find({
    isGroupChat: false,
    $and: [
      { users: { $elemMatch: { $eq: req.user.id } } },
      { users: { $elemMatch: { $eq: userId } } },
    ],
  })
    .populate("users", "-password")
    .populate("latestMessage");

  chat = await User.populate(chat, {
    path: "latestMessage.sender",
    select: "name email _id",
  });

  if (chat.length > 0) {
    res.send(chat[0]);
  } else {
    const createChat = await Chat.create({
      chatName: "sender",
      isGroupChat: false,
      users: [req.user._id, userId],
    });

    const fullChat = await Chat.findOne({ _id: createChat._id }).populate(
      "users",
      "-password"
    );

    res.status(201).json(fullChat);
  }
};

const getChats = async (req, res) => {
  const chat = await Chat.find({ users: { $elemMatch: { $eq: req.user.id } } })
    .populate("users", "-password")
    .populate("groupAdmin", "-password")
    .populate("latestMessage")
    .sort({ updatedAt: -1 });

  const user = await User.populate(chat, {
    path: "latestMessage.sender",
    select: "name email _id",
  });

  res.status(201).json(user);
};

const createGroup = async (req, res) => {
  if (!req.body.users || !req.body.name) {
    return res.status(400).send({ message: "Please Fill all the fields" });
  }

  var users = JSON.parse(req.body.users);

  if (users.length < 2) {
    return res
      .status(400)
      .send("More than 2 users are required to form a group chat");
  }

  users.push(req.user.id);

  const groupChat = await Chat.create({
    chatName: req.body.name,
    users: users,
    isGroupChat: true,
    groupAdmin: req.user.id,
  });

  const fullGroupChat = await Chat.findOne({ _id: groupChat._id })
    .populate("users", "-password")
    .populate("groupAdmin", "-password");

  res.status(200).json(fullGroupChat);
};

const renameGroup = async (req, res) => {
  const { chatId, chatName } = req.body;

  const updateChat = await Chat.findByIdAndUpdate(
    chatId,
    {
      chatName: chatName,
    },
    {
      new: true,
    }
  )
    .populate("users", "-password")
    .populate("groupAdmin", "-password");

  if (!updateChat) {
    throw new BadRequestError("Chat Not Found");
  } else {
    res.json(updateChat);
  }
};

const addUserToGroup = async (req, res) => {
  const { chatId, userId } = req.body;

  const addUser = await Chat.findByIdAndUpdate(
    chatId,
    {
      $push: { users: userId },
    },
    {
      new: true,
    }
  )
    .populate("users", "-password")
    .populate("groupAdmin", "-password");

  if (!addUser) {
    throw new BadRequestError("Chat Not Found");
  } else {
    res.status(201).json(addUser);
  }
};

const removeFromGroup = async (req, res) => {
  const { chatId, userId } = req.body;

  const removeUser = await Chat.findByIdAndUpdate(
    chatId,
    {
      $pull: { users: userId },
    },
    {
      new: true,
    }
  )
    .populate("users", "-password")
    .populate("groupAdmin", "-password");

  if (!removeUser) {
    throw new BadRequestError("Chat Not Found");
  } else {
    res.status(201).json(removeUser);
  }
};

export {
  getChat,
  getChats,
  createGroup,
  removeFromGroup,
  renameGroup,
  addUserToGroup,
};

// Finally add the below code in controllers/message.js

import Message from "../models/message.js";
import User from "../models/user.js";
import Chat from "../models/chat.js";

const sendMessage = async (req, res) => {
  const { message, chatId } = req.body;

  if (!message || !chatId) {
    console.log("Invalid data passed into request");

    return res
      .status(400)
      .json({ error: "Please Provide All Fields To send Message" });
  }

  var newMessage = {
    sender: req.user._id,
    message: message,
    chat: chatId,
  };

  try {
    let m = await Message.create(newMessage);

    m = await m.populate("sender", "name");
    m = await m.populate("chat");
    m = await User.populate(m, {
      path: "chat.users",
      select: "name email _id",
    });

    await Chat.findByIdAndUpdate(chatId, { latestMessage: m }, { new: true });

    res.status(200).json(m);
  } catch (error) {
    res.status(400).json(error.message);
  }
};

const allMessages = async (req, res) => {
  try {
    const { chatId } = req.params;

    const getMessage = await Message.find({ chat: chatId })
      .populate("sender", "name email _id")
      .populate("chat");

    res.status(200).json(getMessage);
  } catch (error) {
    res.status(400).json(error.message);
  }
};

export { allMessages, sendMessage };
    
    
    

Creating Routes in backend:

Now let's create routes in backend for register, login, message and chat. We'll use express router for this. Create a folder named routes in root directory and create 3 files auth.js, chat.js and message.js inside routes.

Copied ✔
    
// routes/auth.js will have code something like this

import express from "express";
const router = express.Router();
import { register, login, searchUser } from "../controllers/auth.js";

import authenticateUser from "../middleware/auth.js";

router.route("/register").post(register);
router.route("/login").post(login);
router.route("/users").get(authenticateUser,searchUser);

export default router;


// routes/chat.js has the following routes

import express from "express";
const router = express.Router();

import {
  getChat,
  getChats,
  createGroup,
  renameGroup,
  removeFromGroup,
  addUserToGroup,
} from "../controllers/chat.js";

router.route("/").post(getChat).get(getChats);
router.route("/createGroup").post(createGroup);
router.route("/renameGroup").patch(renameGroup);
router.route("/removeFromGroup").patch(removeFromGroup);
router.route("/addUserToGroup").patch(addUserToGroup);

export default router;


// routes/message.js will have this code.

import express from "express";
import { allMessages, sendMessage } from "../controllers/message.js";
const router = express.Router();

router.route("/:chatId").get(allMessages);
router.route("/").post(sendMessage);

export default router;
    
    

Verifying JWT:

Since we are using jwt so will also have to verify the jwt of any user attempting to access the routes. Let’s create a folder named middleware in the parent directory of the project. Inside it, create a file named auth.js. Put the below code inside it.

Copied ✔
    
import jwt from "jsonwebtoken";
import User from "../models/user.js";

const auth = async (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith("Bearer")) {
    return res.status(401).send("Authentication Invalid 1");
  }
  let token = authHeader.split(" ")[1];
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.user = await User.findById(payload.id).select("-password");
    next();
  } catch (error) {
    return res.status(401).send("Authentication Invalid 2");
    // throw new Error("Authentication Invalid");
  }
};

export default auth;


// Now let’s import all the necessary files in index.js of backend.

import express from "express";
import dotenv from "dotenv";
const app = express();
import mongoose from "mongoose";
import { createServer } from "http";
import cors from "cors";
import { Server } from "socket.io";
const server = createServer(app);
import authRoute from "./routes/auth.js";
import chatRoute from "./routes/chat.js";
import messageRoute from "./routes/message.js";
import authenticateUser from "./middleware/auth.js";

dotenv.config();
const PORT = process.env.PORT || 4000;

app.use(cors());
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

mongoose
  .connect(process.env.MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => console.log("Database connected!"))
  .catch((err) => console.error(err));

app.get("/", (req, res) => {
  res.send("Hey!! This is a sign that the server is running");
});

app.use("/auth", authRoute);
app.use("/chat", authenticateUser, chatRoute);
app.use("/message", authenticateUser, messageRoute);


server.listen(PORT, () => console.log("Server is running on port", PORT));
    
    

Completing the frontend:

Let’s go back to frontend to create the necessary components.

ChatContainer.js is the main chat window where messages will be visible and it has the following code:

Copied ✔
    
import {React,useContext} from "react";
import SingleChat from "./SingleChat";
import { AuthContext } from "../context/context";


const ChatContainer = ({  fetchAgain, setFetchAgain }) => {
  const { selectedChat } = useContext(AuthContext);

  return (
    
); }; export default ChatContainer; // We forgot to create the javascript file to get some necessary details like sender’s name etc. create a folder named config inside src. Create a file chat.js inside it and put the following code: export const getSender = (loggedUser, users) => { return users[0]._id === loggedUser._id ? users[1].name : users[0].name; }; export const getSenderFull = (loggedUser, users) => { return users[0]._id === loggedUser._id ? users[1] : users[0]; }; export const isSameSenderMargin = (messages, m, i, userId) => { if ( i < messages.length - 1 && messages[i + 1].sender._id === m.sender._id && messages[i].sender._id !== userId ) return 33; else if ( (i < messages.length - 1 && messages[i + 1].sender._id !== m.sender._id && messages[i].sender._id !== userId) || (i === messages.length - 1 && messages[i].sender._id !== userId) ) return 0; else return "auto"; }; export const isSameSender = (messages, m, i, userId) => { return ( i < messages.length - 1 && (messages[i + 1].sender._id !== m.sender._id || messages[i + 1].sender._id === undefined) && messages[i].sender._id !== userId ); }; export const isLastMessage = (messages, i, userId) => { return ( i === messages.length - 1 && messages[messages.length - 1].sender._id !== userId && messages[messages.length - 1].sender._id ); }; export const isSameUser = (messages, m, i) => { return i > 0 && messages[i - 1].sender._id === m.sender._id; };

Displaying list of chats:

The MyChats.js file has the list of all chats of a user. Put the following code in it.

Copied ✔
    
import React, { useState, useEffect, useContext } from "react";
import { AuthContext } from "../context/context";
import axios from "axios";
import GroupChatModal from "./GroupChatModal";
import { getSender } from "../config/chat";
import { toast } from "react-toastify";

const MyChats = ({ fetchAgain }) => {
  const [loggedUser, setLoggedUser] = useState();
  const { user, chats, setChats, selectedChat, setSelectedChat } =
    useContext(AuthContext);

  const fetchChats = async () => {
    try {
      console.log("Fetch chats token " + user.token);
      const { data } = await axios.get(`${process.env.REACT_APP_URL}/chat`, {
        headers: {
          Authorization: `Bearer ${user.token}`,
        },
      });
      setChats(data);
    } catch (error) {
      toast.error(error);
    }
  };

  useEffect(() => {
    setLoggedUser(JSON.parse(localStorage.getItem("user")));
    fetchChats();
    // eslint-disable-next-line
  }, [fetchAgain]);

  return (
    

My Chats

{chats ? ( chats.map((chat) => (
setSelectedChat(chat)} style={{ cursor: "pointer", backgroundColor: selectedChat === chat ? "rgba(67, 43, 255, 0.8)" : "#E8E8E8", color: selectedChat === chat ? "white" : "black", paddingLeft: "2em", margin: "10px", paddingRight: "2em", paddingTop: "1em", paddingBottom: "1em", borderRadius: "1em", }} key={chat?._id} > {!chat?.isGroupChat ? getSender(loggedUser, chat?.users) : chat?.chatName}
)) ) : (
Loading Chats...
)}
); }; export default MyChats;

Message section:

Write the code given below in components/singlechat.js, which handles the functionalities of the chat window and includes the styling for various components like messages,input bar etc. Note that here we will be using socket.io and handling various aspects to ensure real time communication. Also, I have used 2 svg icons here. You can use your own preferred icons and adjust the code accordingly.

Copied ✔
        
import React, { useEffect, useState, useContext } from "react";
import axios from "axios";
import { toast } from "react-toastify";
import { getSender } from "../config/chat";
import ScrollableChat from "./ScrollableChat";
import { AuthContext } from "../context/context";
import sendIcon from "./send.svg";
import UpdateGroupChatModel from "./UpdateGroupChatModel";
import io from "socket.io-client";
import emojiIcon from "./smileyEmoji.svg";
import data from "@emoji-mart/data";
import Picker from "@emoji-mart/react";
import suprsend from "@suprsend/web-sdk";

let socket, selectedChatCompare;

const SingleChat = ({ fetchAgain, setFetchAgain }) => {
  const [messages, setMessages] = useState([]);
  const [loading, setLoading] = useState(false);
  const [newMessage, setNewMessage] = useState("");
  const [socketConnected, setSocketConnected] = useState(false);
  const [typing, setTyping] = useState(false);
  const [isTyping, setIsTyping] = useState(false);
  const [showEmojiBox, setShowEmojiBox] = useState(false);

  const { user, selectedChat, setSelectedChat } =
    useContext(AuthContext);
  const fetchMessages = async () => {
    if (!selectedChat) {
      console.log("no selected chat");
      return;
    }

    try {
      setLoading(true);
      const { data } = await axios.get(
        `${process.env.REACT_APP_URL}/message/${selectedChat._id}`,
        {
          headers: {
            Authorization: `Bearer ${user.token}`,
          },
        }
      );

      setMessages(data);
      setLoading(false);
      socket.emit("join-chat", selectedChat._id);
    } catch (error) {
      console.log(error);
      toast.error(error);
    }
  };

  const sendMessagekeyenter = async (e) => {
    if (e.key === "Enter") {
      sendMessage();
    }
  };
  const sendMessage = async () => {
    if (newMessage) {
      socket.emit("stop-typing", selectedChat._id);
      try {
        const { data } = await axios.post(
          `${process.env.REACT_APP_URL}/message`,
          {
            message: newMessage,
            chatId: selectedChat,
          },
          {
            headers: {
              "Content-type": "application/json",
              Authorization: `Bearer ${user.token}`,
            },
          }
        );

        setNewMessage("");
        setShowEmojiBox(false);
        socket.emit("new-message", data);
        setMessages([...messages, data]);
      } catch (error) {
        toast.error(error);
      }
    }
  };

  useEffect(() => {
    socket = io(`${process.env.REACT_APP_URL}`);
    socket.emit("setup", user);
    socket.on("connected", () => setSocketConnected(true));
    socket.on("typing", () => setIsTyping(true));
    socket.on("stop-typing", () => setIsTyping(false));
    // eslint-disable-next-line
  }, []);

  useEffect(() => {
    fetchMessages();
    selectedChatCompare = selectedChat;
    // eslint-disable-next-line
  }, [selectedChat]);

  useEffect(() => {
    socket.on("message-received", (newMessageReceived) => {
      if (
        !selectedChatCompare ||
        selectedChatCompare._id !== newMessageReceived.chat._id
      ) {
        // for implementing notifications: suprsend.track("NEW_MSG");
      } else {
        setMessages([...messages, newMessageReceived]);
      }
    });
  });

  const typingHandler = (e) => {
    setNewMessage(e.target.value);

    if (!socketConnected) return;

    if (!typing) {
      setTyping(true);
      socket.emit("typing", selectedChat._id);
    }
    let lastTypingTime = new Date().getTime();
    var timerLength = 3000;
    setTimeout(() => {
      var timeNow = new Date().getTime();
      var timeDiff = timeNow - lastTypingTime;
      if (timeDiff >= timerLength && typing) {
        socket.emit("stop-typing", selectedChat._id);
        setTyping(false);
      }
    }, timerLength);
  };
        
      

Copied ✔
        
  return (
    <>
      {selectedChat !== undefined ? (
        <>
          
{!selectedChat.isGroupChat ? (
{getSender(user, selectedChat.users)}
) : ( <> {selectedChat.chatName.toUpperCase()} )}
{loading ? (
Loading...
) : (
)}
{isTyping && selectedChat.isGroupChat ? (
{getSender(user, selectedChat.users)} is typing ...
) : isTyping ? (
Typing ...
) : ( <> )}
emojiIcon setShowEmojiBox(!showEmojiBox)} /> {showEmojiBox && (
setNewMessage(newMessage.concat(emoji.native)) } />
)} send
) : (

Click On A User to Start Conversation

)} ); }; export default SingleChat;

Handling the search functionality:

Put the following code inside sidebar.js. This component enables a user to search for any other user and add people for messaging.

Copied ✔
        
import React, { useState, useContext } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import { toast } from "react-toastify";
import { AuthContext } from "../context/context";


const SideBar = () => {
  const [search, setSearch] = useState("");
  const [searchResult, setSearchResult] = useState([]);
  const [loading, setLoading] = useState(false);
  const [loadingChat, setLoadingChat] = useState(false);
  const [open, setOpen] = useState(false);
  const navigate = useNavigate();

  const {
    setSelectedChat,
    user,
    chats,
    setChats,
  } = useContext(AuthContext);

  const logoutHandler = () => {
    localStorage.removeItem("user");
    navigate("/register");
  };

  const handleSearch = async () => {
    if (!search) {
      toast.error("Please Provide username");
      return;
    }

    try {
      setLoading(true);

      const { data } = await axios.get(
        `${process.env.REACT_APP_URL}/auth/users?search=${search}`,
        {
          headers: {
            Authorization: `Bearer ${user.token}`,
          },
        }
      );

      setLoading(false);
      setSearchResult(data);
    } catch (error) {
      toast.error(error);
    }
  };

  const accessChat = async (userId) => {
    try {
      setLoadingChat(true);

      const { data } = await axios.post(
        `${process.env.REACT_APP_URL}/chat`,
        {
          userId,
        },
        {
          headers: {
            Authorization: `Bearer ${user.token}`,
          },
        }
      );

      if (!chats.find((c) => c._id === data._id)) setChats([data, ...chats]);
      setSelectedChat(data);
      setLoadingChat(false);
      setOpen(false);
    } catch (error) {
      toast.error(error);
    }
  };
  return (
    <>
      

Chat app

{open && (

Search Users

{/*
*/} setSearch(e.target.value)} /> {/*
*/} {loading ? (

Loading...

) : ( searchResult?.map((user) => (
accessChat(user._id)} > {user.name}
)) )} {loadingChat &&

Loading Chat...

}
)} ); }; export default SideBar;


The component scrollable chat is the part of chat window where the messages are present. Put the below code in components/scrollablechat.js

Copied ✔
        
import image from "../images/user.svg";
import React, { useRef, useEffect, useContext } from "react";
import {
  isLastMessage,
  isSameSender,
  isSameSenderMargin,
  isSameUser,
} from "../config/chat";
import { AuthContext } from "../context/context";

const ScrollableChat = ({ messages }) => {
  const { user } = useContext(AuthContext);

  const messagesEndRef = useRef(null);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  return (
    
{messages && messages.map((m, i) => (
{(isSameSender(messages, m, i, user._id) || isLastMessage(messages, i, user._id)) && ( {m.sender.name} )}
{m.message}
))}
); }; export default ScrollableChat;

Creating a Group:

Now let’s create the components for group chat.The Groupchatmodal component is used to create a group. Inside components/groupchatmodel.js, put the following code:

Copied ✔
        
import React, { useState, useContext } from "react";
import axios from "axios";
import { AuthContext } from "../context/context";
import UserBadgeItem from "./UserBadgeItem";
import UserListItem from "./UserListItem";
import { toast } from "react-toastify";

const GroupChatModal = ({ children }) => {
  const [isOpen, setIsOpen] = useState(false);
  const [groupChatName, setGroupChatName] = useState("");
  const [selectedUsers, setSelectedUsers] = useState([]);
  const [search, setSearch] = useState("");
  const [searchResult, setSearchResult] = useState([]);
  const [loading, setLoading] = useState(false);

  const { chats, setChats, user } = useContext(AuthContext);

  const handleSearch = async (query) => {
    setSearch(query);
    if (!query) {
      return;
    }

    try {
      setLoading(true);
      const { data } = await axios.get(`${process.env.REACT_APP_URL}/auth/users?search=${search}`,{
        headers: {
          Authorization: `Bearer ${user.token}`,
        },
      });

      setLoading(false);
      setSearchResult(data);
    } catch (error) {
      toast.error(error);
    }
  };

  const handleDelete = (delUser) => {
    setSelectedUsers(selectedUsers.filter((sel) => sel._id !== delUser._id));
  };

  const handleGroup = (userToAdd) => {
    if (selectedUsers.includes(userToAdd)) {
      toast.error("User Already Added!");
      return;
    }

    setSelectedUsers([...selectedUsers, userToAdd]);
  };

  const handleSubmit = async () => {
    if (!groupChatName || !selectedUsers) {
      toast.error("Please Fill Up All The Fields");
      return;
    }

    try {
      const { data } = await axios.post(`${process.env.REACT_APP_URL}/chat/createGroup`, {
        name: groupChatName,
        users: JSON.stringify(selectedUsers.map((u) => u._id)),
      },{
        headers: {
          "Content-type": "application/json",
          Authorization: `Bearer ${user.token}`,
        },
      });
      setChats([data, ...chats]);
      setIsOpen(false);
      toast.success("Successfully Created New Group");
    } catch (error) {
      toast.error("Failed To Create Group.");
    }
  };

  return (
    <>
       setIsOpen(true)}>{children}

      {isOpen && (
        

Create Group Chat

setGroupChatName(e.target.value)} /> handleSearch(e.target.value)} />
{selectedUsers.map((u) => ( handleDelete(u)} /> ))}
{loading ? (
Loading...
) : ( searchResult ?.slice(0, 4) .map((user) => ( handleGroup(user)} /> )) )}
)} ); }; export default GroupChatModal;


After that you'll need to update the group details.

Finishing the Backend:

Congrats. We have completed the frontend for our chat app. Let’s complete the backend code also. We’ll firstly import Server from socket.io, then add it to cors, and finally establish the connection with frontend. Then we’ll handle all the aspects of chatting using socket.io.  Open index.js and add the code for socket.io and various events related to it. 

Copied ✔
        
import express from "express";
import dotenv from "dotenv";
const app = express();
import mongoose from "mongoose";
import { createServer } from "http";
import cors from "cors";
import { Server } from "socket.io";
const server = createServer(app);
import authRoute from "./routes/auth.js";
import chatRoute from "./routes/chat.js";
import messageRoute from "./routes/message.js";
import authenticateUser from "./middleware/auth.js";

dotenv.config();
const PORT = process.env.PORT || 4000;

app.use(cors());
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

mongoose
  .connect(process.env.MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => console.log("Database connected!"))
  .catch((err) => console.error(err));

app.get("/", (req, res) => {
  res.send("Hey!! This is a sign that the server is running");
});

app.use("/auth", authRoute);
app.use("/chat", authenticateUser, chatRoute);

app.use("/message", authenticateUser, messageRoute);

const io = new Server(server, {
  pingTimeout: 60000,
  cors: {
    origin: "http://localhost:3000",
  },
});

io.on("connection", (socket) => {
  console.log("Socket connected "+ socket.id)
  socket.on("setup", (userData) => {
    socket.join(userData._id);
    console.log(userData._id + " connected")
    socket.emit("connected");
  });
  

  socket.on("join-chat", (room) => {
    console.log(room+" joined")
    socket.join(room);
  });

  socket.on("typing", (room) => socket.in(room).emit("typing"));
  socket.on("stop-typing", (room) => socket.in(room).emit("stop-typing"));

  socket.on("new-message", (newMessageReceived) => {
    let chat = newMessageReceived.chat;

    if (!chat.users) return console.log(`chat.users not defined`);

    chat.users.forEach((user) => {
      if (user._id === newMessageReceived.sender._id) return;
console.log("Hey got a message " + newMessageReceived)
      socket.in(user._id).emit("message-received", newMessageReceived);
    });
  });

  socket.off("setup", () => {
    console.log("Socket disconnected")
    socket.leave(userData._id);
  });
});

server.listen(PORT, () => console.log("Server is running on port", PORT));

        
      


So, finally our app looks something like this:

After logging in:


Sidebar
:

After sending and receiving messages:

Chatting in group:

Updating group:

After Admin renamed and added more people to group:

Implementing Suprsend Notifications:

By leveraging SuprSend's powerful push notification capabilities, your chat app can deliver instant alerts and updates directly to users' devices, even when they are not actively using the app. This ensures that your users stay connected and informed, fostering better communication and user engagement.

The benefits of using SuprSend for push notifications in chat apps are numerous. Here are a few key advantages:

  1. Real-time Communication: SuprSend enables instant delivery of push notifications, allowing users to stay connected and engaged with their chat conversations in real-time.
  2. Increased User Engagement: By leveraging push notifications, you can keep users informed about new messages, mentions, or important updates, encouraging them to actively participate in conversations.
  3. Personalized Experiences: SuprSend offers the ability to segment users and send personalized notifications, ensuring that users receive relevant and tailored messages based on their preferences and behaviors.
  4. Retention and Re-engagement: Push notifications serve as a powerful tool to re-engage users and bring them back to your chat app. You can remind users about ongoing conversations, invite them to join groups, or notify them about new features, fostering app retention.
  5. Seamless Integration: SuprSend provides easy-to-use APIs and SDKs that seamlessly integrate with popular chat app frameworks, making it effortless to incorporate push notifications into your existing infrastructure.

For this chat app, we can implement web push notifications. For this, we can use the Suprsend Client javascript SDK.

NOTE: Make sure your website uses https protocol.

Creating a template:

Create a template for web push notifications on suprsend platform.

Creating a workflow:

After the template, create a workflow on the suprsend platform.

The Event Name will be needed for sending notifications in the code.

Suprsend SDK Installation:

Install Suprsend Javascript SDK in the client.

npm i @suprsend/web-sdk

Service worker:

Add service worker file in your client. Create a file named serviceworker.js inside public folder(it has to be publicly accessible) and put the following content inside it:

importScripts("https://cdn.jsdelivr.net/npm/@suprsend/web-sdk@0.1.30/serviceworker/serviceworker.min.js");init_workspace(WORKSPACE_KEY);‍

Initializing the SDK:

Initialize the Suprsend SDK. Make sure to initialize it before calling other methods of it. Since we are using create-react-app, initialize it on top of App class in App.js. For initializing SDK, you need WORKSPACE KEY and WORKSPACE SECRET and VAPID KEY. SuprSend creates a Vapid key when your account is created. You can find this key on the Vendors page on SuprSend dashboard. Go to Vendors > Web Push > VAPID Keys page, copy the public VAPID key and paste it here in vapid key object value


Replace WORKSPACE_KEY and WORKSPACE_SECRET with your workspace values. You will get both the tokens from the Suprsend dashboard (Settings page -> "API keys" section).

import suprsend from "@suprsend/web-sdk"; suprsend.init(WORKSPACE KEY, WORKSPACE SECRET, {vapid_key:})

Registering for Web Push notifications: 

Register for webpush notifications. Suprsend SDK provides method to register service worker which enables your website to prompt notification permission to your users, if not enabled already.

suprsend.web_push.register_push();

Check for permission:

You can check if the user has granted permission to show push notifications using the below method

suprsend.web_push.notification_permission();


This will return a string representing the current permission. The value can be:

  • granted: The user has granted permission for the current origin to display notifications.
  • denied: The user has denied permission for the current origin to display notifications.
  • default: The user's decision is unknown. This will be permission when user first lands on website.

Create/Identify a new user

You can identify a user using suprsend.identify() method.

Call this method as soon as you know the identity of user, that is after login authentication.

suprsend.identify(unique_id);

Sending Events to SuprSend: 

Once the workflow is configured, you can pass the Event Name (NEW_MSG in our case) defined in workflow configuration from the SDK and the related workflow will be triggered. Variables added in the template should be passed as event properties

You can send Events from your app to SuprSend platform by using suprsend.track() method

suprsend.track(event_name, property_obj);


Here property_obj is object (optional). Additional data related to the event (event properties) can be tracked using this field.

After implementing notifications, you will get a notification like this:

Github Repo: SuprSend-NotificationAPI/Chatting-App (github.com)
Deployed Link: Chirpy Chat (mychttingapp.netlify.app)

Wrapping Up

Throughout the tutorial, you have learned how to set up a React project, implement user authentication, create chat components, handle messages and notifications, and integrate SuprSend for seamless push notification delivery. These skills and concepts can be extended and customized to fit your specific chat app requirements and preferences.

Remember, push notifications play a crucial role in enhancing user engagement, facilitating real-time communication, and providing a personalized chat experience. By harnessing the capabilities of SuprSend, you can take your chat app to the next level and ensure that your users stay connected and informed at all times.

Now it's time to apply what you have learned, unleash your creativity, and build amazing chat applications that deliver exceptional user experiences. Keep exploring the vast possibilities of React, SuprSend, and other tools available to you, and never hesitate to dive deeper into the world of chat app development.

Happy coding, and may your chat app flourish with seamless push notifications powered by SuprSend!

Written by:
Anjali Arya
Product & Analytics, SuprSend
Get a powerful notification engine with SuprSend
Build smart notifications across channels in minutes with a single API and frontend components
Implement a powerful stack for your notifications
By clicking “Accept All Cookies”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.