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:

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

https://gist.github.com/ezesundayeze/35a869b4e4fdf630a9f210f58adf2942

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?

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

npm install -g browserify

So, in your root directory (chat-application), run:

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

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

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

const 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';

Chat bubble

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:

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

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

NODE_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

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

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

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

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

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

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

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

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

// 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.

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

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

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

browserify 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!

TutorialsChat