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>
Delete everything in App.css
In index.css, add this code,
* {
margin: 0;
}
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>
);
}
In App.css, add this code to style the app:
.App {
display: grid;
place-items: center;
background-color: #dadbd3;
height: 100vh;
}
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);
}
In src folder, create a new folder called sidebar.js:
import React from 'react'
function Sidebar() {
return (
<div>Sidebar</div>
)
}
export default Sidebar
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>
)
}
Edit the div of className App in App.js to look like this,
<div className="App">
<div className="app_body">
<Sidebar/>
{/* Chat */}
</div>
</div>
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
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
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;
}
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
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
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;
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
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>
)
}
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;
}
We have added a new prop, addNewChat in SidebarChat,
const SidebarChat = ({ addNewChat }) => {
Then in Sidebar.js, we have,
<SidebarChat addNewChat/>
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
In App.js, lets now add the component, Chat
<Chat />
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
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
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;
}
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
On this line of code,
<p className={`chat_message ${true && "chat_reciever"}`}>
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;
}
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
We now add an onClick sendMessage function on the sendmessage button here:
<button onClick={sendMessage} type="submit">Send a message</button>
Just before the return statement on chat.js, we have the function:
const sendMessage = (e) => {
}
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"/>
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"/>
In this sendMessage function,
const sendMessage = (e) => {
e.preventDefault();
console.log('You typed >>>', input)
}
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;
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(()=> {
}, [])
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
-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
To add a new room, add this to sidebarChat.js,
if(roomName) {
//do some cclever stuff in database
db.collection("rooms").add({
name: roomName,
})
}
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;
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';
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]);
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))
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';
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;
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;
}
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>
);
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);
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;
in App.js, add this code,
import { useStateValue } from './StateProvider';
const [{ user }, dispatch] = useStateValue();
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;
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}/>
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]);
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>
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]);
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("");
};
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"}`}>
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>
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();
};
}
}, [])
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>
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)