category: How toAug 22, 2022

Building a chat – Browser Notifications with React, Websockets and Web-Push 🤯

In the previous article in this series, we talked about Socket.io, how you can send messages between a React app client and a Socket.io server, how to get active users in your web application, and how to add the "User is typing..." feature present in most modern chat applications.

What is this article about?

We have all encountered chat over the web, that can be Facebook, Instagram, Whatsapp and the list goes on.
Just to give a bit of context, you send a message to a person or a group, they see the message and reply back. Simple yet complex.

In the previous article in this series, we talked about Socket.io, how you can send messages between a React app client and a Socket.io server, how to get active users in your web application, and how to add the “User is typing…” feature present in most modern chat applications.

In this final article, we’ll extend the chat application features. You will learn how to keep your users engaged by sending them desktop notifications when they are not online and how you can read and save the messages in a JSON file. However, this is not a secure way of storing messages in a chat application. Feel free to use any database of your choice when building yours.

Push Notifications

Novu – the first open-source notification infrastructure

Just a quick background about us. Novu is the first open-source notification infrastructure. We basically help to manage all the product notifications. It can be In-App (the bell icon like you have in Facebook – Websockets), Emails, SMSs and so on.
I would be super happy if you could give us a star! And let me also know in the comments ❤️
https://github.com/novuhq/novu

Novu

How to send desktop messages to users

Here, I’ll guide you through sending desktop notifications to offline users when they have new chat messages.

Notification

In the previous article, we created the ChatFooter component containing a form with an input field and a send button. Since we will be sending a notification immediately after a user sends a message, this is where the desktop notifications functionality will exist.

Chat App

Follow the steps below:

Update the ChatFooter.js component to contain a function named checkPageStatus that runs after a message is sent to the Socket.io server. The function accepts the username and the user’s message.

1import React, {useState} from 'react'
2
3const ChatFooter = ({socket}) => {
4    const [message, setMessage] = useState("")
5    const handleTyping = () => socket.emit("typing",`${localStorage.getItem("userName")} is typing`)
6
7    const handleSendMessage = (e) => {
8        e.preventDefault()
9        if(message.trim() && localStorage.getItem("userName")) {
10        socket.emit("message", 
11            {
12            text: message, 
13            name: localStorage.getItem("userName"), 
14            id: `${socket.id}${Math.random()}`
15            }) 
16                //Here it is 👇🏻
17        checkPageStatus(message, localStorage.getItem("userName")) 
18        }}
19        setMessage("")
20    }
21
22    //Check PageStatus Function
23    const checkPageStatus = () => {
24
25    }
26
27  return (
28    <div className='chat__footer'>
29        <form className='form' onSubmit={handleSendMessage}>
30          <input 
31            type="text" 
32            placeholder='Write message' 
33            className='message' 
34            value={message} 
35            onChange={e => setMessage(e.target.value)}
36            onKeyDown={handleTyping}
37            />
38            <button className="sendBtn">SEND</button>
39        </form>
40     </div>
41  )
42}
43
44export default ChatFooter;

Tidy up the ChatFooter component by moving the checkPageStatus function into a src/utils folder. Create a folder named utils.

1cd src
2mkdir utils

Create a JavaScript file within the utils folder containing the checkPageStatus function.

1cd utils
2touch functions.js

Copy the code below into the functions.js file.

1export default function checkPageStatus(message, user){
2
3}

Update the ChatFooter component to contain the newly created function from the utils/functions.js file.

1import React, {useState} from 'react'
2import checkPageStatus from "../utils/functions"
3//....Remaining codes

You can now update the function within the functions.js file as done below:

1export default function checkPageStatus(message, user) {
2    if(!("Notification" in window)) {
3      alert("This browser does not support system notifications!")
4    } 
5    else if(Notification.permission === "granted") {
6      sendNotification(message, user)
7    }
8    else if(Notification.permission !== "denied") {
9       Notification.requestPermission((permission)=> {
10          if (permission === "granted") {
11            sendNotification(message, user)
12          }
13       })
14    }
15}

From the code snippet above, the JavaScript Notification API  is used to configure and display notifications to users. It has three properties representing its current state. They are:

  • Denied – notifications are not allowed.
  • Granted – notifications are allowed.
  • Default – The user choice is unknown, so the browser will act as if notifications are disabled. (We are not interested in this)

The first conditional statement (if) checks if the JavaScript Notification API is unavailable on the web browser, then alerts the user that the browser does not support desktop notifications.

The second conditional statement checks if notifications are allowed, then calls the sendNotification function.

The last conditional statement checks if the notifications are not disabled, it then requests the permission status before sending the notifications.

Next, create the sendNotification function referenced in the code snippet above.

1//utils/functions.js
2function sendNotification(message, user) {
3
4}
5export default function checkPageStatus(message, user) {
6  .....
7}

Update the sendNotification function to display the notification’s content.

1/*
2title - New message from Open Chat
3icon - image URL from Flaticon
4body - main content of the notification
5*/
6function sendNotification(message, user) {
7    const notification = new Notification("New message from Open Chat", {
8      icon: "https://cdn-icons-png.flaticon.com/512/733/733585.png",
9      body: `@${user}: ${message}`
10    })
11    notification.onclick = ()=> function() {
12      window.open("http://localhost:3000/chat")
13    }
14}

The code snippet above represents the layout of the notification, and when clicked, it redirects the user to http://localhost:3000/chat.

Congratulations!💃🏻 We’ve been able to display desktop notifications to the user when they send a message. In the next section, you’ll learn how to send alerts to offline users.

💡 Offline users are users not currently viewing the webpage or connected to the internet. When they log on to the internet, they will receive notifications.

How to detect if a user is viewing your web page

In this section, you’ll learn how to detect active users on the chat page via the  JavaScript Page visibility API. It allows us to track when a page is minimized, closed, open, and when a user switches to another tab.

Next, let’s use the API to send notifications to offline users.

Update the sendNotification function to send the notification only when users are offline or on another tab.

1function sendNotification(message, user) {
2    document.onvisibilitychange = ()=> {
3      if(document.hidden) {
4        const notification = new Notification("New message from Open Chat", {
5          icon: "https://cdn-icons-png.flaticon.com/512/733/733585.png",
6          body: `@${user}: ${message}`
7        })
8        notification.onclick = ()=> function() {
9          window.open("http://localhost:3000/chat")
10        }
11      }
12    }  
13}

From the code snippet above, document.onvisibilitychange detects visibility changes, and document.hidden checks if the user is on another tab or the browser is minimised before sending the notification. You can learn more about the different states here.

Next, update the checkPageStatus function to send notifications to all the users except the sender.

1export default function checkPageStatus(message, user) {
2  if(user !== localStorage.getItem("userName")) {
3    if(!("Notification" in window)) {
4      alert("This browser does not support system notifications!")
5    } else if(Notification.permission === "granted") {
6      sendNotification(message, user)
7    }else if(Notification.permission !== "denied") {
8       Notification.requestPermission((permission)=> {
9          if (permission === "granted") {
10            sendNotification(message, user)
11          }
12       })
13    }
14  }     
15}

Congratulations!🎉
You can now send notifications to offline users.

Optional: How to save the messages to a JSON “database” file

In this section, you’ll learn how to save the messages in a JSON file – for simplicity. Feel free to use any real-time database of your choice at this point, and you can continue reading if you are interested in learning how to use a JSON file as a database.

We’ll keep referencing the server/index.js file for the remaining part of this article.

1//index.js file
2const express = require("express")
3const app = express()
4const cors = require("cors")
5const http = require('http').Server(app);
6const PORT = 4000
7const socketIO = require('socket.io')(http, {
8    cors: {
9        origin: "http://localhost:3000"
10    }
11});
12
13app.use(cors())
14let users = []
15
16socketIO.on('connection', (socket) => {
17    console.log(`⚡: ${socket.id} user just connected!`)  
18    socket.on("message", data => {
19      console.log(data)
20      socketIO.emit("messageResponse", data)
21    })
22
23    socket.on("typing", data => (
24      socket.broadcast.emit("typingResponse", data)
25    ))
26
27    socket.on("newUser", data => {
28      users.push(data)
29      socketIO.emit("newUserResponse", users)
30    })
31
32    socket.on('disconnect', () => {
33      console.log('🔥: A user disconnected');
34      users = users.filter(user => user.socketID !== socket.id)
35      socketIO.emit("newUserResponse", users)
36      socket.disconnect()
37    });
38});
39
40app.get("/api", (req, res) => {
41  res.json({message: "Hello"})
42});
43
44
45http.listen(PORT, () => {
46    console.log(`Server listening on ${PORT}`);
47});

Retrieving messages from the JSON file

Navigate into the server folder and create a messages.json file.

1cd server
2touch messages.json

Add some default messages to the file by copying the code below – an array containing default messages.

1{
2   "messages":[
3      {
4         "text":"Hello!",
5         "name":"nevodavid",
6         "id":"abcd01"
7      },
8      {
9         "text":"Welcome to my chat application!💃🏻",
10         "name":"nevodavid",
11         "id":"defg02"
12      },
13      {
14         "text":"You can start chatting!📲",
15         "name":"nevodavid",
16         "id":"hijk03"
17      }
18   ]
19}

Import and read the messages.json file into the server/index.js file by adding the code snippet below to the top of the file.

1const fs = require('fs');
2//Gets the messages.json file and parse the file into JavaScript object
3const rawData = fs.readFileSync('messages.json');
4const messagesData = JSON.parse(rawData);

Render the messages via the API route.

1//Returns the JSON file
2app.get('/api', (req, res) => {
3  res.json(messagesData);
4});

We can now fetch the messages on the client via the ChatPage component. The default messages are shown to every user when they sign in to the chat application.

1import React, { useEffect, useState, useRef} from 'react'
2import ChatBar from './ChatBar'
3import ChatBody from './ChatBody'
4import ChatFooter from './ChatFooter'
5
6const ChatPage = ({socket}) => { 
7  const [messages, setMessages] = useState([])
8  const [typingStatus, setTypingStatus] = useState("")
9  const lastMessageRef = useRef(null);
10
11/**  Previous method via Socket.io */
12  // useEffect(()=> {
13  //   socket.on("messageResponse", data => setMessages([...messages, data]))
14  // }, [socket, messages])
15
16/** Fetching the messages from the API route*/
17    useEffect(()=> {
18      function fetchMessages() {
19        fetch("http://localhost:4000/api")
20        .then(response => response.json())
21        .then(data => setMessages(data.messages))
22      }
23      fetchMessages()
24  }, [])
25
26 //....remaining code
27}
28
29export default ChatPage;

Saving messages to the JSON file

In the previous section, we created a messages.json file containing default messages and displayed the messages to the users.

Here, I’ll walk you through updating the messages.json file automatically after a user sends a message from the chat page.

Update the Socket.io message listener on the server to contain the code below:

1socket.on("message", data => {
2  messagesData["messages"].push(data)
3  const stringData = JSON.stringify(messagesData, null, 2)
4  fs.writeFile("messages.json", stringData, (err)=> {
5    console.error(err)
6  })
7  socketIO.emit("messageResponse", data)
8})

The code snippet above runs after a user sends a message. It adds the new data to the array in the messages.json file and rewrites it to contain the latest update.

Head back to the chat page, send a message, then reload the browser. Your message will be displayed. Open the messages.json file to view the updated file with the new entry.

Conclusion

In this article, you’ve learnt how to send desktop notifications to users, detect if a user is currently active on your page, and read and update a JSON file. These features can be used in different cases when building various applications.

This project is a demo of what you can build with Socket.io; you can improve this application by adding authentication and connecting any database that supports real-time communication.

The source code for this tutorial is available here:
https://github.com/novuhq/blog/tree/main/build-a-chat-app-part-two

Thank you for reading!

Related Posts

category: How to

How to Build a Notion-Like Notification Inbox with Chakra UI and Novu

Learn how to build a Notion-inspired real-time notification inbox in React using Chakra UI and Novu's customizable notification component. Includes code examples, styling tips, and a live demo.

Emil Pearce
Emil Pearce
category: How to

Build a Real-time Notification System with Socket.IO and ReactJS

Learn how to build a real-time notification system in a chat app with ReactJS and Socket.io. This step-by-step guide covers setup, event handling, notifications, and best practices.

Emil Pearce
Emil Pearce
category: How to

Case Study: How Novu Migrated User Management to Clerk

Discover how Novu implemented Clerk for user management, enabling features like SAML SSO, OAuth providers, and multi-factor authentication. Learn about the challenges faced and the innovative solutions developed by our team. This case study provides insights into our process, integration strategies that made it possible.

Emil Pearce
Emil Pearce