DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Building a chat - Browser Notifications with React, Websockets and Web-Push 🀯
Nevo David for novu

Posted on • Originally published at novu.co

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

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

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.

Chat

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

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

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.

import React, {useState} from 'react'

const ChatFooter = ({socket}) => {
    const [message, setMessage] = useState("")
    const handleTyping = () => socket.emit("typing",`${localStorage.getItem("userName")} is typing`)

    const handleSendMessage = (e) => {
        e.preventDefault()
        if(message.trim() && localStorage.getItem("userName")) {
        socket.emit("message", 
            {
            text: message, 
            name: localStorage.getItem("userName"), 
            id: `${socket.id}${Math.random()}`
            }) 
                //Here it is πŸ‘‡πŸ»
        checkPageStatus(message, localStorage.getItem("userName")) 
        }}
        setMessage("")
    }

    //Check PageStatus Function
    const checkPageStatus = () => {

    }

  return (
    <div className='chat__footer'>
        <form className='form' onSubmit={handleSendMessage}>
          <input 
            type="text" 
            placeholder='Write message' 
            className='message' 
            value={message} 
            onChange={e => setMessage(e.target.value)}
            onKeyDown={handleTyping}
            />
            <button className="sendBtn">SEND</button>
        </form>
     </div>
  )
}

export default ChatFooter
Enter fullscreen mode Exit fullscreen mode

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

cd src
mkdir utils
Enter fullscreen mode Exit fullscreen mode

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

cd utils
touch functions.js
Enter fullscreen mode Exit fullscreen mode

Copy the code below into the functions.js file.

export default function checkPageStatus(message, user){

}
Enter fullscreen mode Exit fullscreen mode

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

import React, {useState} from 'react'
import checkPageStatus from "../utils/functions"
//....Remaining codes
Enter fullscreen mode Exit fullscreen mode

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

export default function checkPageStatus(message, user) {
    if(!("Notification" in window)) {
      alert("This browser does not support system notifications!")
    } 
    else if(Notification.permission === "granted") {
      sendNotification(message, user)
    }
    else if(Notification.permission !== "denied") {
       Notification.requestPermission((permission)=> {
          if (permission === "granted") {
            sendNotification(message, user)
          }
       })
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

//utils/functions.js
function sendNotification(message, user) {

}
export default function checkPageStatus(message, user) {
  .....
}
Enter fullscreen mode Exit fullscreen mode

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

/*
title - New message from Open Chat
icon - image URL from Flaticon
body - main content of the notification
*/
function sendNotification(message, user) {
    const notification = new Notification("New message from Open Chat", {
      icon: "https://cdn-icons-png.flaticon.com/512/733/733585.png",
      body: `@${user}: ${message}`
    })
    notification.onclick = ()=> function() {
      window.open("http://localhost:3000/chat")
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

function sendNotification(message, user) {
    document.onvisibilitychange = ()=> {
      if(document.hidden) {
        const notification = new Notification("New message from Open Chat", {
          icon: "https://cdn-icons-png.flaticon.com/512/733/733585.png",
          body: `@${user}: ${message}`
        })
        notification.onclick = ()=> function() {
          window.open("http://localhost:3000/chat")
        }
      }
    }  
}
Enter fullscreen mode Exit fullscreen mode

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.

export default function checkPageStatus(message, user) {
  if(user !== localStorage.getItem("userName")) {
    if(!("Notification" in window)) {
      alert("This browser does not support system notifications!")
    } else if(Notification.permission === "granted") {
      sendNotification(message, user)
    }else if(Notification.permission !== "denied") {
       Notification.requestPermission((permission)=> {
          if (permission === "granted") {
            sendNotification(message, user)
          }
       })
    }
  }     
}
Enter fullscreen mode Exit fullscreen mode

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.

//index.js file
const express = require("express")
const app = express()
const cors = require("cors")
const http = require('http').Server(app);
const PORT = 4000
const socketIO = require('socket.io')(http, {
    cors: {
        origin: "http://localhost:3000"
    }
});

app.use(cors())
let users = []

socketIO.on('connection', (socket) => {
    console.log(`⚑: ${socket.id} user just connected!`)  
    socket.on("message", data => {
      console.log(data)
      socketIO.emit("messageResponse", data)
    })

    socket.on("typing", data => (
      socket.broadcast.emit("typingResponse", data)
    ))

    socket.on("newUser", data => {
      users.push(data)
      socketIO.emit("newUserResponse", users)
    })

    socket.on('disconnect', () => {
      console.log('πŸ”₯: A user disconnected');
      users = users.filter(user => user.socketID !== socket.id)
      socketIO.emit("newUserResponse", users)
      socket.disconnect()
    });
});

app.get("/api", (req, res) => {
  res.json({message: "Hello"})
});


http.listen(PORT, () => {
    console.log(`Server listening on ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Retrieving messages from the JSON file

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

cd server
touch messages.json
Enter fullscreen mode Exit fullscreen mode

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

"messages": [
        {
           "text": "Hello!",
           "name": "nevodavid",
           "id": "abcd01" 
        }, {
            "text": "Welcome to my chat application!πŸ’ƒπŸ»",
           "name": "nevodavid",
           "id": "defg02" 
        }, {
            "text": "You can start chatting!πŸ“²",
           "name": "nevodavid",
           "id": "hijk03" 
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

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.

const fs = require('fs');
//Gets the messages.json file and parse the file into JavaScript object
const rawData = fs.readFileSync('messages.json');
const messagesData = JSON.parse(rawData);
Enter fullscreen mode Exit fullscreen mode

Render the messages via the API route.

//Returns the JSON file
app.get('/api', (req, res) => {
  res.json(messagesData);
});
Enter fullscreen mode Exit fullscreen mode

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.

import React, { useEffect, useState, useRef} from 'react'
import ChatBar from './ChatBar'
import ChatBody from './ChatBody'
import ChatFooter from './ChatFooter'

const ChatPage = ({socket}) => { 
  const [messages, setMessages] = useState([])
  const [typingStatus, setTypingStatus] = useState("")
  const lastMessageRef = useRef(null);

/**  Previous method via Socket.io */
  // useEffect(()=> {
  //   socket.on("messageResponse", data => setMessages([...messages, data]))
  // }, [socket, messages])

/** Fetching the messages from the API route*/
    useEffect(()=> {
      function fetchMessages() {
        fetch("http://localhost:4000/api")
        .then(response => response.json())
        .then(data => setMessages(data.messages))
      }
      fetchMessages()
  }, [])

 //....remaining code
}

export default ChatPage
Enter fullscreen mode Exit fullscreen mode

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:

socket.on("message", data => {
  messagesData["messages"].push(data)
  const stringData = JSON.stringify(messagesData, null, 2)
  fs.writeFile("messages.json", stringData, (err)=> {
    console.error(err)
  })
  socketIO.emit("messageResponse", data)
})
Enter fullscreen mode Exit fullscreen mode

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

Help me out!

If you feel like this article helped you understand WebSockets better! 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
Help

Thank you for reading!

Top comments (35)

Collapse
joelbonetr profile image
JoelBonetR

Love it! 😁

Collapse
nevodavid profile image
Nevo David Author

Thank you Joel! ❀️
How are you this fine Monday? :)

Collapse
joelbonetr profile image
JoelBonetR

Just finished an implementation and waiting for the pipeline to finish. Hopefully I'll be out of the computer in few minutes πŸ˜†

Thread Thread
nevodavid profile image
Nevo David Author

Hopefully you won't get any errors, that can be all night πŸ’€

Thread Thread
joelbonetr profile image
JoelBonetR

I can confirm it works! πŸ˜‚

Now I can go buy some groceries to survive one day more.

For today I only need to move a sideproject from a VPS to another, change the CI config, test it and hopefully, I'll be free for dinner. πŸ˜…

Collapse
nevodavid profile image
Nevo David Author

The next article I am going to write about how to make the dev.to bell icon! What do you think?

Collapse
combarnea profile image
Tomer Barnea

Ohhhh yeah! :)

Collapse
nevodavid profile image
Nevo David Author

πŸš€

Collapse
vaibhavkhulbe profile image
Vaibhav Khulbe

Nice work! This series is going to be really exciting 🀩

Collapse
nevodavid profile image
Nevo David Author

I am excited too! What should I write about next?

Collapse
aycom366 profile image
Ay

thanks for this great article, I really learned alot
Please can you write on how to build a no code tool project??

basically starting from creating the widget, it can be anything
after this, we push the package to npm to be used by any frontend framework, react,vue e.t.c

I would be glad if you consider my request. Thanks once again

Collapse
nevodavid profile image
Nevo David Author

New article!!
I implemented the Dev Community Notification Center with React, Novu and Websockets πŸ”₯

dev.to/novu/i-implemented-the-dev-...

Collapse
toan1606 profile image
Toan1606

Thanks. But I have some questions.

  1. Is Your solution for chat and real time the best ?
  2. I'm a Java Developer so I can do it in Java instead of React. So Can you compare between them ?

Thanks

Collapse
nevodavid profile image
Nevo David Author • Edited on

Hi Toan1606, Thank you for asking.

  1. There are many ways to write it with different libraries and different languages, but the main goal is to stick to the browser limitation. If you want two way communication between the browser and the server you have to use websockets. You can find more information about it on the first article.

  2. Unfortunately, you can't use Java for the frontend. You can change React to any other frontend or just a vanilla js. You can definetly replace Java with node.js for the websockets. I am not a big Java expert but a quick google brought this:
    github.com/TooTallNate/Java-WebSocket

Collapse
toan1606 profile image
Toan1606

Thanks. I will learn React and using my next project.

Collapse
bobbyiliev profile image
Bobby Iliev

This is awesome!

Collapse
nevodavid profile image
Nevo David Author

You are awesome!
How are you!!??

Collapse
rogue_rm_ profile image
RΙ™bbi

It is great. I'm interested that, are there any sounds for this notifications?

Collapse
nevodavid profile image
Nevo David Author

Hi Rabbi! Thank you for asking, I think there is sound by default and you can make it silent if you want.

Feel free to look at all the properties here:
developer.mozilla.org/en-US/docs/W...

Collapse
obaino82 profile image
Obaino82

Nice one

Collapse
nevodavid profile image
Nevo David Author

Thank you Obaino82, Have you used browser notifications before?

Collapse
nevodavid profile image
Nevo David Author

Thank you Steven!
How are you? :)

Collapse
nevodavid profile image
Nevo David Author

Have you ever used browser notifications before?

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
mateuszlatka9 profile image
Mateusz Łątka

test2

Collapse
nevodavid profile image
Nevo David Author

test3

Collapse
attaullahkhanit profile image
Attaullahkhanit

It's amazing

Collapse
browncoder profile image
Mahendra Bishnoi

where's completed app source code & deployed version πŸ₯Ί

Collapse
hasannaqvi83 profile image
hasannaqvi83

Wonderful article @nevodavid
Just wondering if notification component can be used in a SharePoint app developed using SharePoint framework.

Collapse
nevodavid profile image
Nevo David Author

Hi @hasannaqvi83 do you refer to Novu or the Article? :)

Collapse
fernandocavill profile image
fernandocavill

Great!
How about the performance if 10,000 people are online at the same time.

Collapse
nevodavid profile image
Nevo David Author

Hi @fernandocavill,
It can hold even more, it all depends on the Devops.
If you are referring to one server, and concurrency of 10,000 people, it will probably not hold πŸ˜†

Collapse
fsappdev profile image
fsappdev

this is so helpful, thanks!!

Collapse
sindoudedv profile image
Sindou KONE

Add to the discussion

Thank you.

Β 
Thanks for visiting DEV, we’ve worked really hard to cultivate this great community and would love to have you join us. If you’d like to create an account, you can sign up here.