Implement Stream Chat with Vanilla JS

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:

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:

1const Stream = require('../../node_modules/stream-chat/dist/index.js');
2const config = require('../Utils/config');
3const { StreamChat } = Stream;
4const { BubbleTemplate } = config;
5const messsageText = document.getElementById('message-input');
6const info = document.getElementById('info');
7const token = 'fdjfknfdfmdbfknmbhvcwewf';
8const apiKey = 'bfkjbeifjkbjwe';
9
10const client = new StreamChat(apiKey);
11//initialize user
12client.setUser(
13  {
14    id: 'userid',
15    name: `First Name Last Name`,
16    image: 'https://image.flaticon.com/icons/svg/145/145867.svg'
17  },
18  token
19);
20
21// Set Channel details
22const channel = client.channel('messaging', 'General', {
23  name: 'Awesome channel about traveling'
24});
25
26// fetch the channel state, subscribe to future updates
27const state = channel.watch();
28
29// sends an event typing.start to all channel participants
30const checkTyping = async e => {
31  if (e.type === 'keypress') {
32    // sends an event typing.start to all channel participants
33    await channel.keystroke();
34  }
35};
36
37// Send message to the channel
38messsageText.addEventListener('keypress', e => {
39  checkTyping(e);
40  if (e.keyCode == 13) {
41    // clean message input a bit
42    const text = e.target.value
43      .replace(/</g, '<')
44      .replace(/>/g, '>')
45      .trim();
46    if (text === '') {
47      return -1; //empty messages cannot be sent
48    } else {
49      channel.sendMessage({
50        text: e.target.value
51      });
52      e.target.value = '';
53    }
54  }
55});
56
57messsageText.addEventListener('keyup', e => {
58  checkTyping(e);
59});
60
61// Listen for new messages
62channel.on('message.new', event => {
63  const message = channel.state.messages[channel.state.messages.length - 1];
64  //push the new message to the UI
65  singleMessageDisplay(message);
66});
67
68channel.on('typing.start', event => {
69  // You don't want to see your own 'you are typing...'. So only show that other people are typing and not you
70  if (event.user.name !== client.user.name) {
71    info.textContent = event.user.name + ' is typing...';
72  }
73});
74
75channel.on('typing.stop', event => {
76  // Replace the ... is typing ... text with nothing 2 seconds after they stop typing.
77  setTimeout(() => {
78    info.textContent = '';
79  }, 2000);
80});
81
82// What is our current channel state? We get to know that from here
83async function getState() {
84  return await state;
85}
86
87// Get historical messages
88getState().then(data => {
89  document.getElementById('loading').textContent = '';
90  data.messages.map(message => {
91    singleMessageDisplay(message);
92  });
93});
94
95// Push single message to display. This function pushes a chat bubble to the UI when you hit enter
96const singleMessageDisplay = message => {
97  if (message.user.id === client.user.id) {
98    const div = document.createElement('div');
99    div.className = 'msg right-msg';
100    div.innerHTML = BubbleTemplate(
101      message.user.name,
102      message.user.id,
103      message.text,
104      message.created_at,
105      client.user.image
106    );
107    document.getElementById('right-msg').appendChild(div);
108  }
109
110  if (message.user.id !== client.user.id) {
111    const div = document.createElement('div');
112    div.className = 'msg left-msg';
113    div.innerHTML = BubbleTemplate(
114      message.user.name,
115      message.user.id,
116      message.text,
117      message.created_at,
118      message.user.image
119    );
120    document.getElementById('left-msg').appendChild(div);
121  }
122};

And this to the src/Utils/configuration.js file:

1const TimeAgo = require('../../node_modules/javascript-time-ago');
2
3// Load locale-specific relative date/time formatting rules.
4const en = require('javascript-time-ago/locale/en');
5TimeAgo.addLocale(en);
6
7const timeAgo = new TimeAgo('en-US');
8
9const config = {
10  BubbleTemplate: (name, id, text, date, image) => {
11    return `
12      <div id="userImage" style="background-image: url(${image})"
13      class="msg-img">
14          </div>
15              <div class="msg-bubble">
16                  <div class="msg-info">
17                      <div class="msg-info-name" id="name">${name}</div>
18                      <div class="msg-info-time">${timeAgo.format(
19                        date
20                      )}</div>
21                  </div>
22                  <div class="msg-text">
23                      ${text}
24                  </div>
25              </div>
26      `;
27  }
28};
29
30module.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:

1const Stream = require('../../node_modules/stream-chat/dist/index.js');
2const config = require('../Utils/config');
3
4const { StreamChat } = Stream;
5const { BubbleTemplate } = config;
6
7const messsageText = document.getElementById('message-input');
8const info = document.getElementById('info');
9
10const token = 'fdjfknfdfmdbfknmbhvcwewf';
11const 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:

1const messsageText = document.getElementById('message-input');
2const info = document.getElementById('info');
3const token = '<USER_TOKEN>';
4const 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/nparsons08/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:

1NODE_ENV=development
2PORT=8080
3STREAM_API_KEY=YOUR_STREAM_API_KEY
4STREAM_API_SECRET=YOUR_STREAM_API_SECRET
5MONGODB_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:

1const token = '<USER_TOKEN>';
2const 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:

1const axios = require('../../node_modules/axios/index.js');
2
3const email = document.getElementById('email');
4const password = document.getElementById('password');
5const lastname = document.getElementById('lastname');
6const firstname = document.getElementById('firstname');
7const submit = document.getElementById('submit');
8
9submit.addEventListener('submit', e => {
10  e.preventDefault();
11  initStream(e);
12});
13
14const initStream = async e => {
15  e.preventDefault();
16  await axios
17    .post('https://react-api-stream.herokuapp.com/v1/auth/init', {
18      headers: {
19        'Content-Type': 'application/json'
20      },
21      name: { first: firstname.value, last: lastname.value },
22      password: password.value,
23      email: email.value
24    })
25    .then(response => {
26      localStorage.setItem('user', JSON.stringify(response.data.user));
27      localStorage.setItem('token', response.data.token);
28      localStorage.setItem('apiKey', response.data.apiKey);
29      window.location.href = '/public/index.html';
30    })
31    .catch(err => {
32      // setLoading(false);
33      // setErrorMessage("Your registration wasn't successful, please, try again.")
34      console.log('error', err);
35    });
36};

and an html sign up page like this:

1    <!DOCTYPE html>
2    <html lang="en">
3    <head>
4        <meta charset="UTF-8">
5        <meta name="viewport" content="width=device-width, initial-scale=1.0">
6        <meta http-equiv="X-UA-Compatible" content="ie=edge">
7        <!-- <script src="https://cdn.jsdelivr.net/npm/stream-chat"></script> -->
8        <link src='https://use.fontawesome.com/releases/v5.0.13/js/all.js'></link>
9        <link rel="stylesheet" href="./static/login.css">
10        <title>Stream Chat App </title>
11    </head>
12    <body>
13        <div class='container'>
14            <form method='post' id="submit">
15              <h1>
16                  Sign Up
17              </h1>
18              <div class='form-content'>
19                <input name='first-name' placeholder='First Name' type='text' id='firstname' />
20                <input name='last-name' placeholder='Last Name' type='text' id="lastname" />
21                <input name='email' placeholder='Email' type='text' id="email" />
22                <input name='password' placeholder='Password' type='password' id="password" />
23
24                <br />
25                <button class='button'  type='submit'>
26                  Submit
27                </button>
28                <br />
29              </div>
30            </form>
31          </div>
32          <script src="./../login.bundle.js" type="module"></script>
33    </body>
34    </html>

That’s like a pretty good starting point...

So, now that we understand what is going on here:

1const Stream = require('../../node_modules/stream-chat/dist/index.js');
2const config = require('../Utils/config');
3
4const { StreamChat } = Stream;
5const { BubbleTemplate } = config;
6
7const messsageText = document.getElementById('message-input');
8const info = document.getElementById('info');
9const token = 'fdjfknfdfmdbfknmbhvcwewf';
10const 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:

1const client = new StreamChat(apiKey);
2
3//initialize user
4client.setUser(
5  {
6    id: 'userid',
7    name: `First Name Last Name`,
8    image: 'https://image.flaticon.com/icons/svg/145/145867.svg'
9  },
10  token
11);

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):

1// Set Channel details
2const channel = client.channel('messaging', 'General', {
3  name: 'Awesome channel about traveling'
4});
5
6// fetch the channel state, subscribe to future updates
7const 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:

1const checkTyping = async e => {
2  if (e.type === 'keypress') {
3    // sends an event typing.start to all channel participants
4    await channel.keystroke();
5  }
6};

and when you finish and hit enter, we'll catch that using the keyCode 13, and then send the message with channel.sendMessage:

1// Send message to the channel
2messsageInput.addEventListener('keypress', e => {
3  checkTyping(e);
4  if (e.keyCode == 13) {
5    // clean message input a bit
6    const text = e.target.value
7      .replace(/</g, '<')
8      .replace(/>/g, '>')
9      .trim();
10
11    if (text === '') {
12      return -1; //empty messages cannot be sent
13    } else {
14      channel.sendMessage({
15        text: e.target.value
16      });
17      e.target.value = '';
18    }
19  }
20});

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.

1// Listen for new messages
2channel.on('message.new', event => {
3  const message = channel.state.messages[channel.state.messages.length - 1];
4  //push the new message to the UI
5  singleMessageDisplay(message);
6});

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:

1// What is our current channel state? We get to know that from here
2async function getState() {
3  return await state;
4}
5
6// Get historical messages
7getState().then(data => {
8  document.getElementById('loading').textContent = '';
9  data.messages.map(message => {
10    singleMessageDisplay(message);
11  });
12});

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:

1// Push single message to display. This function pushes a chat bubble to the UI when you hit enter
2const singleMessageDisplay = message => {
3  if (message.user.id === client.user.id) {
4    const div = document.createElement('div');
5    div.className = 'msg right-msg';
6    div.innerHTML = BubbleTemplate(
7      message.user.name,
8      message.user.id,
9      message.text,
10      message.created_at,
11      client.user.image
12    );
13    document.getElementById('right-msg').appendChild(div);
14  }
15
16  if (message.user.id !== client.user.id) {
17    const div = document.createElement('div');
18    div.className = 'msg left-msg';
19    div.innerHTML = BubbleTemplate(
20      message.user.name,
21      message.user.id,
22      message.text,
23      message.created_at,
24      message.user.image
25    );
26    document.getElementById('left-msg').appendChild(div);
27  }
28};

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!