Add Push Notifications to Your React Chat App

Posted on October 12, 2019


Push notifications are an important feature of any modern chat application. Notifications ensure your messages are the first users see when they pick up their device and that your product is always front of mind.

In this tutorial, you will first learn to build a simple React group chat application before enabling push notifications with CometChat and the CometChat Push notifications extension. Here’s a sneak peak of what you’ll build:

Showing a push notification

If you’d like to jump straight into code, you can grab the complete code on GitHub.

Creating CometChat Account

CometChat is a developer tool that enables you to add text, voice, and video chat to your web and mobile applications. Aside from core chat features like groups, real-time messaging, and typing indicators, CometChat provides a multitude of powerful extensions which leverage other services. In this tutorial, we’ll be using the Firebase push notifications extension. You’re going to love how seamless it is to enable. First, though, you’ll need to create a free CometChat account.

Head to the dashboard and create your free account. Then, create a CometChat app to connect to its services. From the dashboard, enter your app name in the Add New App box and hit the + icon.

I called my application "react-chat-pn" but feel free to name it as you wish.

Adding a new CometChat app

Hold your horses ⚠️🐴!
To follow this tutorial or run the example source code you'll need to create a V1 application.
cometchatv1 v2
v2 will be out of beta soon at which point we will update this tutorial.

Once done, you’ll be taken into the dashboard of your app, preloaded with default users and API key that you can immediately use. Go ahead and click on API Keys. From here, take note of your API key and App ID :

CometChat API Key and App ID

And you’re done setting up CometChat. Next up is creating your chat app.

Setting up React application

Rather than creating your React application from scratch, you can clone or download the starter code required for this tutorial here. Don’t worry, all it contains are just images, style and packages required to make an awesome chat app. You will still code everything from scratch. Let’s hit the ground running by using the command line npm install.

While you wait for the installation to finish, here is a short explanation of the project files:

  • The src/assets folder includes svg images that you’re going to use to beautify the app
  • The src/components folder includes minimal placeholder for the app. There’s the Login and Chat component inside, which you will improve later on
  • src/App.css covers the styling of the app. This way you don’t need to write any styling
  • src/App.js is where the app will be initialized. CometChat and Firebase will be called from here
  • src/index.js serve as the entry point of the app

There are also some extra packages in this repo, they are:

  • @cometchat-pro/chat contains the JavaScript SDK of CometChat
  • react-md-spinner a small library to create spinner when loading chat messages
  • react-notifications create a toast notification, used in Login component later
  • react-router-dom takes care of routing from one React component to another

Once your installation is finished, rename .env-example file to .env and edit its content with your CometChat credentials:

REACT_APP_COMETCHAT_API_KEY=YOUR_API_KEY
REACT_APP_COMETCHAT_APP_ID=YOUR_APP_ID
REACT_APP_COMETCHAT_GUID=supergroup

Copy and paste your CometChat Api Key to REACT_APP_COMETCHAT_API_KEY and your App ID to REACT_APP_COMETCHAT_APP_ID. You will reference them from the app later.

You can run npm start and test your React app. It should run without any problem. Of course it doesn’t do anything interesting yet. Let’s change it.

Building React components

Before you code the components, let’s take a look at the content of App.js file:

import React from 'react';
import {CometChat} from '@cometchat-pro/chat';
import {BrowserRouter as Router, Route} from 'react-router-dom';
import {NotificationContainer} from 'react-notifications';

import './App.css';
import 'react-notifications/lib/notifications.css';
import Login from './components/Login';
import Chat from './components/Chat';

CometChat.init(process.env.REACT_APP_COMETCHAT_APP_ID);

const App = () => {
  return (
    <Router>
      <React.Fragment>
        <NotificationContainer />
        <Route exact path='/' component={Login} />
        <Route path='/login' component={Login} />
        <Route path='/chat' component={Chat} />
      </React.Fragment>
    </Router>
  );
};

export default App;

This App component will act as the container of the entire chat app. CometChat is initialized with CometChat.init function, then three routes are declared inside the Router component, one for default /, one for /login and the last for /chat route.

The default and /login route will render the Login component, while /chat will render Chatcomponent. NotificationContainer component will be responsible for displaying toast notification on the authentication process.

We’ll take care of these two components and complete the app before we work on push notification, so let’s start with editing the Login component.

As its name implies, the primary job of Login component is to log the user into CometChat server, so that the user can send and receive message from CometChat.

First, we define the component, the layout and a simple form. The handleLogin function is the most important one for this component, and we’ll work on it next. For now, write down the component like this:

import React from 'react';
import { CometChat } from '@cometchat-pro/chat';
import { NotificationManager } from 'react-notifications';
import loginIllustration from '../assets/login-illustration.svg';

class Login extends React.Component {
  state = {
    username: '',
    isLoading: false,
  };

  handleLogin = event => {
    event.preventDefault();
    console.log("Login handler");
  };

render() {
  const {username, isLoading} = this.state;
  const loadingSpinner = isLoading? <span className="fa fa-spin fa-spinner"/> :'';
  return (
      <div className='login-page'>
        <div className='login'>
          <div className='login-container'>
            <div className='login-form-column'>
              <form>
                <h3>Welcome!</h3>
                <p>
                  Login with the username "superhero1" or "superhero2" to test this React-CometChat application. To create your own user, visit{' '}
                  <a href='https://prodocs.cometchat.com/reference#createuser'>
                    our documentation
                  </a>
                </p>
                <div className='form-wrapper'>
                  <label>Username</label>
                  <input
                    type='text'
                    name='username'
                    id='username'
                    placeholder='Enter your username'
                    value={username}
                    onChange={event => this.setState({ username: event.target.value })}
                    className='form-control'
                    required
                  />
                </div>
                <button type='submit' onClick={this.handleLogin} disabled={isLoading}>
                  LOG IN {loadingSpinner}
                </button>
              </form>
            </div>
            <div className='login-image-column'>
              <div className='image-holder'>
                <img src={loginIllustration} alt='Login illustration' />
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }
}

export default Login

In the snippet above, we define a form which asks the user for their username. Once submitted, handleSubmit is called and we activate the loading spinner. To test if our form works, we print the username to the console.

Of course, we actually want to check and login the user, so let’s do that now. Replace handleLogin code with the following snippet:

handleLogin = event => {
  event.preventDefault();
  const { username } = this.state;
  if(!username){
    NotificationManager.error('Username must not be empty', 'Login Failed');
    return;
  }
  this.setState({
    isLoading: true
  })
  CometChat.login(username, process.env.REACT_APP_COMETCHAT_API_KEY).then(
    user => {
      NotificationManager.success('You are now logged in', 'Login Success');
      this.setState({ username: '', isLoading: false });
      this.props.history.push({
        pathname: '/chat',
        state: { user }
      })
    },
    error => {
      NotificationManager.error('Username is not registered', 'Login Failed');
      this.setState({
        isLoading: false
      })
    }
  );
};

First, we check if username is not empty. If it is, we call NotificationManager.error to show an error notification on the top right corner of the screen. The NotificationManager function is imported from react-notifications module.

Showing error notification to user

When username is not empty, isLoading state is set to true, which cause React to show a loading indicator and disable the submit button, to prevent multiple submission.

The function then calls the CometChat.Login function, sending username and CometChat API key as its arguments. If the function returns an error, we use the same NotificationManager I introduced you to earlier to show another error. Since all is good from our side, the most likely reason for this error is the username is not registered. **

If the username is registered, CometChat will return a success response and a user object. We’ll use NotificationManager.success function to show a Login Success notification, while at the same time React-Router will redirect the browser into /chat route and pass the user object.

Go ahead and try the app, you should be able to login and get redirected to Chat component.

Building the Chat component

With the Login component finished, we can move on into Chat component. Let’s start by defining the states:

import React from 'react';
import {CometChat} from '@cometchat-pro/chat';
import MDSpinner from 'react-md-spinner';
import emptyChatImage from '../assets/empty-state.svg';
const {REACT_APP_COMETCHAT_GUID} = process.env;

class Chat extends React.Component {
  constructor(props) {
    super();
    this.state = {
      message: '',
      chat: [],
      isLoading: true,
      user: props.location.state? props.location.state.user : '',
    };
  }
}

export default Chat;

By now, the snippet above should be clear for you, but let me explain the use of these states first.

  • The message state is used to hold the value of user input
  • chat will store an array of text message objects. We will display them later as chat bubbles
  • isLoading will be used to display and remove a loading indicator while the chat component is fetching previous messages
  • user to identify messages sent by the currently logged in user

Alright, now let’s create a componentDidMount function to fetch any previous messages send to this chat room:

componentDidMount() {
  var messagesRequest = new CometChat.MessagesRequestBuilder()
    .setGUID(REACT_APP_COMETCHAT_GUID)
    .setLimit(100)
    .build();

  messagesRequest.fetchPrevious().then(
    messages => {
      this.setState({
        chat: messages,
        isLoading: false,
      });
    },
    error => {
      console.log('Message fetching failed with error:', error);
    }
  );
}

In the snippet above, we created an object of MessagesRequestBuilder class, which is provided by CometChat to fetch messages in bulk. Once the messages are received from CometChat, we assign them into chat state and remove the loading indicator from screen. By implementing this code, the user will see a handful of previous messages instead of an empty chat room whenever they login into the app.

Another thing you need to do is to set a CometChat message listener that will listen for any incoming message in real-time. Once a message is received, push it into the chat state array:

componentDidMount() {
  ...

  CometChat.addMessageListener(
    'MESSAGE_LISTENER_KEY',
    new CometChat.MessageListener({
      onTextMessageReceived: message => {
        var {chat} = this.state;
        console.log('Incoming Message Log', {message});
        chat.push(message);
        this.setState({
          chat
        });
      },
    })
  );

}

So far so good. Let’s create a handleSendMessage that will send user input as a new message to CometChat server.

First, we need to create a TextMessage object using the new CometChat.TextMessage() function.
Through the documentation, we learned that the function requires us to specify several mandatory parameters as follows:

  • receiverID: This is to identify the recipient of the message. We will use the GUID imported from .env.
  • messageText: The text message that needs to be sent to the server, taken from our state
  • messageType: The type of message, in our case is a text
  • receiverType: The type of user that will receive the message. In our case, it’s a group

Finally, we send the message using CometChat.sendMessage function. Once successfully send, we will push the message into chat state array, so that the newly sent message is displayed in the chat box.

handleSendMessage = event => {
    event.preventDefault();
    const {message} = this.state;
    var textMessage = new CometChat.TextMessage(
      REACT_APP_COMETCHAT_GUID,
      message,
      CometChat.MESSAGE_TYPE.TEXT,
      CometChat.RECEIVER_TYPE.GROUP
    );
    CometChat.sendMessage(textMessage).then(
      message => {
        const { chat } = this.state;
        chat.push(message);
        this.setState({
          chat
        })
        console.log('Message sent successfully:', message);
      },
      error => {
        console.log('Message sending failed with error:', error);
      }
    );
    this.setState({
      message: '',
    });
  };

Now that we’ve taken care about the logic, let’s write the render function to actually display a chat room that we can interact with. This is where we map the value of chat state to create message bubbles. We will also create a simple text input for typing the message:

render() {
  var {chat, message, isLoading, user} = this.state;
  var chatContent = (
    <div className='loading-messages-container'>
      <MDSpinner size='100' />
      <span className='loading-text'>Loading Messages</span>
    </div>
  );
  if (!isLoading && !chat.length) {
    chatContent = (
      <div className='text-center img-fluid empty-chat'>
        <div className='empty-chat-holder'>
          <img src={emptyChatImage} className='img-res' alt='empty chat' />
        </div>
        <div>
          <h2> No new message? </h2>
          <h6 className='empty-chat-sub-title'>
            Send your first message below.
          </h6>
        </div>
      </div>
    );
  } 
  else if (!isLoading && chat.length) {
    chatContent = chat.map(chat => {
      var isUser = user.uid === chat.sender.uid;
      var renderName;
      if (isUser) {
        renderName = null;
      } else {
        renderName = (
          <div className='sender-name'>{chat.sender.name}</div>
        );
      }
      return (
        <div key={chat.id} className='chat-bubble-row' style={{flexDirection: isUser ? 'row-reverse' : 'row'}}>
            <img src={chat.sender.avatar} alt='sender avatar' className='avatar' style={isUser ? {marginLeft: '15px'} : {marginRight: '15px'}} />
            <div className={`chat-bubble ${isUser? 'is-user':'is-other'}`}>
              {renderName}
              <div className='message' style={{color: isUser ? '#FFF' : '#2D313F'}}>
                {chat.text}
              </div>
            </div>
        </div>
      )
    });
  }
  return (
    <div className='chat-container'>
      <div className='chat'>
        <div className='container'>
          <div className='chat-header'>
            <div className='active'>
              <h5>Chat</h5>
            </div>
          </div>
          <div className='chat-page'>
                <div className='msg-page'>
                  {chatContent}
                  <div id='endofchat'></div>
                </div>
                <div className='msg-footer'>
                  <form
                    className='message-form'
                    onSubmit={this.handleSendMessage}>
                    <div className='input-group'>
                      <input
                        type='text'
                        className='form-control message-input'
                        placeholder='Type something'
                        value={message}
                        onChange={ event => this.setState({ message: event.target.value }) }
                        required
                      />
                    </div>
                  </form>
                </div>
          </div>
        </div>
      </div>
    </div>
  );
}

That’s a bit long for a snippet! Let me break down what it does for you.

Basically, this render function will have 3 dynamic views which gets rendered depending on the values of local state at the moment. The first one is a loading view, where we show a spinner imported from md-spinner package to indicate that the chat app is fetching previous messages from CometChat:

Loading messages view

Once the fetch process is done, we’re going to check if the chat array holds any message for us to display. If there is none, we will display a simple no message illustration and ask the user to send his or her first message:

No messages view

Finally, when we have some messages from chat array to render, we display the chat messages for users to view:

A chat room view with previous messages

The Chat component is ready for testing, but let’s add a few improvements to make it perfect. First, you can get the app to scroll to the latest message by using a simple scrollIntoView function. Call this function from componentDidUpdate function, so that it will be called each time the state changes. :

componentDidUpdate() {
  this.scrollToBottom();
}

scrollToBottom = () => {
  const chat = document.getElementById("endofchat");
  chat.scrollIntoView();
}

If you found any error while following the tutorial, you can compare your Chat component code with the one from here.

By now, your chat application should be working perfectly. Go take a short break for now. When you’re ready, it’s time to add the push notification feature!

Creating a Firebase project

We’ll need to make use of Firebase cloud messaging (FCM) in order to add push notification for this chat app, so head over to Firebase website and create a new account if you haven’t had one.

Once inside Firebase console, add a new project by clicking on the + symbol:

Creating a new Firebase project

Then inside your newly created Firebase project, add a new web app by clicking on the </> symbol:

Adding a web app to Firebase

Once the web app is added, you will be shown a Firebase config snippet like this:

Firebase config snippet

Take note of this config, because you will need it later. For now, let’s move on into CometChat dashboard.

Enabling CometChat push notification extension

To enable push notification feature from CometChat, you need to enable the push notification extension from CometChat dashboard, so let’s head back to where you grab CometChat API key and click on the Extensions menu. Add Push Notification from the extensions list:

Adding push notification extension

Once the extension is enabled, grab the FCM server key from the screenshot below:

Getting Firebase server key

And paste it into the extension by clicking on Actions → Settings:

Saving FCM server key inside CometChat

Now CometChat will be able to make use of FCM. Let’s update our chat app to make use of it.

Adding Firebase messaging to your chat app

Now we need to install Firebase SDK package to the React app. Let’s do another npm install:

npm install firebase

And create a firebase.js file inside our src directory. Here, we will write a function that initialize a firebase instance:

import firebase from 'firebase/app';
import 'firebase/messaging';

export function initializeFirebase() {
  if (firebase.messaging.isSupported()) {
    const config = {
      // your firebase config here
    };
    const init = firebase.initializeApp(config);
    
  }
}

Put your Firebase web config into the config constant above and import initializeFirebase function in your App.js file. We’ll call the function from there:

import {initializeFirebase} from './firebase';

CometChat.init(process.env.REACT_APP_COMETCHAT_APP_ID);
initializeFirebase();

// ... rest of the code

With the snippet above, a new Firebase instance will be initialized when the app is running.

Now you need to request permission from the user to display push notifications. The messaging.requestPermission function is used to do just that. In the snippet below, we’ll ask for the permission to send push notification, once allowed, we can retrieve a token by using the messaging.getToken function. I will show you what to do with it later, so let’s focus on getting the permission first:

const messaging = init.messaging();

messaging.requestPermission()
    .then(() => {
        console.log("Have Permission");
        return messaging.getToken();
    })
    .then(token => {
        console.log("FCM Token:", token);
        //Do something with Token like subscribe to topics
    })
    .catch(error => {
        if (error.code === "messaging/permission-blocked") {
            console.log("Please Unblock Notification Request Manually");
        } else {
            console.log("Error Occurred", error);
        }
    });

Now that we can get the token, let’s figure out what to do with it. We are going to use the token to subscribe to a topic.

What’s a topic, you asked? It’s a feature from FCM that allows you to send messages to all devices that subscribed to a particular topic.

You can view the documentation of topic based messaging if you want to, but in a nutshell, you need to send a POST request to subscribe to the current chat room topic using the format of APPID_receiverType_ReceiverId as shown in CometChat documentation. The complete code for topic subscription is as follows:

.then(token => {
  console.log('FCM Token:', token);
  var userType = 'group';
  var UID = process.env.REACT_APP_COMETCHAT_GUID;
  var appId = process.env.REACT_APP_COMETCHAT_APP_ID;
  var topic = appId + '_' + userType + '_' + UID;
  var url = 'https://ext-push-notifications.cometchat.com/fcmtokens/' +
    token + '/topics/' + topic;

  fetch(url, {
    method: 'POST',
    headers: new Headers({
      'Content-Type': 'application/json',
    }),
    body: JSON.stringify({appId: appId}),
  })
  .then(response => {
    if (response.status < 200 || response.status >= 400) {
      console.log(
        'Error subscribing to topic: ' +
          response.status +
          ' - ' +
          response.text()
      );
    }
    console.log('Subscribed to "' + topic + '"');
  })
  .catch(error => {
    console.error(error);
  });
})

Once your browser is subscribed to the topic, it will receive a message object each time a chat message is being sent to the chat room.

You can handle that message by using the messaging.onMessage function from Firebase messaging:

messaging.onMessage(function(payload) {
  var sender = JSON.parse(payload.data.message);
  console.log('Receiving foreground message', sender);
  // Customize notification here
    var notificationTitle = 'New CometChat message';
    var notificationOptions = {
      body: payload.data.alert,
      icon: sender.data.entities.sender.entity.avatar,
    };
    var notification = new Notification(
      notificationTitle,
      notificationOptions
    );
    notification.onclick = function(event) {
      //handle click event onClick on Web Push Notification
      notification.close();
    };
});

This function will be called each time Firebase received a message from CometChat, and using the data contained in the payload, you can form a new push notification by using the Notification API.

And now you can go ahead and try the app. Allow the permission request for push notifications and then send any text message. You should be receiving push notifications. Awesome!

Preventing push notification from self

You might notice that the browser received a push notification even though you are the one sending the mesage. To prevent push notification when sending a message, you need to update the messaging.onMessage function to check if the sender is actually the currently logged in user.

To do that, create a variable to store the user uid inside firebase.js file. You can call it meUid:

let meUid = ''

export function initializeFirebase() {
  ...
}

Now create a function to update its value:

export function updateFirebaseLoggedInUser(uid) {
  if (firebase.messaging.isSupported()) {
    meUid = uid;
  }
}

Next, update the messaging.onMessage function to check the value of sender uid and compare it to meUid so that it will only send push notification from other users:

messaging.onMessage(function(payload) {
  const sender = JSON.parse(payload.data.message);
  console.log('Receiving foreground message', sender);
  // check the uid
  if (sender.data.entities.sender.entity.uid !== meUid) {
    const notificationTitle = 'New CometChat message';
    const notificationOptions = {
      body: payload.data.alert,
      icon: sender.data.entities.sender.entity.avatar,
    };
    const notification = new Notification(
      notificationTitle,
      notificationOptions
    );
    notification.onclick = function(event) {
      notification.close();
    };
  }
});

Finally, call the updateFirebaseLoggedInUser function from inside the componentDidMount function of Chat component:

import {updateFirebaseLoggedInUser} from './../firebase';

//...

componentDidMount() {
  updateFirebaseLoggedInUser(this.state.user.uid);
  //...
}

With this, no push notification will be sent when the new message is your own. To try it out, open the app with two different browsers (Chrome and Firefox) and login with different users. Only the receiving browser will send you a push notification. Very nice!

Enabling Firebase background listener

If you minimize one of your web browser and send a message from the other browser, you’ll notice that the minimized browser won’t receive any notification. This is because push notification isn’t enabled yet when your browser is put on the background.

To enable push notification from apps that are put in the background, you need to create a service worker. If you need to learn more about service worker, visit this Google Developer Docs.

As for the app, all you need to do is create a new JavaScript file named firebase-messaging-sw.js inside your public/ directory with the following content:

importScripts("https://www.gstatic.com/firebasejs/6.4.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/6.4.0/firebase-messaging.js");

if (firebase.messaging.isSupported()) {
  var config = {
    // your firebase messagingSenderId here
  };
  firebase.initializeApp(config);
  const messaging = firebase.messaging();
  messaging.setBackgroundMessageHandler(function(payload) {
    console.log(' Received background message ', payload);
    var sender = JSON.parse(payload.data.message);
    var notificationTitle = 'New CometChat message';
    var notificationOptions = {
      body: payload.data.alert,
      icon: sender.data.entities.sender.entity.avatar,
    };
    return self.registration.showNotification(
      notificationTitle,
      notificationOptions
    );
  });
  self.addEventListener('notificationclick', function(event) {
    event.notification.close();
  });
}

Because you have created the foreground listener, I’m pretty sure you understand what this code is doing. It simply set a background message listener using messaging.setBackgroundMessageHandler function. When a message is received by the listener, it will use the payload received to create a push notification and show it to the user.

Now try sending messages again. You will receive push notifications even if you minimize the browser. Great work!

Conclusion

You have now learned how to add push notification feature to your React chat app. You also learned about CometChat and how easy it is to build a custom chat app using its chat SDK. Congratulations on making it this far!

Don’t forget to checkout CometChat documentation for advanced features such as sending image and voice chat here.

React Distilled 2.0 is released

If you'd like to learn more about React and how you can use it to build a complete web application from scratch, I'm offering a 28% off my book React Distilled to celebrate its release (from $49 to $34). 

It includes new chapters on React Context API and React Hooks, and it shows how you can create React app using nothing but React and Firestore.

Share this:
LinkedIn
Reddit
WhatsApp

Get Free Guides