In any chat application, one of the most integral features is being able to notify users of new messages. While providing this feature comes with its own set of challenges (listeners, timing, notification types, keeping track of which messages are read, etc.), notifying users of unread messages doesn't have to be challenging.
Among the many ways in which building a chat app/feature becomes infinitely easier when building with Stream Chat is the fact that the capabilities for displaying notifications for various actions are built right into the service; the only thing you need to do is hook it up with the notification system of the platform you’re building for!
In this article, we’ll explore how you can display notifications when a user receives a new message. We’ll talk about how to prompt the user to enable notifications, then show how to listen for events that we can then hook into for notifications.
Here’s a live demo of what we’ll be building:
Prerequisites
To follow along with this tutorial, you'll need to have Node.js and npm installed on your computer. You'll also need to have some experience with building React applications.
Logging In to Stream
Create a free Stream account or sign in to your existing account here. Once you’re logged in, create a new application and take note of your App Access Keys which we’ll be making use of shortly.
Setting Up Your React App
Open the terminal on your computer and run the following command to bootstrap a new React application:
1npx create-react-app stream-web-notifications
Once the command finishes running, cd
into the newly created directory and install the following dependencies, which will be needed in the course of fleshing out our demo application:
1$ npm install express cors dotenv body-parser random-username-generator stream-chat stream-chat-react axios
After the dependencies are installed, you can start the development server using npm start
which should open it up on http://localhost:3000!
Setting Up Your Server
Before we can build the application frontend, we need to set up an Express server for the purpose of creating users on our Stream instance and resolving special tokens, which are used to authenticate the user.
Create a new server.js
in the root of your project directory and populate it with the following code:
// server.js require('dotenv').config(); const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const { StreamChat } = require('stream-chat'); const app = express(); app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); // initialize Stream Chat SDK const serverSideClient = new StreamChat( process.env.STREAM_API_KEY, process.env.STREAM_APP_SECRET ); app.post('/join', async (req, res) => { const { username } = req.body; const token = serverSideClient.createToken(username); try { await serverSideClient.updateUser( { id: username, name: username, }, token ); } catch (err) { console.log(err); } const admin = { id: 'admin' }; const channel = serverSideClient.channel('team', 'group-chat', { name: 'Talk to me', created_by: admin, }); try { await channel.create(); await channel.addMembers([username, 'admin']); } catch (err) { console.log(err); } return res .status(200) .json({ user: { username }, token, api_key: process.env.STREAM_API_KEY }); }); app.listen(7000, () => { console.log(`Server running on PORT 7000`); });
The code above allows us to receive a username
from the client, and create the user
on our Stream chat instance, passing in the token for the user
. The token is also included in the response, allowing us to use it to initialise the user in our React application.
Before we can start the server, we need to set some environmental variables. Create a .env
file in the project and paste your Stream application credentials (which you grabbed when you logged in to your account) into the file in the following format:
// .env STREAM_API_KEY=<your api key> STREAM_APP_SECRET=<your app secret>
Now go ahead and start your server on port 7000
with node server.js
!
Building Your Application Frontend
Our application code will be quite simple, thanks to our use of Stream’s React components, which enable us to compose a rich chat interface with only a few lines of code. Here’s all the code we'll need to build out the UI for our chat app:
// src/App.js import React, { useState, useEffect } from 'react'; import './App.css'; import { Chat, Channel, ChannelHeader, Thread, Window, ChannelList, ChannelListTeam, MessageList, MessageTeam, MessageInput, } from 'stream-chat-react'; import { StreamChat } from 'stream-chat'; import rug from 'random-username-generator'; import axios from 'axios'; import 'stream-chat-react/dist/css/index.css'; let chatClient; function App() { const [channel, setChannel] = useState(null); useEffect(() => { const username = rug.generate(); async function getToken() { try { const response = await axios.post('http://localhost:7000/join', { username, }); const { token } = response.data; const apiKey = response.data.api_key; chatClient = new StreamChat(apiKey); chatClient.setUser( { id: username, name: username, }, token ); const channel = chatClient.channel('team', 'group-chat'); await channel.watch(); setChannel(channel); } catch (err) { console.log(err); return; } } getToken(); }, []); if (channel) { return ( <Chat client={chatClient} theme="team dark"> <ChannelList options={{ subscribe: true, state: true, }} List={ChannelListTeam} /> <Channel channel={channel}> <Window> <ChannelHeader /> <MessageList Message={MessageTeam} /> <MessageInput focus /> </Window> <Thread Message={MessageTeam} /> </Channel> </Chat> ); } return <div></div>; } export default App;
And there you go... With just the lines of code above, we have a fully responsive chat UI that supports advanced chat functionality, such as typing indicators, emoji, reactions, file uploads, rich link preview, user presence and more!
With this code, we’re generating a random username
each time the page is loaded, so we don’t have to go through a complicated sign-in process to see the app in action. This also makes it easy to spawn multiple users quickly and to simulate a chat session between them.
In the next section, we’ll consider how to display notifications when messages are sent and received between users.
Enabling Browser Notifications for New Messages
Let’s prompt the user to enable browser notifications by displaying a banner at the top of the page, similarly to the way it’s done in Slack. Update the code in src/App.js
as follows:
// src/App.js import React, { useState, useEffect } from 'react'; import './App.css'; import { Chat, Channel, ChannelHeader, Thread, Window, ChannelList, ChannelListTeam, MessageList, MessageTeam, MessageInput, } from 'stream-chat-react'; import { StreamChat } from 'stream-chat'; import rug from 'random-username-generator'; import axios from 'axios'; import 'stream-chat-react/dist/css/index.css'; let chatClient; function App() { const [channel, setChannel] = useState(null); const [showNotificationBanner, setShowNotificationBanner] = useState(false); useEffect(() => { const username = rug.generate(); async function getToken() { try { const response = await axios.post('http://localhost:7000/join', { username, }); const { token } = response.data; const apiKey = response.data.api_key; chatClient = new StreamChat(apiKey); chatClient.setUser( { id: username, name: username, }, token ); const channel = chatClient.channel('team', 'group-chat'); await channel.watch(); setChannel(channel); } catch (err) { console.log(err); return; } } getToken(); if ( window.Notification && (Notification.permission === 'granted' || Notification.permission === 'denied') ) return; setShowNotificationBanner(true); }, []); function grantPermission() { if (Notification.permission === 'granted') { new Notification('You are already subscribed to web notifications'); return; } if ( Notification.permission !== 'denied' || Notification.permission === 'default' ) { Notification.requestPermission().then(result => { if (result === 'granted') { new Notification('New message from Stream', { body: 'Nice, notifications are now enabled!', }); } }); } setShowNotificationBanner(false); } if (channel) { return ( <Chat client={chatClient} theme="team dark"> {showNotificationToast && ( <div class="alert"> <p> Stream needs your permission to <button onClick={grantPermission}> enable desktop notifications </button> </p> </div> )} <ChannelList options={{ subscribe: true, state: true, }} List={ChannelListTeam} /> <Channel channel={channel}> <Window> <ChannelHeader /> <MessageList Message={MessageTeam} /> <MessageInput focus /> </Window> <Thread Message={MessageTeam} /> </Channel> </Chat> ); } return <div></div>; } export default App;
You'll also need to add some styles for the banner in your src/App.css
file as shown below:
/* src/App.css */ .alert { text-align: center; background-color: #006CFF; color: #fff; padding: 15px; } .alert p { margin: 0; } .alert button { background-color: transparent; border: none; font-size: inherit; color: inherit; margin: 0; padding: 0; text-decoration: underline; }
The setNotificationBanner
hook helps us determine whether to show the alert banner or not. If the user hasn’t granted or denied permission, the banner is shown and, once it's clicked, a prompt is displayed by the browser asking the user to block or allow notifications from the app.
Now that notifications are enabled, the next thing to figure out is a way to display notifications when a user has a new unread message. One method we’ll employ is to change the favicon on the browser window, to give the user multiple indications that a message or few has not yet been read, which is a common technique employed by many web applications today! Once all the messages have been read, we’ll revert back to the normal favicon.
Open your public/index.html
file and add an id
of favicon
to the favicon link
element:
1<link id="favicon" rel="icon" href="%PUBLIC_URL%/favicon.ico" />
Following that, update your src/App.js
file as shown below:
// src/App.js import React, { useState, useEffect } from 'react'; import './App.css'; import { Chat, Channel, ChannelHeader, Thread, Window, ChannelList, ChannelListTeam, MessageList, MessageTeam, MessageInput, } from 'stream-chat-react'; import { StreamChat } from 'stream-chat'; import rug from 'random-username-generator'; import axios from 'axios'; import 'stream-chat-react/dist/css/index.css'; let chatClient; function App() { const [channel, setChannel] = useState(null); const [showNotificationBanner, setShowNotificationBanner] = useState(false); useEffect(() => { const username = rug.generate(); async function getToken() { try { const response = await axios.post('http://localhost:7000/join', { username, }); const { token } = response.data; const apiKey = response.data.api_key; chatClient = new StreamChat(apiKey); chatClient.setUser( { id: username, name: username, }, token ); const channel = chatClient.channel('team', 'group-chat'); await channel.watch(); setChannel(channel); channel.on(event => { if (event.type === 'message.new' && event.unread_count > 0) { new Notification(event.user.name, { body: event.message.text, }); document.getElementById('favicon').href = 'https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/240/google/223/bell_1f514.png'; } if (event.type === 'message.read' && !event.total_unread_count) { document.getElementById('favicon').href = '/favicon.ico'; } }); } catch (err) { console.log(err); return; } } getToken(); if ( window.Notification && (Notification.permission === 'granted' || Notification.permission === 'denied') ) return; setShowNotificationBanner(true); }, []); function grantPermission() { if (Notification.permission === 'granted') { new Notification('You are already subscribed to web notifications'); return; } if ( Notification.permission !== 'denied' || Notification.permission === 'default' ) { Notification.requestPermission().then(result => { if (result === 'granted') { new Notification('New message from Stream', { body: 'Nice, notifications are now enabled!', }); } }); } setShowNotificationBanner(false); } if (channel) { return ( <Chat client={chatClient} theme="team dark"> {showNotificationBanner && ( <div class="alert"> <p> Stream needs your permission to{' '} <button onClick={grantPermission}> enable desktop notifications </button> </p> </div> )} <ChannelList options={{ subscribe: true, state: true, }} List={ChannelListTeam} /> <Channel channel={channel}> <Window> <ChannelHeader /> <MessageList Message={MessageTeam} /> <MessageInput focus /> </Window> <Thread Message={MessageTeam} /> </Channel> </Chat> ); } return <div></div>; } export default App;
The relevant changes are between lines 53-66
. Stream provides an easy way to listen for events on a channel; you can listen for a specific event or for all events, as we’re doing above.
If the message.new
event is triggered, and the unread_count
is greater than 0, we display a notification for the user and set the favicon to a bell icon. Checking the value of unread_count
is necessary because we don’t want to display a notification for messages that have already been read.
Secondly, if the message.read
event is triggered, and the total_unread_count
is falsy (set to 0 or undefined
or null
), we revert to the regular favicon; if the total_unread_count
is falsy, it is an indication that the user has no unread messages.
You can try out your handiwork by using two different browsers to send messages between users. It should send notifications and update the favicons appropriately. You will also notice that the title of the window is updated with the number of unread messages that a user has. This is an automatic feature that we get just by making use of Stream’s React components!
Wrapping Up!
In this tutorial, we’ve described how to set up real time browser notifications for incoming messages with Stream. The Stream Chat docs contain more information on the various types of events that can be listened for, so be sure to check it out to see everything that’s available to you!
The complete source code used in this tutorial can be found in the GitHub repository. Thanks for reading!