Firebase Chat App Tutorial | Jan 2023
alakkadshaw
Posted on December 30, 2022
This article was originally published at https://deadsimplechat.com/blog/firebase-chat-app-tutorial/
In the blog post, we will go through the process of building a chat application using firebase.
What is Firebase?
Firebase is a type of serverless backend provided by google. It offers a set of services to serve as a backend of mobile or web applications.
It offers services like a NoSQL database, social authentication, notification and real-time communication services.
In following tutorial will use the firebase real-time database as a backend for our chat service.
So, let's get started.
Pre-requisite
To following allow this tutorial you would need:
- Basic knowledge of HTML and CSS
- Familiar with JavaScript
Step 1: Signup for a Firebase account
If you don't already have a firebase account, go to firebase.google.com and signup for a free firebase account.
Press the "Get Started" button to create a free account
Name your project anything you like, we will name our project "firebase-chatapp"
Step 2: Configuring Firestore Database
After creating a firebase account, we will create a Firestore Database, that Firestore Database will store our chat messages, and we will use the database to subscribe to real-time updates.
After creating the project using the sidebar select "Firestore database"
Then click the "Create database" button
💡New to DeadSimpleChat? It's a turnkey chat that you can easily add to your website or App —without any complicated code. For Virtual / Live events, SaaS App, Social Platform, Education, Gaming, and Finance Sign Up for Free
Step 3: Allowing Anonymous Authentication
For our chat we want the users to simply type in a username and join the chat, for that we would need "Anonymous" authentication.
To enable that we will select the "Authentication" option from the sidebar.
Click the "Get Started" button from the Authentication page
Now select the "Anonymous" option
And Enable it and save
Now Go back to "Firestore Database" and select "Rules" and update the existing rule to the following:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
This will allow the authenticated users to read and write to the firebase database.
Step 4: Obtaining Firebase Config
The firebase config is required to connect our chat app to the firebase server. Tie obtain the config go to project settings
Click the icon to generate the credentials for the web app
Name the app to whatever you like
Select use "" tag and copy the config.</p> <p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fvcsw0m9wdnizrf1tgxr.png" alt="Image description"></p> <p>These are all the steps required to setup the firebase project, now let's start building our actual chat application.</p> <h2> <a name="step-4-scaffolding-the-initial-chat-ui" href="#step-4-scaffolding-the-initial-chat-ui" class="anchor"> </a> Step 4: Scaffolding the Initial Chat UI </h2> <p>In our chat application, we would require an interface that would allow the user to enter his/her username.</p> <p>And we would require the main chat UI, where all the messages will be displayed and an input area that allows the user to send new messages.</p> <p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wyezrstu8lz615k1at4b.png" alt="Image description"></p> <p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7wkkztzn32gpqat71fa4.png" alt="Image description"></p> <p><a href="https://gist.githubusercontent.com/mlakkadshaw/3802f9061965cf6281f41a6436ff1f90/raw/971d494b0c632a0f7d26b6e9acf8ca6e1dd98761/index.html">View Code Here</a></p> <p>We will use Tailwind CSS to style the user interface, the above code contains our basic UI.</p> <p>We gave the id joinView to the container that contains the text field to enter the username and the join button.</p> <p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zo98wujnqxethikk8j0c.png" alt="Image description"></p> <p>We give the id chatsViews to the main chat room container</p> <p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ppkmhsm8knsl9dzlrj9e.png" alt="Image description"></p> <p>When the user has not joined the chat room, we will hide the chatsView container and when the user has joined the chat room we will hide the joinView container and show the chatsView container.</p> <p>We also created a ul tag and gave it the id messageList this will contain all the messages that are sent in the chat room.</p> <p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e7o2pirolkxrn0yab2or.png" alt="Image description"></p> <p>We will redraw this list each time a new message is sent to the chat room.</p> <h2> <a name="step-5-wiring-up-the-ui" href="#step-5-wiring-up-the-ui" class="anchor"> </a> Step 5: Wiring up the UI </h2> <p>Now let's wire up the UI using JavaScript and Firebase SDK, create the main.js</p> <p>Import the Firebase SDK<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>import { initializeApp } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-app.js"; // Add Firebase products that you want to use import { getAuth, signInAnonymously, } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-auth.js"; import { getFirestore, addDoc, collection, onSnapshot, doc, getDocs, query, where, } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-firestore.js"; </code></pre></div> <p></p> <p>Paste the firebase config, that you had obtained from Step 3<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>// Your web app's Firebase configuration const firebaseConfig = { apiKey: "AIzaSyCRa0JplErns226xER0Fpk1cEulP2c6y0Q", authDomain: "fir-chat-a082b.firebaseapp.com", databaseURL: "https://fir-chat-a082b-default-rtdb.firebaseio.com", projectId: "fir-chat-a082b", storageBucket: "fir-chat-a082b.appspot.com", messagingSenderId: "589407773218", appId: "1:589407773218:web:676cf16926d1dee647607b", }; </code></pre></div> <p></p> <p>We will call the Firebase initializeApp method and pass it the firebaseConfig<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>const app = initializeApp(firebaseConfig); </code></pre></div> <p></p> <p>Obtain the instance of the Firestore database and Auth. We will use the Firestore database to send messages and receive real-time updates about the messages.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>const db = getFirestore(app); const auth = getAuth(app); </code></pre></div> <p></p> <p>Now we will refer to the HTML elements on our UI to listen to the events and to show and hide the user-interface elements.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>const joinButton = document.getElementById("joinButton"); const usernameInput = document.getElementById("usernameInput"); const messageInput = document.getElementById("messageInput"); const sendButton = document.getElementById("sendButton"); const joinView = document.getElementById("joinView"); const chatsView = document.getElementById("chatsView"); </code></pre></div> <p></p> <h2> <a name="handling-join" href="#handling-join" class="anchor"> </a> Handling Join </h2> <p>We will add the click event listener to the "Join" button. When the join button is clicked we will check if there is any value in the usernameInput field.</p> <p>If the user has typed a username in the usernameInput field then we will call the signInAnonymously method of the Firebase SDK, to sign in the user into the chat room.</p> <p>Without calling this method, we cannot read from or write to the Firestore database.</p> <p>Once the sign-in is successful we will hide the joinView and show the chatsView.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>let specifiedUsername = ""; let userLoggedIn = false; joinButton.addEventListener("click", () => { specifiedUsername = usernameInput.value; if (!specifiedUsername) { alert("username cannot be empty"); return; } signInAnonymously(auth) .then(async () => { joinView.classList.add("hidden"); chatsView.classList.remove("hidden"); userLoggedIn = true; await loadHistoricalMessages(); await subscribeToNewMessages(); writeMessagesArray(); console.log("User logged-in"); }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; console.log(errorCode, errorMessage); }); }); </code></pre></div> <p></p> <h2> <a name="loading-previous-messages" href="#loading-previous-messages" class="anchor"> </a> Loading Previous Messages </h2> <p>In the previous code snippet, we logged the user into the chat room.</p> <p>And as you can see in the code snippet, we are also calling the method loadHistoricalMessages() so we will implement that method in this section.</p> <p>In the loadHistoricalMessages() the method we will query the Firestore database to fetch all the previous messages, and we will store them in a global messages array.</p> <p>To fetch the historical messages we will call the getDocs method:<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>async function loadHistoricalMessages() { messages = []; const querySnapshot = await getDocs(collection(db, "messages")); querySnapshot.forEach((doc) => { messages.push({ id: doc.id, ...doc.data(), }); }); console.log(messages); return messages; } </code></pre></div> <p></p> <h2> <a name="listening-to-new-messages" href="#listening-to-new-messages" class="anchor"> </a> Listening to New Messages </h2> <p>To listen to the new messages, we will call the onSnapshot method of the Firestore SDK.</p> <p>The onSnapShot method takes in a query, as we want to listen to the complete collection we will not specify any condition in the query.</p> <p>This will cause the method to trigger each time a new message is sent in the chat room.</p> <p>And when onSnapShot method is triggered, we will push the new messages to our global messages array.</p> <p>But the onSnapShot method returns not just the new message, but also some previous messages along with the new message.</p> <p>We don't want to push the other messages into the array, we just want to push the new message, because it would result in duplicate messages being shown in the chat room.</p> <p>To prevent that we will create a hash map of the id of all the existing messages, and then loop through the newMessages and we will not push messages whose id matches the existing messages.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>function subscribeToNewMessages() { const q = query(collection(db, "messages")); const unsubscribe = onSnapshot(q, (querySnapshot) => { const newMessages = []; querySnapshot.forEach((doc) => { newMessages.push({ id: doc.id, ...doc.data(), }); }); /** * Creating hash map of the existing messages. */ let existingMessageHash = {}; for (let message of messages) { existingMessageHash[message.id] = true; } /** * Push only those messages which do not * exist in the hashMap */ for (let message of newMessages) { if (!existingMessageHash[message.id]) { messages.push(message); } } writeMessagesArray(); }); } </code></pre></div> <p></p> <p>We have created a hashMap of message ID to prevent creating a nested for loop.</p> <h2> <a name="displaying-the-messages-in-the-ui" href="#displaying-the-messages-in-the-ui" class="anchor"> </a> Displaying the Messages in the UI </h2> <p>We have been calling the writeMessagesArray() method in the signInAnonymously method and also in the subscribeToNewMessages listener.</p> <p>The writeMessagesArray() method will display the messages in the messages array in our chat UI.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>function writeMessagesArray() { const html = []; for (let message of messages) { html.push(messageTemplate(message.message, message.user, message.created)); } document.getElementById("messageList").innerHTML = html.join(""); } function messageTemplate(message, username, timestamp) { return `<li> <div class="flex space-x-2 pl-2 pt-2"> <div class="flex flex-col"> <div class="flex items-baseline space-x-2"> <div class="text-sm font-bold">${username}</div> <div class="text-sm text-gray-400">${ new Date(timestamp.seconds * 1000).toLocaleDateString() + " " + new Date(timestamp.seconds * 1000).toLocaleTimeString() }</div> </div> <div class="text-sm text-gray-500">${message}</div> </div> </div> </li>`; } </code></pre></div> <p></p> <p>The method goes through each message in the messages array and calls the messageTemplate method.</p> <p>The messageTemplate methods contains the HTML code to display the message, and it accepts the username, message, and timestamp and returns the html for the message.</p> <p>We push the HTML into an array and the adds the HTML to the messageList ul tag.</p> <h2> <a name="sending-the-messages" href="#sending-the-messages" class="anchor"> </a> Sending the messages </h2> <p>So far we have gone through the process of joining the chat room, loading historical messages, listening to new messages and drawing the messages on screen.</p> <p>Now let's implement the part of actually sending a message.</p> <p>To send the message, we will add a click listener to the sendButton. When the sendButton is clicked we will call the addDoc method of the Firestore database and store the message in the Firestore database.</p> <p>Our message model will contain the following properties:</p> <ul> <li>user - username of the user</li> <li>message - the actual messages</li> <li>created - timestamp when the message was created </li> </ul> <div class="highlight"><pre class="highlight plaintext"><code>sendButton.addEventListener("click", async () => { const message = messageInput.value; messageInput.value = ""; const docRef = await addDoc(collection(db, "messages"), { user: specifiedUsername, message: message, created: new Date(), }); console.log(docRef); }); </code></pre></div> <p></p> <p>Here is the complete code of the main.js file altogether.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>import { initializeApp } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-app.js"; // Add Firebase products that you want to use import { getAuth, signInAnonymously, } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-auth.js"; import { getFirestore, addDoc, collection, onSnapshot, doc, getDocs, query, where, } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-firestore.js"; // Your web app's Firebase configuration const firebaseConfig = { apiKey: "AIzaSyCRa0JplErns226xER0Fpk1cEulP2c6y0Q", authDomain: "fir-chat-a082b.firebaseapp.com", databaseURL: "https://fir-chat-a082b-default-rtdb.firebaseio.com", projectId: "fir-chat-a082b", storageBucket: "fir-chat-a082b.appspot.com", messagingSenderId: "589407773218", appId: "1:589407773218:web:676cf16926d1dee647607b", }; // Initialize Firebase const app = initializeApp(firebaseConfig); const db = getFirestore(app); const auth = getAuth(app); const joinButton = document.getElementById("joinButton"); const usernameInput = document.getElementById("usernameInput"); const messageInput = document.getElementById("messageInput"); const sendButton = document.getElementById("sendButton"); const joinView = document.getElementById("joinView"); const chatsView = document.getElementById("chatsView"); let messages = []; let specifiedUsername = ""; let userLoggedIn = false; joinButton.addEventListener("click", () => { specifiedUsername = usernameInput.value; if (!specifiedUsername) { alert("username cannot be empty"); return; } signInAnonymously(auth) .then(async () => { joinView.classList.add("hidden"); chatsView.classList.remove("hidden"); userLoggedIn = true; await loadHistoricalMessages(); await subscribeToNewMessages(); writeMessagesArray(); console.log("User logged-in"); }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; console.log(errorCode, errorMessage); }); }); sendButton.addEventListener("click", async () => { const message = messageInput.value; messageInput.value = ""; const docRef = await addDoc(collection(db, "messages"), { user: specifiedUsername, message: message, created: new Date(), }); console.log(docRef); }); function subscribeToNewMessages() { const q = query(collection(db, "messages")); const unsubscribe = onSnapshot(q, (querySnapshot) => { const newMessages = []; querySnapshot.forEach((doc) => { newMessages.push({ id: doc.id, ...doc.data(), }); }); /** * Creating hash map of the existing messages. */ let existingMessageHash = {}; for (let message of messages) { existingMessageHash[message.id] = true; } /** * Push only those messages which do not * exist in the hashMap */ for (let message of newMessages) { if (!existingMessageHash[message.id]) { messages.push(message); } } writeMessagesArray(); }); } async function loadHistoricalMessages() { messages = []; const querySnapshot = await getDocs(collection(db, "messages")); querySnapshot.forEach((doc) => { messages.push({ id: doc.id, ...doc.data(), }); }); console.log(messages); return messages; } function writeMessagesArray() { const html = []; for (let message of messages) { html.push(messageTemplate(message.message, message.user, message.created)); } document.getElementById("messageList").innerHTML = html.join(""); } function messageTemplate(message, username, timestamp) { return `<li> <div class="flex space-x-2 pl-2 pt-2"> <div class="flex flex-col"> <div class="flex items-baseline space-x-2"> <div class="text-sm font-bold">${username}</div> <div class="text-sm text-gray-400">${ new Date(timestamp.seconds * 1000).toLocaleDateString() + " " + new Date(timestamp.seconds * 1000).toLocaleTimeString() }</div> </div> <div class="text-sm text-gray-500">${message}</div> </div> </div> </li>`; } </code></pre></div> <p></p> <h2> <a name="step-6-thats-it" href="#step-6-thats-it" class="anchor"> </a> Step 6: That's it </h2> <p>We have built a simple group chat app using Firebase and Firestore database.</p> <p>You can go to your Firebase FireStore dashboard and you can see the message sent in the collection.</p> <p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gil4rq0gx64snj1eiybs.png" alt="Image description"></p> <p>You can also check out the demo of the Chat Room Below</p> <p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i494bqjtcztu158sikat.gif" alt="Chat room demo"></p> <h2> <a name="add-chat-to-your-web-app-with-dead-simple-chat" href="#add-chat-to-your-web-app-with-dead-simple-chat" class="anchor"> </a> Add Chat to your Web App with <a href="https://deadsimplechat.com/">Dead Simple Chat</a> </h2> <p>Dead Simple Chat allows you to easily add prebuilt group chat in minutes to any web or mobile app.</p> <p>It is highly scalable and can be integrated in minutes. It has Chat SDK, Customization and Moderation features to make the chat fit for any chat use-case, be it Live Streaming, Group Chat or 1-1 Chat.</p>
Posted on December 30, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.