DEV Community

Cover image for Whasapp Web Clone with Google authentication and firebase
angela300
angela300

Posted on

Whasapp Web Clone with Google authentication and firebase

This app is build with React JS for Beginners.

Functionalities implemented:

  • Send a message

  • Receive a message

  • Create a new room

  • Login with Google

  • Switch between different rooms

Tech Stack Implemented

  • ReactJS

  • Firebase Firestore Realtime db

  • Materials UI

  • React Router

  • React Context API

  • Google Authentication

  • Deploy using Firebase

Inside your desktop on your pc, run the command to create a react app:

npx create-react-app whatsappwebclone

Set up

-Firebase set up

Visit firebase.com

Go to console

Click on ‘Add new project’

Type the project name as ‘Whats app Clone’

Click on ‘Continue’, the click on ‘Continue’ again

Select default account for Firebase

Click on, ‘Create project’

On the project overview on a screen that looks like show below:

Click on ‘Project settings’ under the settings icon

Sroll to the bottom of the page and click on this icon: </>

As shown below, Add an App nickname, click on ‘Also set up Firebase hosting for this app’, and then click on ‘Register App’.

Click on ‘next’

Run this code on your vs code terminal on the app’s root folder:

npm install -g firebase-tools

Click on ‘next’

Click on ‘continue to console’

At the bottom of the page, click on the switch, ‘Config’:

Copy that code

In your App’s src folder, create a new file ‘firebase.js’, and paste the code on the file.

-Other App set up

To start the app, on your terminal run:

npm start

Delete the following files from your App’s folder files:

Logo.svg

App.test.js

setUpTests.js

Remove the code in App.js and replace it with:

    <div className="App">
      <h1>Let's build a whatsapp Clone</h1>
    </div>
Enter fullscreen mode Exit fullscreen mode

Delete everything in App.css

In index.css, add this code,

* {
  margin: 0;
}
Enter fullscreen mode Exit fullscreen mode

To name components in our app, we will be using the ‘BEM naming convention’, which is a very easy way of naming components in react.

In App.js, we will have a div of className, app_body, and has the components, sidebar and chat

function App() {
  return (
    <div className="App">
      <h1>Let's build a whatsapp Clone</h1>
      <div className="app_body">
        {/* Sidebar */}
        {/* Chat */}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In App.css, add this code to style the app:

.App {
  display: grid;
  place-items: center;
  background-color: #dadbd3;
  height: 100vh;
}
Enter fullscreen mode Exit fullscreen mode

To style the div with className ‘app_body’, add this code to App.css too,

.app_body{
  display: flex;
  background-color: #ededed;
  height: 90vh;
  width: 90vw;
  box-shadow: -1px 4px 20px -6px rgba(0, 0, 0, 0.2);
}
Enter fullscreen mode Exit fullscreen mode

In src folder, create a new folder called sidebar.js:

import React from 'react'

function Sidebar() {
  return (
    <div>Sidebar</div>
  )
}

export default Sidebar
Enter fullscreen mode Exit fullscreen mode

In Sidebar.js, go ahead and import the corresponding css file,

import ‘./Sidebar.css’

Create a new file in src folder, Sidebar.css

Edit sidebar.js to look like this,

function Sidebar() {
  return (
    <div className="sidebar">
        <h1>Sidebar</h1>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Edit the div of className App in App.js to look like this,

    <div className="App">
      <div className="app_body">
        <Sidebar/>
        {/* Chat */}
      </div>
    </div>
Enter fullscreen mode Exit fullscreen mode

Also in App.js, add this line:

import Sidebar from './sidebar';

Edit Sidebar.js to look like this,

import React from 'react'
import "./Sidebar.css"

const Sidebar = () => {
    return (
        <div className="sidebar">
            <div className="sidebar_header">

            </div>
            <div className="sidebar_search">

            </div>
            <div className="sidebar_chats">

            </div>
        </div>
    )
}

export default Sidebar
Enter fullscreen mode Exit fullscreen mode

Now we need some icons, so we will need to install Material UI:

npm install @material-ui/core

Also run this code:

npm install @material-ui/icons

Edit Sidebar.js to look like this,

import React from 'react'
import "./Sidebar.css"
import { Avatar } from "@material-ui/core"
import { DonutLarge } from '@material-ui/icons'
import { Chat } from '@material-ui/icons'
import { MoreVert } from '@material-ui/icons'

const Sidebar = () => {
    return (
        <div className="sidebar">
            <div className="sidebar_header">
                <Avatar />
                <div className="sidebar_headerRight">
                    <DonutLarge />
                    <Chat />
                    <MoreVert />
                </div>
            </div>
            <div className="sidebar_search">

            </div>
            <div className="sidebar_chats">

            </div>
        </div>
    )
}

export default Sidebar
Enter fullscreen mode Exit fullscreen mode

Edit your Sidebar.css to look like this,

.sidebar {
    flex: 0.35;
}
.sidebar_header {
    display: flex;
    justify-content: space-between;
    padding: 20px;
    border-right: 1px solid lightgray;
}

.sidebar_headerRight{
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-width: 10vw;
}
Enter fullscreen mode Exit fullscreen mode

At this point, your application's appearance should resemble the following:

Proceed with the build …

Edit Sidebar.js to look like this,

import React from 'react'
import "./Sidebar.css"
import { Avatar, IconButton } from "@material-ui/core"
import { DonutLarge, SearchOutlined } from '@material-ui/icons'
import { Chat } from '@material-ui/icons'
import { MoreVert } from '@material-ui/icons'
import SidebarChat from './SidebarChat'

const Sidebar = () => {
    return (
        <div className="sidebar">
            <div className="sidebar_header">
                <Avatar />
                <div className="sidebar_headerRight">
                    <IconButton className="MuiSvgIcon-root">
                        <DonutLarge />
                    </IconButton>
                    <IconButton className="MuiSvgIcon-root">
                        <Chat />
                    </IconButton>
                    <IconButton className="MuiSvgIcon-root">
                        <MoreVert />
                    </IconButton>
                </div>
            </div>
            <div className="sidebar_search">
                <div className="sidebar_searchContainer">
                    <SearchOutlined/>
                    <input placeholder="Search or start new chat" type="text"/>
                </div>
            </div>
            <div className="sidebar_chats">
                <SidebarChat/>
                <SidebarChat/>
                <SidebarChat/>
                <SidebarChat/>
                <SidebarChat/>
            </div>
        </div>
    )
}

export default Sidebar
Enter fullscreen mode Exit fullscreen mode

In the src folder, add these to files:

‘SidebarChat.js’ and ‘SidebarChat.css’

- SidebarChat.js

import React from 'react'
import './SidebarChat.css'

const SidebarChat = () => {
  return (
    <div className="sidebarChat">
        <h1>hey</h1>
    </div>
  )
}

export default SidebarChat
Enter fullscreen mode Exit fullscreen mode

Edit sidebar.css to look like this,

.sidebar {
    display: flex;
    flex-direction: column;
    flex: 0.35;
}
.sidebar_search {
    display: flex;
    align-items: center;
    background-color: #f6f6f6;
    height: 39px;
    padding: 10px;
}
.sidebar_searchContainer{
    display: flex;
    align-items: center;
    background-color: white;
    width: 100%;
    height: 35px;
    border-radius: 20px;
    padding-left: 5px;
}
.sidebar_searchContainer > .MuiSvgIcon-root{
    color: grey;
    padding: 10;
}
.sidebar_searchContainer > input {
    border: none;
    margin-left: 10px;
}
.sidebar_header {
    display: flex;
    justify-content: space-between;
    padding: 20px;
    border-right: 1px solid lightgray;
}

.sidebar_headerRight{
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-width: 10vw;
}

.sidebar_headerRight >.MuiSvgIcon-root{
    margin-right: 2vw;
    font-size: 24px !important;
}

.sidebar_chats{
    flex: 1;
    background-color: white;
    overflow: scroll;
Enter fullscreen mode Exit fullscreen mode

Next, we are going to add some avatars:

We will use an endpoint that gives you a random avatar

Edit SidebarChat.js to look like this,

import React from 'react'
import './SidebarChat.css'
import { Avatar } from '@material-ui/core'

const SidebarChat = () => {
  return (
    <div className="sidebarChat">
        <Avatar 
src="https://api.dicebear.com/
7.x/adventurer/svg?seed=Fel675gfcedsix667"/>
        <div className="sidebarChat_info">
            <h2>Room name</h2>
            <p>Last message...</p>
        </div>
    </div>
  )
}

export default SidebarChat
Enter fullscreen mode Exit fullscreen mode

You notice we added Avatar as this,

<Avatar src="https://api.dicebear.com/7.x/adventurer/svg?seed=Fel675gfcedsix667"/>

We will make use of a UseEffect hook to generate a new avatar each time the Api is loaded/called:

    const [seed, setSeed] = useState('');
    useEffect(()=>{
        setSeed(Math.floor(Math.random()*5000))
    }, [])
  return (
    <div className="sidebarChat">
        <Avatar src=
{`https://api.dicebear.com/7.x/adventurer/svg?seed=${seed}`}/>
        <div className="sidebarChat_info">
            <h2>Room name</h2>
            <p>Last message...</p>
        </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now we will go ahead and make this accept some prompts.

Add this to SidebarChat.css,

.sidebarChat {
    display: flex;
    padding: 20px;
    cursor: pointer;
    border-bottom: 1px solid #f6f6f6;
}

.sidebarChat:hover{
    background-color: #ebebeb;
}

.sidebarChat_info > h2 {
    font-size: 16px;
    margin-bottom: 8px;
}

.sidebarChat_info{
    margin-left: 15px;
}
Enter fullscreen mode Exit fullscreen mode

We have added a new prop, addNewChat in SidebarChat,

const SidebarChat = ({ addNewChat }) => {

Then in Sidebar.js, we have,

<SidebarChat addNewChat/>
Enter fullscreen mode Exit fullscreen mode

In SidebarChat.js, before we return our jsx, we will have:

If !addNewChat, we have SidebarChat.js show the normal stuff. Otherwise, it will render a div with a className of SidebarChat, and when clicked, it will execute some function to createChat.

By now, SidebarChat.js looks like this,

import React from 'react'
import './SidebarChat.css'
import { Avatar } from '@material-ui/core'
import { useEffect } from 'react'
import { useState } from 'react'

const SidebarChat = ({ addNewChat }) => {
    const [seed, setSeed] = useState('');
    useEffect(()=>{
        setSeed(Math.floor(Math.random()*5000))
    }, [])

    const createChat = () => {
        const roomName = prompt("Please enter name for chat")

        if(roomName) {
            //do some clever stuff in database
        }
    }

  return !addNewChat ? (
    <div className="sidebarChat">
        <Avatar src=
{`https://api.dicebear.com/7.x/adventurer/svg?seed=${seed}`}/>
        <div className="sidebarChat_info">
            <h2>Room name</h2>
            <p>Last message...</p>
        </div>
    </div>
  ): (
    <div onClick={createChat}
    className="sidebarChat">
        <h2>Add new Chat</h2>
    </div>
  )
}

export default SidebarChat
Enter fullscreen mode Exit fullscreen mode

In App.js, lets now add the component, Chat

 <Chat />
Enter fullscreen mode Exit fullscreen mode

In Src folder, create two new files, Chat.js and Chat.css

Chat.js:

import React from 'react'
import "./Chat.css"

const Chat = () => {
  return (
    <div className="chat">

    </div>
  )
}

export default Chat
Enter fullscreen mode Exit fullscreen mode

Edit Chat.js to look like this,

import React from 'react'
import "./Chat.css"
import { Avatar, IconButton } from '@material-ui/core'
import { useState, useEffect } from 'react'
import { AttachFile, MoreVert, SearchOutlined } 
from '@material-ui/icons'

const Chat = () => {
    const [seed, setSeed] = useState('');
    useEffect(()=>{
        setSeed(Math.floor(Math.random()*5000))
    }, [])

    return (
        <div className="chat">
            <div className="chat_header">
                <Avatar src=
{`https://api.dicebear.com/7.x/adventurer/svg?seed=${seed}`}/>
                <div className="chat_headerInfo">
                    <h3>Room name</h3>
                    <p>Last seet at ...</p>
                </div>
                <div className="chat_headerRight">
                    <IconButton>
                        <SearchOutlined/>
                    </IconButton>
                    <IconButton>
                        <AttachFile/>
                    </IconButton>
                    <IconButton>
                        <MoreVert/>
                    </IconButton>
                </div>
            </div>
            <div className="chat_body">

            </div>
            <div className="chat_footer">

            </div>
        </div>
    )
}

export default Chat
Enter fullscreen mode Exit fullscreen mode

Add this to chat.css,

.chat {
    flex: 0.66;
}

.chat_header{
    padding: 20px;
    display: flex;
    align-items: center;
    border-bottom: 1px solid lightgray;
}

.chat_headerInfo {
    flex: 1;
    padding-left: 20px;
}
Enter fullscreen mode Exit fullscreen mode

By now your app should appear like this:

By now, your chat.js file looks like this,

import React from 'react'
import "./Chat.css"
import { Avatar, IconButton } from '@material-ui/core'
import { useState, useEffect } from 'react'
import { AttachFile, MoreVert, SearchOutlined } 
from '@material-ui/icons'

const Chat = () => {
    const [seed, setSeed] = useState('');
    useEffect(() => {
        setSeed(Math.floor(Math.random() * 5000))
    }, [])

    return (
        <div className="chat">
            <div className="chat_header">
                <Avatar src=
{`https://api.dicebear.com/7.x/adventurer/svg?seed=${seed}`} />
                <div className="chat_headerInfo">
                    <h3>Room name</h3>
                    <p>Last seet at ...</p>
                </div>
                <div className="chat_headerRight">
                    <IconButton>
                        <SearchOutlined />
                    </IconButton>
                    <IconButton>
                        <AttachFile />
                    </IconButton>
                    <IconButton>
                        <MoreVert />
                    </IconButton>
                </div>
            </div>
            <div className="chat_body">
                <p className=
{`chat_message ${true && "chat_reciever"}`}>
                    <span className="chat_name">Brown Tylor</span>
                    Hey Guys
                    <span className="chat_timestamp">3:52pm</span>
                </p>
            </div>
            <div className="chat_footer">

            </div>
        </div>
    )
}

export default Chat
Enter fullscreen mode Exit fullscreen mode

On this line of code,

 <p className={`chat_message ${true && "chat_reciever"}`}>
Enter fullscreen mode Exit fullscreen mode

The chat_reciever class is added only if a certain condition is true.

For the User who is signed in, this will be evaluated to true, hence their message will be green, as styled in chat.css:

Chat.css:

.chat {
    flex: 0.65;
    display: flex;
    flex-direction: column;
}

.chat_header{
    padding: 20px;
    display: flex;
    align-items: center;
    border-bottom: 1px solid lightgray;
}

.chat_headerInfo {
    flex: 1;
    padding-left: 20px;
}

.chat_headerInfo > h3 {
    margin-bottom: 3px;
    font-weight: 500;
}

.chat_headerInfo > p {
color: grey
}

.chat_message {
    position: relative;
    font-size: 16px;
    padding: 10px;
    border-radius: 10px;
    width: fit-content;
    background-color: #ffffff;
    margin-bottom: 30px;
}

.chat_reciever{
    margin-left: auto;
    background-color: #dcf8c6;
}

.chat_timestamp {
    margin-left: 10px;
    font-size: xx-small;
}

.chat_name {
    position: absolute;
    top: -15px;
    font-weight: 800;
    font-size: xx-small;
}

.chat_headerRight {
    display: flex;
    justify-content: space-between;
    min-width: 100px;
}

.chat_body {
    flex: 1;
    background-image: url("https://user-images.githubusercontent.com/15075759/28719144-86dc0f70-73b1-11e7-911d-60d70fcded21.png");
    background-repeat: repeat;
    background-position: center;
    padding: 30px;
    overflow: scroll;
}
Enter fullscreen mode Exit fullscreen mode

Your Chat.js now looks like this,

import React from 'react'
import "./Chat.css"
import { Avatar, IconButton } from '@material-ui/core'
import { useState, useEffect } from 'react'
import { AttachFile, InsertEmoticon, MoreVert, SearchOutlined, Mic }
 from '@material-ui/icons'

const Chat = () => {
    const [seed, setSeed] = useState('');
    useEffect(() => {
        setSeed(Math.floor(Math.random() * 5000))
    }, [])

    return (
        <div className="chat">
            <div className="chat_header">
                <Avatar src=
{`https://api.dicebear.com/7.x/adventurer/svg?seed=${seed}`} />
                <div className="chat_headerInfo">
                    <h3>Room name</h3>
                    <p>Last seet at ...</p>
                </div>
                <div className="chat_headerRight">
                    <IconButton>
                        <SearchOutlined />
                    </IconButton>
                    <IconButton>
                        <AttachFile />
                    </IconButton>
                    <IconButton>
                        <MoreVert />
                    </IconButton>
                </div>
            </div>
            <div className="chat_body">
                <p className=
{`chat_message ${true && "chat_reciever"}`}>
                    <span className="chat_name">Brown Tylor</span>
                    Hey Guys
                    <span className="chat_timestamp">3:52pm</span>
                </p>
            </div>
            <div className="chat_footer">
                <InsertEmoticon />
                <form>
                    <input placeholder="Type a message" type="text"/>
                    <button type="submit">Send a message</button>
                </form>
                <Mic />
            </div>
        </div>
    )
}

export default Chat
Enter fullscreen mode Exit fullscreen mode

We now add an onClick sendMessage function on the sendmessage button here:

<button onClick={sendMessage} type="submit">Send a message</button>
Enter fullscreen mode Exit fullscreen mode

Just before the return statement on chat.js, we have the function:

    const sendMessage = (e) => {

    }
Enter fullscreen mode Exit fullscreen mode

Now we need to keep track of the message that the user is typing, so that when he/she pushes the enter key we can proceed to the next phase, like pushing to a database, etc. We keep track of the message by storing it in the state.

const [input, setInput] = useState("");

Then the value of our input variable will be the value set up in the useState,

<input value={input} placeholder="Type a message" type="text"/>
Enter fullscreen mode Exit fullscreen mode

Each time the input changes, we need to fire off an an event to update the input value in the state with the latest user entry:

<input value={input} onChange={e => setInput(e.target.Value)} 
placeholder="Type a message" type="text"/>
Enter fullscreen mode Exit fullscreen mode

In this sendMessage function,

    const sendMessage = (e) => {
        e.preventDefault();
        console.log('You typed >>>', input)
    }
Enter fullscreen mode Exit fullscreen mode

e.preventDefault ensures:

By calling e.preventDefault(), you are telling the browser not to perform the default action associated with the form submission, which is typically a page reload. Instead, the custom behavior defined in the sendMessage function will be executed.

setInput("");

ensures the input is cleaned after pressing enter.

Adding firebase config

Now we will go ahead and add the Firebase config.

Install Firebase with:

npm i firebase

Create/Edit your firebase.js file to look like this:

import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { getAuth, GoogleAuthProvider } from 'firebase/auth';

const firebaseConfig = {
  apiKey: "AIzaSyDcHSKgEks5yWmdS6kb54US0KlbIXeIHR4",
  authDomain: "whats-app-clone-aac80.firebaseapp.com",
  projectId: "whats-app-clone-aac80",
  storageBucket: "whats-app-clone-aac80.appspot.com",
  messagingSenderId: "131021031275",
  appId: "1:131021031275:web:a8e2711178865ea927ae2e",
  measurementId: "G-RH8WL675D8"
};

const firebaseApp = initializeApp(firebaseConfig);
const db = getFirestore(firebaseApp);
const auth = getAuth(firebaseApp);
const provider = new GoogleAuthProvider();

export { auth, provider};
export default db;
Enter fullscreen mode Exit fullscreen mode

Open Firebase on your browser

Click on Firestore database

click on ‘Create Database’,

Click on ‘next’,

Click on ‘Start in test mode’, and click on ‘Enable’.

Click on ‘Start collection’, and call it ‘rooms’, and populate it as follows, with field ‘name’, and value ‘Dance Room’:

Click on ‘Auto-ID’ at the top, and click on ‘save’.

Click ‘Add Document’ to add a second room, populate it with Field ‘name’, and Value ‘Dev Room’, and save:

Let’s now connect the Sidebar to the database.

In sidebar.js, add this:

const [rooms, setRooms] = useSatte([]);

Then add a useEffect, for when the sidebar component loads, we want to show a piece of code.

    useEffect(()=> {

    }, [])
Enter fullscreen mode Exit fullscreen mode

Above code says, perform this action once, when the sidebar component loads.

Import db from firebase with:

import db from './firebase'

Edit your sidebar.js and sidebarChat.js to appear as below:

- Sidebar.js

import "./Sidebar.css"
import React, { useState, useEffect } from 'react';
import { Avatar, IconButton } from '@material-ui/core';
import { DonutLarge, SearchOutlined, Chat, MoreVert } from 
'@material-ui/icons';
import SidebarChat from './SidebarChat';
import { db } from './firebase'; 
// Assuming your firebase.js exports the db instance
import { getFirestore, collection, onSnapshot } from 
'firebase/firestore';

const Sidebar = () => {
    const [rooms, setRooms] = useState([]);

    useEffect(() => {
        const fetchData = async () => {
          const roomsCollection = collection(db, 'rooms');
          console.log('Rooms Collection:', roomsCollection); 
            // Log the rooms collection

          const unsubscribe = onSnapshot(roomsCollection, 
            (snapshot) => {
            console.log('Snapshot received:', snapshot.docs);    
            // Log the snapshot data

            setRooms(
              snapshot.docs.map((doc) => ({
                id: doc.id,
                data: doc.data(),
              }))
            );
          });

          return () => {
            unsubscribe();
          };
        };

        fetchData();
      }, []);


    return (
        <div className="sidebar">
            <div className="sidebar_header">
                <Avatar />
                <div className="sidebar_headerRight">
                    <IconButton className="MuiSvgIcon-root">
                        <DonutLarge />
                    </IconButton>
                    <IconButton className="MuiSvgIcon-root">
                        <Chat />
                    </IconButton>
                    <IconButton className="MuiSvgIcon-root">
                        <MoreVert />
                    </IconButton>
                </div>
            </div>
            <div className="sidebar_search">
                <div className="sidebar_searchContainer">
                    <SearchOutlined />
                    <input placeholder="Search or start new chat" 
                type="text" />
                </div>
            </div>
            <div className="sidebar_chats">
                <SidebarChat addNewChat />
                {rooms.map(room => (
                    <SidebarChat key={room.id} id={room.id} 
                        name={room.data.name} />
                ))}
            </div>
        </div>
    )
}

export default Sidebar
Enter fullscreen mode Exit fullscreen mode

-Sidebarchat.js

import React from 'react'
import './SidebarChat.css'
import { Avatar } from '@material-ui/core'
import { useEffect } from 'react'
import { useState } from 'react'

const SidebarChat = ({ addNewChat, id, name }) => {
    const [seed, setSeed] = useState('');
    useEffect(()=>{
        setSeed(Math.floor(Math.random()*5000))
    }, [])

    const createChat = () => {
        const roomName = prompt("Please enter name for chat")

        if(roomName) {
            //do some cclever stuff in database
        }
    }

  return !addNewChat ? (
    <div className="sidebarChat">
        <Avatar src=
{`https://api.dicebear.com/7.x/adventurer/svg?seed=${seed}`}/>
        <div className="sidebarChat_info">
            <h2>{name}</h2>
            <p>Last message...</p>
        </div>
    </div>
  ): (
    <div onClick={createChat}
    className="sidebarChat">
        <h2>Add new Chat</h2>
    </div>
  )
}

export default SidebarChat
Enter fullscreen mode Exit fullscreen mode

To add a new room, add this to sidebarChat.js,

        if(roomName) {
            //do some cclever stuff in database
            db.collection("rooms").add({
                name: roomName,
            })
        }
Enter fullscreen mode Exit fullscreen mode

Implement the react router

Let’s now implement the react router, such that when you click on any of the rooms, it reflects on the sidebarChat.js

Install react-router-dom with:

npm i react-router-dom

Surround everything in App.js with a Router and Routes.

Edit your App.js to look like this,

import React from 'react';
import './App.css';
import Chat from './Chat';
import Sidebar from './Sidebar';
import { BrowserRouter as Router, Routes, Route } from 
'react-router-dom';

function App() {
  return (
    <div className="App">
      <div className="app_body">
        <Router>
          <Routes>
            <Route path="/rooms/:roomId" element={
              <>
                <Sidebar />
                <Chat />
              </>
            } />
            <Route path="/app" element={
              <>
                 <Sidebar />
              </>
            } />
          </Routes>
        </Router>
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

In Chat.js, let’s capture the roomId with useParams() hook:

const { roomId } = useParams();

The params in this case is the id variable passed to the link here: (The roomId) variable,

<Route path="/rooms/:roomId" element={

as this is the path under which Chat.js is displayed.

Add these imports to Chat.js,

import db from './firebase'
import { doc, onSnapshot } from 'firebase/firestore';
Enter fullscreen mode Exit fullscreen mode

Make use of the roomId and a useEffect hook to get to capture the roomName:

    const [roomName, setRoomName] = useState("");

    useEffect(() => {
        if (roomId) {
            const roomRef = doc(db, 'rooms', roomId); 
            const unsubscribe = onSnapshot(roomRef, (snapshot) => {
                setRoomName(snapshot.data()?.name || ''); 
            });

            return () => unsubscribe(); 
        }
    }, [roomId]);
Enter fullscreen mode Exit fullscreen mode

In the above, each time the roomId changes, useEffect ensures the roomName is updated.

We also update the seed each time room Id changes:

Add setSeed below setRoomName in Chat.js,

setRoomName(snapshot.data()?.name || ''); 
setSeed(Math.floor(Math.random() * 5000))
Enter fullscreen mode Exit fullscreen mode

To make the avatars consistent, we can rather set seed to the roomId value.

In SidebarChat.js, replace Avatar source to this:

<Avatar src={https://api.dicebear.com/7.x/adventurer/svg?seed=${id}} />

In Chat.js, replace Avatar source to this:

<Avatar src={https://api.dicebear.com/7.x/adventurer/svg?seed=${roomId}} />

Login with google authentication

Before working on the messages,

Let us work on the Login stuff with google authentication:

In App.js, we will render the app only if there is a user logged in.

Add this to App.js:

const [user, setUser] = useState(null);

In your src folder, add a Login.js component.

Add these to firebase.js:

import { getAuth, GoogleAuthProvider, signInWithPopup } 
from 'firebase/auth';
Enter fullscreen mode Exit fullscreen mode

Your Login.js appears like this,

import React from 'react';
import { Button } from '@material-ui/core';
import './Login.css';
import { auth, provider } from './firebase'; 
import { signInWithPopup } from './firebase';

const Login = () => {
  const signIn = async () => {
    try {
      // Destructure signInWithPopup from auth
      const { user } = await signInWithPopup(auth, provider);
      console.log('User signed in:', user);
    } catch (error) {
      alert(error.message);
    }
  };

  return (
    <div className="login">
      <div className="login_container">
        <img 
        src="https://tochat.be/whatsapp-icon-white.png" alt="" />
        <div className="login_text">
          <h1>Sign in to WhatsApp</h1>
        </div>
        <Button type="submit" onClick={signIn}>
          Sign In With Google
        </Button>
      </div>
    </div>
  );
};

export default Login;
Enter fullscreen mode Exit fullscreen mode

your Login.css appears like this:

.login {
    background-color: #f8f8f8;
    height: 100vh;
    width: 100vw;
    display: grid;
    place-items: center;
}

.login_container {
    padding: 100px;
    text-align: center;
    background-color: white;
    border-radius: 10px;
    box-shadow: -1px 4px 20px -6px rgba(0, 0, 0, 0.2);
}

.login_container > img {
    object-fit: contain;
    height: 100px;
    margin-bottom: 40px;
}

.login_container > button {
    margin-top: 50px;
    text-transform: inherit !important;
    background-color: #0a8d48 !important;
    color: white;
}
Enter fullscreen mode Exit fullscreen mode

Go to firebase on your browser and click on Authentication:

Click on Get started and this screen will pop up:

Click on ‘Google’, the ‘enable’, then select a support email address, and click on ‘save’.

We now need to wrap the app in state provider.

In index.js, wrap the app in a state provider:

root.render(
  <React.StrictMode>
    <StateProvider initialState={initialState} reduce={reducer}>
      <App />
    </StateProvider>
  </React.StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

The Stateprovider acts as a data layer, where we can push and pull data. When we sign in, we push the user into the data layer, and we can pull the user from the data layer as well.

In your scr folder, create a new file, StateProvider.js:

import React, { createContext, useContext, useReducer } 
from "react";
import reducer, { initialState } from "./reducer"; 

export const StateContext = createContext();

export const StateProvider = ({ children }) => (
  <StateContext.Provider value={useReducer(reducer, initialState)}>
    {children}
  </StateContext.Provider>
);

export const useStateValue = () => useContext(StateContext);
Enter fullscreen mode Exit fullscreen mode

In the src folder, create a new file, reducer.js

export const initialState = {
    user: null, //We start with the user ot being logged in
};

export const actionTypes = {
    SET_USER: "SET_USER", 
//We push signed in user to the data layer
};

const reducer = (state, action) => {
    console.log(action);
    switch (action.type) {
        case actionTypes.SET_USER: 
            return {
                user: action.user,
            };

            default:
                return state;
    }
};

export default reducer;
Enter fullscreen mode Exit fullscreen mode

in App.js, add this code,

import { useStateValue } from './StateProvider';

const [{ user }, dispatch] = useStateValue();
Enter fullscreen mode Exit fullscreen mode

Your Login.js now appears as this:

import React from 'react';
import { Button } from '@material-ui/core';
import './Login.css';
import { auth, provider } from './firebase'; 
import { signInWithPopup } from './firebase';
import { useStateValue } from './StateProvider';
import { actionTypes } from './reducer';

const Login = () => {
    const [{}, dispatch] = useStateValue();

    const signIn = () => {
        signInWithPopup(auth, provider)
          .then((result) => {
            console.log('user',result.user)
            dispatch({
              type: actionTypes.SET_USER,
              user: result.user,
            });
          })
          .catch((error) => {
            alert(error.message);
          });
      };

  return (
    <div className="login">
      <div className="login_container">
        <img 
        src="https://tochat.be/whatsapp-icon-white.png" alt="" />
        <div className="login_text">
          <h1>Sign in to WhatsApp</h1>
        </div>
        <Button type="submit" onClick={signIn}>
          Sign In With Google
        </Button>
      </div>
    </div>
  );
};

export default Login;
Enter fullscreen mode Exit fullscreen mode

For the picture at the top on the image below,

We made it pick the Google photoURL of the logged in user by editing the avatar source in sidebar.js to this:

<Avatar src={user?.photoURL}/>
Enter fullscreen mode Exit fullscreen mode

and adding this line at the top in sidebar.js

const [{ user }, dispatch] = useStateValue();

Adding a new collection, messages

Inside the rooms collection, let’s have a new collection: messages

Click on the first room, click on add collection.

On collection id, type ‘messages’

Click on auto id, on the Field input, type ‘message’ and under value, type ‘Hey guys’, then save

Add another Field, ‘name’ and value ‘Asha’

Add another field ‘timestamp’, Type ‘timestamp’ and choose a date and save.

Pulling messages from db

Now to pull the messages:

We will create a state that keeps track of all the messages.

Edit the useEffect hook in chat.js to add this,

useEffect(() => {
        if (roomId) {
            const roomRef = doc(db, 'rooms', roomId);
            const unsubscribeRoom = onSnapshot(roomRef, 
            (snapshot) => {
                setRoomName(snapshot.data()?.name || '');
            });

            const messagesQuery = query(
                collection(db, 'rooms', roomId, 'messages'),
                orderBy('timestamp', 'asc')
            );

            const unsubscribeMessages = onSnapshot(messagesQuery, 
            (snapshot) => {
                setMessages(snapshot.docs.map((doc) => doc.data()));
            });

            return () => {
                unsubscribeRoom();
                unsubscribeMessages();
            };
        }
    }, [roomId]);
Enter fullscreen mode Exit fullscreen mode

And also add this line of code:

const [messages, setMessages] = useState([]);

Next, edit the div with className chatBody, to appear like this:

<div className="chat_body">
                {messages.map(message => (
                    <p className={`chat_message ${true 
                        && "chat_reciever"}`}>
                        {message.timestamp ? (
                            <>
                                <span 
                className="chat_name">{message.name}</span>
                                {message.message}
                                <span className="chat_timestamp">
                 {new Date(message.timestamp.toDate()).toUTCString()}
                                </span>
                            </>
                        ) : (

                            <span className="loading_timestamp">
                                Loading ...
                            </span>
                        )}
                    </p>

                ))}
            </div>
Enter fullscreen mode Exit fullscreen mode

Add a useEffect to scroll to the bottom of the chat in chat.js:

    useEffect(() => {
        const chatBody = document.querySelector('.chat_body');
        chatBody.scrollTop = chatBody.scrollHeight;
    }, [messages]);
Enter fullscreen mode Exit fullscreen mode

Adding a message to messages collection

Next, lets add a message to the messages collection:

In chat.js, we will have this in the sendMessage function:

const sendMessage = async (e) => {
        e.preventDefault();
        console.log('You typed >>>', input);

        try {
            const messageRef = await addDoc(
                collection(db, 'rooms', roomId, 'messages'),
                {
                    message: input,
                    name: user.displayName,
                    timestamp: serverTimestamp(),
 // Use serverTimestamp instead of FieldValue.serverTimestamp()
                }
            );

            console.log('Message added with ID: ', messageRef.id);
        } catch (error) {
            console.error('Error adding message: ', error);
        }

        setInput("");
    };
Enter fullscreen mode Exit fullscreen mode

Differentiate sender and receiver messages

Next, we ensure that the chats from the sender are colored white, while those from the current signed in user are colored green.

In chat.js in the div with the chat_body class, edit the p tag to look like this,

    <p className={`chat_message ${message.name === user.displayName 
&& "chat_reciever"}`}>
Enter fullscreen mode Exit fullscreen mode

This will result to this effect:

Enable last seen in for each room

To determine the last seen in chat.js, we add this code to get the timestamp of the last message in that room:

         <p>
            {messages.length > 0 && "Last seen"}
               {
     messages.length > 0 && messages[messages.length - 1]?.timestamp
                                ? (
     <span className="chat_timestamp" style={{ fontSize: '14px' }}>
     {new Date(messages[messages.length - 1]
.timestamp.toDate()).toUTCString()}
                                    </span>
                                )
                                : 'No messages'
                        }

                    </p>
Enter fullscreen mode Exit fullscreen mode

The code above, added just below the RoomName, displays a last seen timestamp when there are messages in the room, and displays ‘no messages’ when there are no messages in the room:

Determine last message for each room

To determine the last message in SidebarChat.js,

const [messages, setMessages] = useState("")

    useEffect(() => {
        if(id){
            const messagesQuery = query(
                collection(db, 'rooms', id, 'messages'),
                orderBy('timestamp', 'desc')
            );

            const unsubscribeMessages = 
            onSnapshot(messagesQuery, (snapshot) => {
                setMessages(snapshot.docs.map((doc) => doc.data()));
            });

            return () => {
                unsubscribeMessages();
            };
        }
    }, [])
Enter fullscreen mode Exit fullscreen mode

Replace the p tag containing last message with this code:

<p>
                        {messages.length > 0 && "Last message ..."}
                        {
                            messages.length > 0 
                                ? (
          <span className="chat_timestamp" 
                style={{ fontSize: '14px' }}>
                                        {messages[0]?.message}
                                    </span>
                                )
                                : 'No messages'
                        }
                    </p>
Enter fullscreen mode Exit fullscreen mode

Deploying app

Now let us go ahead to deploy our App

On your terminal in the root folder:

Run:

firebase login

and proceed with the steps to login.

Run:

firebase init

on the pop up that comes up, scroll down and hit the spacebar on hosting, click enter, and click on ‘use an existing project’, select your whatsapp clone project and click Enter.

On ‘What do you want to use as your public directory?’, type ‘build’ and press Enter.

On ‘Configure as a single-page app’ type ‘y’

On ‘Set up automatic builds and deploys with GitHub?’ type ‘No’

Run:

npm run build

Run:

firebase deploy

This will produce a link to your App:

https://whats-app-clone-aac80.web.app/

Thanks so much for reading through! To support my blog you can make a donation through https://www.buymeacoffee.com/angela300

Top comments (0)