February 20, 2021

Building a Whatsapp Web clone with React , Express and Firebase - Part 4

In this last part , we are going to work on our backend . On part 3 , we ended up firing localhost:3001/user so as we can register our user . Now let’s set up our backend .

Inside your project directory , crate a server directory, go inside it and init the node project as below:-

npm init -y

-y will allow us to accept default options . From now we should have package.json file created .

From here let’s add express

npm i express

bacause we want to use es6 syntax , we will add the key type with value module in our package.js file

"type": "module"

Let’s create a basic express server by creating index.js file and add below content:-

import express from "express";

const app = express();
const PORT = 3001;

app.post("/user", (req, res) => {
  res.send("Create User");
});

app.listen(PORT, "localhost", () => {
  console.log(`Server started at localhost://${PORT}`);
});

Go ahead and start the application by running node .

Or if you have nodemon installed , add a start script in package.js file under scripts as

 "start":"nodemon"

Now if you go and type npm start

Nodemon will start the server and handle refresh whenever our code changes .

Npw if we inspect our create user request , we should get “create User” response .

if you console.log req.body , you should see undefined , and this is because we need to parse our request using a middleware . Go ahead and add below code above your user route

app.use(express.json());

Now , when we console.log the req.body , we should be able to see the json payload .

Next step is to store our payload to the DB , to do so we will use MongoDB and the mongoose ORM to help us with database operation .

Go on and install mongoose

npm i mongoose

Now , let’s create a User model , create a file called User.js and add below code :-

import mongoose from "mongoose";

const userSchema = mongoose.Schema({
  uid: {
    type: String,
    required: true,
  },
  mobile: { type: String, required: true },
  username: { type: String, required: false, default: "" },
});

export default mongoose.model("user", userSchema);

from the above code , we have a model with three field , uid , mobile and username . The username by default is "" as later on our frontend app you may choose to say your contact with their actual names :

Let’s import our user model to our index.js file and finalize creating user route as below:-

app.post("/user", async (req, res) => {
  // Check if user exists
  const user = await User.findOne({ uid: req.body.uid });

  if (!user) {
    User.create(req.body, (error, data) => {
      if (error) {
        return res.status(500).send(error);
      } else {
        return res.status(201).send(data);
      }
    });
  } else {
    return res.status(200).send(user);
  }
});

From the above , when you login with your mobile , we will check if the user already exists, and if not we will create the user else we will just return existing user .

Before we run our endpoint again , we need to establish connection to our MongoDB. Go ahead to mongo atlas create your DB and copy your connection string . Just below express() function , let’s add our database connection as below :-

//connect to DB
const connection_url =
  "mongodb+srv://admin:YOUR_ATLAST_CONNECTION_STRING_.mongodb.net/whatsappdb?retryWrites=true&w=majority";

mongoose.connect(connection_url, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

Now , when we revisit our client app and reload the page , if everything goes well , you should see a new user document added to our mongo users collection .

At this point , our index.js file should be as below :-

import express from "express";
import mongoose from "mongoose";
import User from "./User.js";
const app = express();
const PORT = 3001;

//connect to DB
const connection_url =
  "mongodb+srv://admin:YOUR_ATLAST_CONNECTION_STRING_.mongodb.net/whatsappdb?retryWrites=true&w=majority";

mongoose.connect(connection_url, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

//Middleware
app.use(express.json());

app.post("/user", async (req, res) => {
  // Check if user exists
  const user = await User.findOne({ uid: req.body.uid });

  if (!user) {
    User.create(req.body, (error, data) => {
      if (error) {
        return res.status(500).send(error);
      } else {
        return res.status(201).send(data);
      }
    });
  } else {
    return res.status(200).send(user);
  }
});

app.listen(PORT, "localhost", () => {
  console.log(`Server started at localhost://${PORT}`);
});

Now our users are created , let’s retrieve them by adding a get route as below:-

app.get("/users", (req, res) => {
  User.find((error, data) => {
    if (error) {
      return res.status(500).send(error);
    } else {
      return res.status(200).send(data);
    }
  });
});

Now , let’s go to our client app under chat contacts and call the endpoint to get all users . Go to Sidebar Component then create a useEffect function that will retrieve our users as below :-

useEffect(() => {
    axios.get("http://localhost:3001/users").then(async (res) => {
      const users = await res.data;
      dispatch({ type: "GET_USERS", payload: users });
    });
  }, []);

Now , lets get our users fro store as well as creating a contacts state that will store our users when they are retrieved .

  const [contacts, setContacts] = useState([]);
  const { loggedInUser, users } = state;

From there , let’s create another useEffect to listen to users change in our store , check if users.length is greater than 0 then remove logged in user from users array so as we can create our contacts as below :-

 useEffect(() => {
  if (users.length > 0) {
    console.log("contacts retrieved  = ", users);
    let retrievedUsers = users.filter((u) => u.uid !== loggedInUser.uid);
    setContacts(retrievedUsers);
  }
}, [users]);

Now when you refresh your page you should see the console log with your list of users .

Since we already have our contacts , let’s modify our contact component to use the actual contact as below:-

import React from "react";
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
import "./Contact.css";

const Contact = ({ contact }) => {
  return (
    <div className="contact">
      <div className="contact__avatar">
        <AccountCircleIcon />
      </div>
      <div className="contact__text">
        <h2>{contact.username === "" ? contact.mobile : contact.username}</h2>
        <p>Last message here</p>
      </div>
    </div>
  );
};

export default Contact;

Now we should see our contact list .

Next thing to do , is when a user clicks on one of the contact , to take to chat screen . TO achieve that , we will create onClick method called enterChat that will take selected Contact object , Once we get the chat we will call the API to give us conversation between the two contacts as shown below :-

import React, { useContext } from "react";
import axios from "axios";
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
import { store } from "../../store";
import "./Contact.css";

const Contact = ({ contact }) => {
  const { dispatch, state } = useContext(store);
  const { loggedInUser } = state;

  const enterChat = () => {
    dispatch({ type: "ENTER_CHAT", payload: contact });
  };

  return (
    <div className="contact" onClick={enterChat}>
      <div className="contact__avatar">
        <AccountCircleIcon />
      </div>
      <div className="contact__text">
        <h2>{contact.username === "" ? contact.mobile : contact.username}</h2>
        <p>Last message here</p>
      </div>
    </div>
  );
};

export default Contact;

Let’s dig in the enterChat Function . When we click enter chat , first we will call “ENTER_CHAT” action that will make change to our store by setting selectedChatUser to the contact clicked as well as isChatSelected setting to true.

 case "ENTER_CHAT":
      return {
        ...state,
        isChatSelected: true,
        selectedChatUser: action.payload,
      }

Now , modify our HomePage to below :-

import React, { useContext } from "react";
import Chat from "../../components/Chat/Chat";
import NoChatSelected from "../../components/NoChatSelected/NoChatSelected";
import Sidebar from "../../components/Sidebar/Sidebar";
import { store } from "../../store";

const HomePage = () => {
  const { state } = useContext(store);
  const { isChatSelected } = state;
  return (
    <div style={{ display: "flex" }}>
      <Sidebar />
      {isChatSelected ? <Chat /> : <NoChatSelected />}
    </div>
  );
};

export default HomePage;

Now , we will use isChatSelected from our store to display our chat screen when a contact is selected . go ahead and test .

Now you should see a chat screen , with our hard coded chat we add sometime back in this tutorial .

From our chat , let’s change sender’s mobile with an actual sender’s mobile number . By now , our Chat component should be as below : -

import React, { useContext } from "react";
import SentimentSatisfiedIcon from "@material-ui/icons/SentimentSatisfied";
import { IconButton } from "@material-ui/core";
import AttachFileIcon from "@material-ui/icons/AttachFile";
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
import KeyboardVoiceIcon from "@material-ui/icons/KeyboardVoice";
import SearchIcon from "@material-ui/icons/Search";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import { store } from "../../store";
import "./Chat.css";

const Chat = () => {
  const { state } = useContext(store);
  const { selectedChatUser } = state;
  return (
    <div className="chat">
      <div className="chat__header">
        <div className="chat__headerLeft">
          <AccountCircleIcon />

          <h3>
            {selectedChatUser.username === ""
              ? selectedChatUser.mobile
              : selectedChatUser.username}
          </h3>
        </div>
        <div className="chat__headerRight">
          <IconButton>
            <SearchIcon />
          </IconButton>
          <IconButton>
            <MoreVertIcon />
          </IconButton>
        </div>
      </div>
      <div className="chat__body">
        <div className="chat__message chat__messageIn">
          <div className="chat__messageCotainer">
            <span className="chat__messageContent">
              Hello , how are you doing?
            </span>
            <span class="chat__messageTime">18:22</span>
          </div>
        </div>

        <div className="chat__message chat__messageOut ">
          <div className="chat__messageCotainer">
            <span className="chat__messageContent">I am doing great :-)</span>
            <span class="chat__messageTime">18:22</span>
          </div>
        </div>
      </div>

      <div className="chat_footer">
        <SentimentSatisfiedIcon />
        <AttachFileIcon />
        <input type="text" placeholder="Type a message" />

        <KeyboardVoiceIcon />
      </div>
    </div>
  );
};

export default Chat;

Now , when you change any contact , you should see phone number is changing as well .

Now , let’s add our first message . go to our backend and create a Message model as below : -

import mongoose from "mongoose";

const MessageSchema = mongoose.Schema({
  from: {
    type: String,
    required: true,
  },
  to: {
    type: String,
    required: true,
  },
  message: {
    type: String,
    required: true,
  },
  isReceived: {
    type: Boolean,
    default: false,
  },
  createdAt: {
    type: Date,
    default: Date.now,
  },
});

export default mongoose.model("message", MessageSchema);

From that model , go on import the model inside index.js file and create a message post route to enable us to add messages :-

app.post("/message", (req, res) => {
  Message.create(req.body, (error, data) => {
    if (error) {
      return res.status(500).send(error);
    } else {
      return res.status(201).send(data);
    }
  });
});

Now , we can start chat . Let’s change the chat component to below and i will explain what changed :-

import React, { useContext, useState, useRef } from "react";
import axios from "axios";
import SentimentSatisfiedIcon from "@material-ui/icons/SentimentSatisfied";
import { IconButton } from "@material-ui/core";
import AttachFileIcon from "@material-ui/icons/AttachFile";
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
import KeyboardVoiceIcon from "@material-ui/icons/KeyboardVoice";
import SearchIcon from "@material-ui/icons/Search";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import SendIcon from "@material-ui/icons/Send";
import { store } from "../../store";
import "./Chat.css";

const Chat = () => {
  const [canShowSendIcon, setCanShowSendIcon] = useState(false);
  const messageInputRef = useRef(null);
  const { state } = useContext(store);
  const { loggedInUser, selectedChatUser } = state;

  const sendMessage = async (e) => {
    e.preventDefault();
    let message = messageInputRef.current.value;
    let data = {
      from: loggedInUser.uid,
      to: selectedChatUser.uid,
      message,
    };
    await axios.post("http://localhost:3001/message", data).then((res) => {
      if (res.status === 201) {
        setCanShowSendIcon(false);
        messageInputRef.current.value = "";
      }
    });
  };

  return (
    <div className="chat">
      <div className="chat__header">
        <div className="chat__headerLeft">
          <AccountCircleIcon />

          <h3>
            {selectedChatUser.username === ""
              ? selectedChatUser.mobile
              : selectedChatUser.username}
          </h3>
        </div>
        <div className="chat__headerRight">
          <IconButton>
            <SearchIcon />
          </IconButton>
          <IconButton>
            <MoreVertIcon />
          </IconButton>
        </div>
      </div>
      <div className="chat__body">
        <div className="chat__message chat__messageIn">
          <div className="chat__messageCotainer">
            <span className="chat__messageContent">
              Hello , how are you doing?
            </span>
            <span class="chat__messageTime">18:22</span>
          </div>
        </div>

        <div className="chat__message chat__messageOut ">
          <div className="chat__messageCotainer">
            <span className="chat__messageContent">I am doing great :-)</span>
            <span class="chat__messageTime">18:22</span>
          </div>
        </div>
      </div>

      <div className="chat_footer">
        <SentimentSatisfiedIcon />
        <AttachFileIcon />
        <input
          type="text"
          placeholder="Type a message"
          ref={messageInputRef}
          onFocus={() => setCanShowSendIcon(true)}
        />
        {!canShowSendIcon ? (
          <KeyboardVoiceIcon />
        ) : (
          <IconButton onClick={sendMessage}>
            <SendIcon />
          </IconButton>
        )}
      </div>
    </div>
  );
};

export default Chat;

So fro above , starting from our send message input , we have use ref to capture message that is typed , when a user clicks on the input , we will show a send icon and allow hom to type the message and once clicked we will call a sendMessage function .

Inside sendMessage function , we will get the message typed and call create message API to send our message to the backend .

Now , if everything went well and you refresh your Database , under messages collection you should be able to see our message .

Now , how do we actually retrieve the conversations per specific user . To do that , we will have to make a change to enterChat function by calling http://localhost:30001/chat . change enterCHat function as below : -

import React, { useContext } from "react";
import axios from "axios";
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
import { store } from "../../store";
import "./Contact.css";

const Contact = ({ contact }) => {
  const { dispatch, state } = useContext(store);
  const { loggedInUser } = state;

  const enterChat = () => {
    dispatch({ type: "ENTER_CHAT", payload: contact });
    axios
      .get("http://localhost:3001/chat", {
        params: {
          from: `${loggedInUser.uid};${contact.uid}`,
          to: `${loggedInUser.uid};${contact.uid}`,
        },
      })
      .then(async (res) => {
        const messages = await res.data;
        dispatch({ type: "GET_MESSAGES", payload: messages });
      });
  };

  return (
    <div className="contact" onClick={enterChat}>
      <div className="contact__avatar">
        <AccountCircleIcon />
      </div>
      <div className="contact__text">
        <h2>{contact.username === "" ? contact.mobile : contact.username}</h2>
        <p>Last message here</p>
      </div>
    </div>
  );
};

export default Contact;

Back to our enterChat method , when you click any contact , you should be able to see a call to http://localhost:30001/chat that has 404 , and that’s fine . Let’s switch to our APIs and create that route .

app.get("/chat", (req, res) => {
  let fromArr = req.query.from.split(";");
  let toArr = req.query.to.split(";");
  Message.find(
    { from: { $in: fromArr }, to: { $in: toArr } },
    (error, data) => {
      if (error) {
        return res.status(500).send(error);
      } else {
        return res.status(200).send(data);
      }
    }
  );
});

From the above , based on our model , when user A send message to user B , we will store from field as user A uid and to field as user B uid . Now , to get the conversation between the two users , i need to get all message where to has user A and B uids as well as from field being the same . That is what our route is doing above .

Now when we call our localhost:3001/chat , we should be able to get an array of our chats :-

Let’s go ahead and replace our dummy text with actual chat messages . Inside Chat component replace the dummy messages with below:-

   <div className="chat__body">
        {messages &&
          messages.map((message, index) => (
            <div
              className={`chat__message ${
                message.from === loggedInUser.uid
                  ? "chat__messageOut"
                  : "chat__messageIn"
              }`}
              key={message._id}
            >
              <div className="chat__messageCotainer">
                <span className="chat__messageContent">{message.message}</span>
                <span class="chat__messageTime">18:22</span>
              </div>
            </div>
          ))}
      </div>

From the above , we check if message is from logged in user and apply correct class so as we can render the conversation propery . Now , when you click on any contact you should be able to see the messages .

One more thing left , as now when you add your message , you have to refresh in order for the message to refresh . To solve this issue , we will user Pusher to have a realtime chat experience .

GO ahead signup to pusher.com . Go to channel then create an app . choose backend as React and Frontend as Node.js then click create App .

From there , Pusher should give you a screen on how to integrate with both Frontend and Backend environment . Let’s start with backend , go ahead to your server directory and install pusher

npm install pusher

Below database initialization in our index.js file , let’s add pusher config as below :-

const Pusher = require("pusher");

const pusher = new Pusher({
  appId: "1161XXX",
  key: "d7ea768a3e2c6fd2aXXX",
  secret: "87ab995c8bd0XXXXX",
  cluster: "ap2",
  useTLS: true
});

From there , we will create a db connection and using pusher listen for a change specifically insert operation on messages collection . Once we detect that operation , we will trigger a pusher call to update.

db.once("open", () => {
  console.log("DB is connected");
  const messageCollection = db.collection("messages");
  const changeStream = messageCollection.watch();

  changeStream.on("change", (change) => {
    console.log("changeStream Triggered");
    if (change.operationType === "insert") {
      const messageDetails = change.fullDocument;
      pusher.trigger("messages", "inserted", {
        from: messageDetails.from,
        to: messageDetails.to,
        message: messageDetails.message,
        createdAt: messageDetails.createdAt,
        isReceived: messageDetails.isReceived,
        _id: messageDetails._id,
      });
    } else {
      console.log("Error triggering pusher");
    }
    console.log(change);
  });
});

Now back to our client app , we need to install pusher client .

npm i pusher-js

Now inside our Chat component in useEffect , add below : -

  useEffect(() => {
    const pusher = new Pusher("PUSHER_KEY", {
      cluster: "mt1",
    });

    var channel = pusher.subscribe("messages");
    channel.bind("inserted", (newMessage) => {
      console.log("Message Added");
      dispatch({ type: ADD_MESSAGE, payload: newMessage });
    });

    return () => {
      channel.unbind_all();
      channel.unsubscribe();
    };
  }, []);

Our final CHat.js component should be as below : -

import React, { useContext, useState, useRef, useEffect } from "react";
import axios from "axios";
import Pusher from "pusher-js";
import SentimentSatisfiedIcon from "@material-ui/icons/SentimentSatisfied";
import { IconButton } from "@material-ui/core";
import AttachFileIcon from "@material-ui/icons/AttachFile";
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
import KeyboardVoiceIcon from "@material-ui/icons/KeyboardVoice";
import SearchIcon from "@material-ui/icons/Search";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import SendIcon from "@material-ui/icons/Send";
import { store } from "../../store";
import "./Chat.css";

const Chat = () => {
  const [canShowSendIcon, setCanShowSendIcon] = useState(false);
  const messageInputRef = useRef(null);
  const { state, dispatch } = useContext(store);
  const { loggedInUser, selectedChatUser, messages } = state;

  useEffect(() => {
    var pusher = new Pusher("d7ea768a3e2c6fd2abf3", {
      cluster: "ap2",
    });

    var channel = pusher.subscribe("messages");
    channel.bind("inserted", (newMessage) => {
      console.log("Message Added");
      dispatch({ type: "ADD_MESSAGE", payload: newMessage });
    });

    return () => {
      channel.unbind_all();
      channel.unsubscribe();
    };
  }, []);

  const sendMessage = async (e) => {
    e.preventDefault();
    let message = messageInputRef.current.value;
    let data = {
      from: loggedInUser.uid,
      to: selectedChatUser.uid,
      message,
    };
    await axios.post("http://localhost:3001/message", data).then((res) => {
      if (res.status === 201) {
        setCanShowSendIcon(false);
        messageInputRef.current.value = "";
      }
    });
  };

  return (
    <div className="chat">
      <div className="chat__header">
        <div className="chat__headerLeft">
          <AccountCircleIcon />

          <h3>
            {selectedChatUser.username === ""
              ? selectedChatUser.mobile
              : selectedChatUser.username}
          </h3>
        </div>
        <div className="chat__headerRight">
          <IconButton>
            <SearchIcon />
          </IconButton>
          <IconButton>
            <MoreVertIcon />
          </IconButton>
        </div>
      </div>
      <div className="chat__body">
        {messages &&
          messages.map((message, index) => (
            <div
              className={`chat__message ${
                message.from === loggedInUser.uid
                  ? "chat__messageOut"
                  : "chat__messageIn"
              }`}
              key={message._id}
            >
              <div className="chat__messageCotainer">
                <span className="chat__messageContent">{message.message}</span>
                <span class="chat__messageTime">18:22</span>
              </div>
            </div>
          ))}
      </div>

      <div className="chat_footer">
        <SentimentSatisfiedIcon />
        <AttachFileIcon />
        <input
          type="text"
          placeholder="Type a message"
          ref={messageInputRef}
          onFocus={() => setCanShowSendIcon(true)}
        />
        {!canShowSendIcon ? (
          <KeyboardVoiceIcon />
        ) : (
          <IconButton onClick={sendMessage}>
            <SendIcon />
          </IconButton>
        )}
      </div>
    </div>
  );
};

export default Chat;

Now if everything went well , you should be able to see your message in realtime :-)

I would like to continue this series , but it’s best to wrap it from here with a hope you can continue build amazing app based on what you have learn from this entire series .

In case of any things , don’t hesisate to reach our via Get in touch form or comment section and see you in soon , happy coding :-)

Buy Me A Coffee
````

Robert Rutenge
Front-end Developer | ReactJS enthusiast | Problem Solver . Find me on twitter and Github