In this post, we are going to implement a simple Group Chat application with Vanilla Javascript and Stream Chat!
Stream Chat allows you to rapidly ship real-time messaging systems that are reliable and robust, without the overhead cost and time of managing the infrastructure by yourself. And it’s pretty easy to use...
You’ll find out how easy it is to plug in a messaging module (with all the cool features) to your existing application or to build something new and exciting with the Stream SDK/API from a simple chat application to something even more complex, and you don’t have to worry about building all those functionalities yourself!
Let’s get started!
Create an Account on Stream
Creating an account on Stream is pretty straight forward.
Visit the Stream website and Sign up:
Once your account is created successfully, you can quickly log in to your dashboard to get your app secret
and key
as shown below:
You'll need the API key and secret to authenticate with the Stream API. Keep it safe 🙂
Set Up
Let’s set up the application structure by creating some directories and installing Stream...
To create your working directory and move into that directory, run:
1mkdir chat-application && cd chat-application
Then make your directory to look like what we have below:
├── dashboard.bundle.js
├── public
│ ├── index.html
│ └── static
│ ├── login.css
│ └── style.css
└── src
├── Dashboard
│ └── index.js
└── Utils
└── config.js
Let’s start with the UI!
We are going to use the following html template for our chat application; add this template to your public/index.html
file:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link src='https://use.fontawesome.com/releases/v5.0.13/js/all.js'></link> <link rel="stylesheet" href="./static/style.css"> <title>Stream Chat App </title> </head> <body> <section class="msger"> <header class="msger-header"> <div class="msger-header-title"> <i class="fas fa-comment-alt"></i> Stream Chat App <p id="count"></p> </div> <div class="msger-header-options"> <span><i class="fas fa-cog"></i></span> </div> </header> <main class="msger-chat"> <p id="loading">Loading...</p> <ul id="left-msg"></ul> <ul id="right-msg"></ul> </main> <div class="msger-inputarea"> <input type="text" class="msger-input" id="message-input" placeholder="Enter your message..."> </div> <p id="info"></p> </section> <script src="./../dashboard.bundle.js" type="module"> </script> </script> </body> </html>
Then, download this CSS file and add it to the /public/static/index.css
file.
Now, you should have the UI up and running, but it doesn’t do anything. It’s just a dumb HTML and CSS web page:
So, let’s get Stream to add some functionality to the html page!
Did you notice that we are loading a bundled version of our javascript file to the HTML page?
1<script src="./../dashboard.bundle.js" type="module"></script>
That’s because we're using the Stream Chat NPM module! So, we are going to install Stream Chat with NPM or yarn and then use browserify to bundle everything into dashboard.bundle.js
.
You can install browserify globally; it’s not required for your application to work, it’s a tool to help us bundle npm modules and use them in the browser:
1npm install -g browserify
So, in your root directory (chat-application
), run:
1npm i stream-chat
Now, we are all set up! Let’s get coding 🙂
To prepare, open the dashboard index file src/Dashboard/index.js
; this is where we’ll write our chat logic.
└── src
├── Dashboard
│ └── index.js
Group Chat Code
Add the code below to the src/Dashboard/index.js
file:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122const Stream = require('../../node_modules/stream-chat/dist/index.js'); const config = require('../Utils/config'); const { StreamChat } = Stream; const { BubbleTemplate } = config; const messsageText = document.getElementById('message-input'); const info = document.getElementById('info'); const token = 'fdjfknfdfmdbfknmbhvcwewf'; const apiKey = 'bfkjbeifjkbjwe'; const client = new StreamChat(apiKey); //initialize user client.setUser( { id: 'userid', name: `First Name Last Name`, image: 'https://image.flaticon.com/icons/svg/145/145867.svg' }, token ); // Set Channel details const channel = client.channel('messaging', 'General', { name: 'Awesome channel about traveling' }); // fetch the channel state, subscribe to future updates const state = channel.watch(); // sends an event typing.start to all channel participants const checkTyping = async e => { if (e.type === 'keypress') { // sends an event typing.start to all channel participants await channel.keystroke(); } }; // Send message to the channel messsageText.addEventListener('keypress', e => { checkTyping(e); if (e.keyCode == 13) { // clean message input a bit const text = e.target.value .replace(/</g, '<') .replace(/>/g, '>') .trim(); if (text === '') { return -1; //empty messages cannot be sent } else { channel.sendMessage({ text: e.target.value }); e.target.value = ''; } } }); messsageText.addEventListener('keyup', e => { checkTyping(e); }); // Listen for new messages channel.on('message.new', event => { const message = channel.state.messages[channel.state.messages.length - 1]; //push the new message to the UI singleMessageDisplay(message); }); channel.on('typing.start', event => { // You don't want to see your own 'you are typing...'. So only show that other people are typing and not you if (event.user.name !== client.user.name) { info.textContent = event.user.name + ' is typing...'; } }); channel.on('typing.stop', event => { // Replace the ... is typing ... text with nothing 2 seconds after they stop typing. setTimeout(() => { info.textContent = ''; }, 2000); }); // What is our current channel state? We get to know that from here async function getState() { return await state; } // Get historical messages getState().then(data => { document.getElementById('loading').textContent = ''; data.messages.map(message => { singleMessageDisplay(message); }); }); // Push single message to display. This function pushes a chat bubble to the UI when you hit enter const singleMessageDisplay = message => { if (message.user.id === client.user.id) { const div = document.createElement('div'); div.className = 'msg right-msg'; div.innerHTML = BubbleTemplate( message.user.name, message.user.id, message.text, message.created_at, client.user.image ); document.getElementById('right-msg').appendChild(div); } if (message.user.id !== client.user.id) { const div = document.createElement('div'); div.className = 'msg left-msg'; div.innerHTML = BubbleTemplate( message.user.name, message.user.id, message.text, message.created_at, message.user.image ); document.getElementById('left-msg').appendChild(div); } };
And this to the src/Utils/configuration.js
file:
123456789101112131415161718192021222324252627282930const TimeAgo = require('../../node_modules/javascript-time-ago'); // Load locale-specific relative date/time formatting rules. const en = require('javascript-time-ago/locale/en'); TimeAgo.addLocale(en); const timeAgo = new TimeAgo('en-US'); const config = { BubbleTemplate: (name, id, text, date, image) => { return ` <div id="userImage" style="background-image: url(${image})" class="msg-img"> </div> <div class="msg-bubble"> <div class="msg-info"> <div class="msg-info-name" id="name">${name}</div> <div class="msg-info-time">${timeAgo.format( date )}</div> </div> <div class="msg-text"> ${text} </div> </div> `; } }; module.exports = config;
Now, let’s dig into the code and see how it all comes together...
Right here, we are importing Stream and importing the chatbubble template, which we defined in the config file:
1234567891011const Stream = require('../../node_modules/stream-chat/dist/index.js'); const config = require('../Utils/config'); const { StreamChat } = Stream; const { BubbleTemplate } = config; const messsageText = document.getElementById('message-input'); const info = document.getElementById('info'); const token = 'fdjfknfdfmdbfknmbhvcwewf'; const apiKey = 'bfkjbeifjkbjwe';
The BubbleTemplate
is a function that returns HTML when we pass the name
, id
, text
, date
, and the image url of the bubble
. We need this bubble template because each time we receive a message from the group, we want to push it to the UI as a bubble containing a message in a group chat. We can easily pass the message object info to this function and re-use it as many times as we want to:
BubbleTemplate(name, id, text, date, image)
Next, let’s select the message
input field and define some important information:
1234const messsageText = document.getElementById('message-input'); const info = document.getElementById('info'); const token = '<USER_TOKEN>'; const apiKey = '<YOUR_API_KEY>';
In reality, the token should be generated after you log in/create a user for your Stream application using the Stream API. You can start by using this open-source Stream API to log users in.
Run this command in a different directory to clone the open-source API and install the dependencies:
1git clone https://github.com/astrotars/stream-chat-api && cd stream-chat-api && npm i
The API uses MongoDB to store user data; create a MongoDB database with Mongo Atlas and get your MongoDB URI. Then, get your API Secret
and Key
from your Stream Dashboard, as explained earlier.
Create a .env
file in the root directory of the API and add the following configuration to it:
12345NODE_ENV=development PORT=8080 STREAM_API_KEY=YOUR_STREAM_API_KEY STREAM_API_SECRET=YOUR_STREAM_API_SECRET MONGODB_URI=YOUR_MONGODB_URI
Then, run
1npm start
to start up the API; it should be running on the PORT=8080
, as defined in the .env
file.
You can now create a user using Postman by sending a POST
request to this url http://localhost:8080/v1/auth/init
, with this data:
{
"name": {
"first": "First Name",
"last": "Last Name"
},
"email": "foo@bar.baz",
"password": "qux"
}
Copy the token
and apiKey
, and user id
from the response. Switch back to your chat application src/Dashboard/index.js
file we’ve been looking at and update the token
and apiKey
with the token and key from the response:
12const token = '<USER_TOKEN>'; const apiKey = '<YOUR_API_KEY>';
It would be fun if you can create a registration interface to make an api call to this api and store the data in local storage after registration...
You can start with something like this:
123456789101112131415161718192021222324252627282930313233343536const axios = require('../../node_modules/axios/index.js'); const email = document.getElementById('email'); const password = document.getElementById('password'); const lastname = document.getElementById('lastname'); const firstname = document.getElementById('firstname'); const submit = document.getElementById('submit'); submit.addEventListener('submit', e => { e.preventDefault(); initStream(e); }); const initStream = async e => { e.preventDefault(); await axios .post('https://react-api-stream.herokuapp.com/v1/auth/init', { headers: { 'Content-Type': 'application/json' }, name: { first: firstname.value, last: lastname.value }, password: password.value, email: email.value }) .then(response => { localStorage.setItem('user', JSON.stringify(response.data.user)); localStorage.setItem('token', response.data.token); localStorage.setItem('apiKey', response.data.apiKey); window.location.href = '/public/index.html'; }) .catch(err => { // setLoading(false); // setErrorMessage("Your registration wasn't successful, please, try again.") console.log('error', err); }); };
and an html sign up page like this:
12345678910111213141516171819202122232425262728293031323334<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <!-- <script src="https://cdn.jsdelivr.net/npm/stream-chat"></script> --> <link src='https://use.fontawesome.com/releases/v5.0.13/js/all.js'></link> <link rel="stylesheet" href="./static/login.css"> <title>Stream Chat App </title> </head> <body> <div class='container'> <form method='post' id="submit"> <h1> Sign Up </h1> <div class='form-content'> <input name='first-name' placeholder='First Name' type='text' id='firstname' /> <input name='last-name' placeholder='Last Name' type='text' id="lastname" /> <input name='email' placeholder='Email' type='text' id="email" /> <input name='password' placeholder='Password' type='password' id="password" /> <br /> <button class='button' type='submit'> Submit </button> <br /> </div> </form> </div> <script src="./../login.bundle.js" type="module"></script> </body> </html>
That’s like a pretty good starting point...
So, now that we understand what is going on here:
12345678910const Stream = require('../../node_modules/stream-chat/dist/index.js'); const config = require('../Utils/config'); const { StreamChat } = Stream; const { BubbleTemplate } = config; const messsageText = document.getElementById('message-input'); const info = document.getElementById('info'); const token = 'fdjfknfdfmdbfknmbhvcwewf'; const apiKey = 'bfkjbeifjkbjwe';
Let’s move on to the next bit of code!
Setting A User
Setting a user in Stream Chat is pretty simple. Create a new instance of StreamChat
called client
by passing in your API key
to the constructor. This will serve as an entry point to access the StreamChat
functionality.
Next, we initialize a new user
. You can get all the information you need to set a user
in the response you receive from the API call you made initially:
1234567891011const client = new StreamChat(apiKey); //initialize user client.setUser( { id: 'userid', name: `First Name Last Name`, image: 'https://image.flaticon.com/icons/svg/145/145867.svg' }, token );
Set The Channel Details And Subscribe to Further Updates
So, what channel do you want your users to be in? You can specify the channel information in the channel.client
function. The channel
receives three parameters. The first is the channel type
(we'll use 'messaging'
), and the other are the channel ID
and details
you can specify as many details as you want):
1234567// Set Channel details const channel = client.channel('messaging', 'General', { name: 'Awesome channel about traveling' }); // fetch the channel state, subscribe to future updates const state = channel.watch();
Send a Message to The Group
Next, we'll plug in an event listener to the messageInput
(the input box you type your messages into) that we defined initially.
We'll listen to your key presses with the checkTyping
function and let everyone know that you are typing:
123456const checkTyping = async e => { if (e.type === 'keypress') { // sends an event typing.start to all channel participants await channel.keystroke(); } };
and when you finish and hit enter, we'll catch that using the keyCode 13
, and then send the message with channel.sendMessage
:
1234567891011121314151617181920// Send message to the channel messsageInput.addEventListener('keypress', e => { checkTyping(e); if (e.keyCode == 13) { // clean message input a bit const text = e.target.value .replace(/</g, '<') .replace(/>/g, '>') .trim(); if (text === '') { return -1; //empty messages cannot be sent } else { channel.sendMessage({ text: e.target.value }); e.target.value = ''; } } });
Listening For New Messages
If any member of the channel sends a message, we get notified using the message.new
event. We look at the messages in that channel state
and pick the latest message
and push it to the UI.
123456// Listen for new messages channel.on('message.new', event => { const message = channel.state.messages[channel.state.messages.length - 1]; //push the new message to the UI singleMessageDisplay(message); });
Loading Historical Messages
The first time we open our app, the first thing we want is to load our previous conversations so we can start up from there...
That is what this part of the code does. It gets the state
data, and pushes it to the UI:
123456789101112// What is our current channel state? We get to know that from here async function getState() { return await state; } // Get historical messages getState().then(data => { document.getElementById('loading').textContent = ''; data.messages.map(message => { singleMessageDisplay(message); }); });
Last, but not least, let’s talk about the singleMessageDisplay
function; this function just stacks the latest messages (using the bubble template) to the UI. If the message is from you, it stacks it to the right, and if it’s from anyone else in the group, it stacks it to the left:
12345678910111213141516171819202122232425262728// Push single message to display. This function pushes a chat bubble to the UI when you hit enter const singleMessageDisplay = message => { if (message.user.id === client.user.id) { const div = document.createElement('div'); div.className = 'msg right-msg'; div.innerHTML = BubbleTemplate( message.user.name, message.user.id, message.text, message.created_at, client.user.image ); document.getElementById('right-msg').appendChild(div); } if (message.user.id !== client.user.id) { const div = document.createElement('div'); div.className = 'msg left-msg'; div.innerHTML = BubbleTemplate( message.user.name, message.user.id, message.text, message.created_at, message.user.image ); document.getElementById('left-msg').appendChild(div); } };
So, far here is what we have:
Our application is ready! Here is how the final chat application works:
You can find the completed project on Github here.
A Few Notes
I'd advise using browserify
to bundle your javascript code like so:
1browserify src/Dashboard/index.js -o dashboard.bundle.js
Wrapping Up
Creating a chat application with Vanilla Javascript and Stream is pretty simple, and there is so much more you can do with Stream, this is just scratching the surface...
Check out the docs to learn more! I challenge you to add even more functionalities to the chat application after reading the docs...
I can’t wait to see the awesome stuff you’ll build with Stream and Javascript!