Advertisement

Real-Time Messaging for Meteor With Meteor Streams

by

This is 2013. If you are going to build a webapp, you must add real-time capabilities to the app. It is the standard. Meteor does a pretty good job at helping you to quickly build and make apps real-time. But meteor is tightly coupled with MongoDB and it is the only way to add real-time capabilities. Sometimes, this is overkill.

MongoDB is a perfect match for Meteor. But we don't need to use MongoDB for all our real-time activities. For some problems, messaging based solutions work really well. It's the same problem that pubnub and real-time.co are also addressing.

It would be great if we could have a hybrid approach to real-time, in Meteor, combining the MongoDB Collection based approach and a Messaging based approach. Thus Meteor Streams was born to add this messaging based, real-time communication to Meteor.


Introducing Meteor Streams

A Stream is the basic building block of Meteor Streams. It is a real-time EventEmitter. With a Stream, you can pass messages back and forth between connected clients. It is highly manageable and has a very good security model.

Lets Give It a Try

Let's create a very simple, browser console based chat application with Meteor Streams. We'll first create a new Meteor application:

meteor create hello-stream

Next we install Meteor Streams from the atmosphere:

mrt add streams

Then we need to create a file named chat.js and place in the following code:

chatStream = new Meteor.Stream('chat');

if(Meteor.isClient) {
  sendChat = function(message) {
    chatStream.emit('message', message);
    console.log('me: ' + message);
  };

  chatStream.on('message', function(message) {
    console.log('user: ' + message);
  });
}

Start your app with:

meteor

Your app will now be running on - http://localhost:3000.

Now you have a fully functioning chat app. To start chatting, open the browser console and use the sendChat method as shown as below.

Browser Console based Chat with Meteor Streams

Let's Dive In Further

It's kind of hard to understand Meteor Streams with just a simple console based example, like the one we just built above. So, let's build a full featured chat application to become more familiar with Meteor Streams.

The App

The app we are creating is a web based chat application. Anyone can chat anonymously. Also, users can register and chat with their identity(username). It also has a filtering system, which filters out bad words (profanity).

At the end, it will look something like this. You can grab the source code from github to see the final result.

Meteor Streams Chat App in Action

Let's Create the App

Let's create a standard Meteor app and install Meteor Streams from atmosphere. We'll also be adding support for bootstrap and Meteor Accounts.

meteor create awesome-chat-app
cd awesome-chat-app
meteor remove insecure autopublish
meteor add bootstrap accounts-password accounts-ui
mrt add streams
rm awesome-chat-app.* //remove files added automatically

Let's Build the UI

The user interface for our app will be pretty simple. We have a div showing the chat messages and an input box to enter in new chat messages. See below for the complete HTML of our UI. Check out the inline comments if you need help understanding the code.

Add the following content into client/home.html:

<head>
  <title>Awesome Chat App</title>
  <style type="text/css">
    #chat-message {
      width: 500px;
      height: 50px;
    }

    #messages {
      width: 700px;
      height: 300px;
      border: 1px solid rgb(230, 230, 230);
      margin: 0px 0px 10px 0px;
    }
  </style>
</head>

<body>
  {{> mainBox}}
</body>

<!-- Main Chat Window -->
<template name='mainBox'>
  <div class='container'>
    <h2>Awesome Chat App</h2>
    <!-- shows login buttons -->
    {{loginButtons}}
    {{> chatBox}}
  </div>
</template>

<!-- Chat Box with chat messages and the input box -->
<template name='chatBox'>
  <div id='messages'>
    {{#each messages}}
      {{>chatMessage}}
    {{/each}}
  </div>
  <textarea id='chat-message'></textarea><br>
  <button class='btn btn-primary' id='send'>Send Chat</button>
</template>

<!-- Template for the individual chat message -->
<template name='chatMessage'>
  <div>
    <b>{{user}}:</b> {{message}}
  </div>
</template>

Wiring Up Our Chat

Meteor's reactivity is an awesome concept and very useful. Now, Meteor Streams is not a reactive data source. But it can work well with local only collections to provide reactivity.

As the name implies, local only collections do not sync its data with the server. Its data is only available inside the client(browser tab).

Add the following content into lib/namespace.js to create our local only collection:

if(Meteor.isClient) {
  chatCollection = new Meteor.Collection(null);
}

Now it's time to wire up our templates with the collection. Let's do following:

  • Assign the collection to the messages helper in the chatBox template.
  • Generate a value for the user helper in the chatMessage template.
  • When the Send Chat button is clicked, add the typed chat message into the collection.

Add the following content to client/ui.js:

// assign collection to the `messages` helper in `chatBox` template
Template.chatBox.helpers({
  "messages": function() {
    return chatCollection.find();
  }
});

// generate a value for the `user` helper in `chatMessage` template
Template.chatMessage.helpers({
  "user": function() {
    return this.userId;
  }
});

// when `Send Chat` clicked, add the typed chat message into the collection
Template.chatBox.events({
  "click #send": function() {
    var message = $('#chat-message').val();
    chatCollection.insert({
      userId: 'me',
      message: message
    });
    $('#chat-message').val('');
  }
});

With the above changes you'll be able to chat, but messages are only display on your client. So let's handover the rest of the job to Meteor Streams.

Let's Create the Stream

We'll be creating the stream on both the client and the server (with the same name) and adding the necessary permissions.

Append the following code into lib/namespace.js to create the stream:

chatStream = new Meteor.Stream('chat-stream');

Just creating the stream alone is not enough; we need to give the necessary permissions, which allow clients to communicate through it. There are two types of permissions (read and write). We need to consider the event, userId, and the subscriptionId when we are creating the permission.

  • userId is the userId of the client connected to the stream.
  • subscriptionId is the unique identifier created for each client connected to the stream.

For our chat app, we need to give anyone using the app full read and write access to the chat event. This way, clients can use it for sending and receiving chat messages.

Add the following code to server/permissions.js:

chatStream.permissions.read(function(eventName) {
  return eventName == 'chat';
});

chatStream.permissions.write(function(eventName) {
  return eventName == 'chat';
});

Connecting the Stream With the UI

Now that we have a fully functioning stream, let's connect it to the UI so others can see the messages that you are sending.

The first thing we need to do is add our chat messages to the stream, when we click on the Send Chat button. For that, we need to modify the code related to the Send Chat button's click event(click #send), as follows (in client/ui.js):

Template.chatBox.events({
  "click #send": function() {
    var message = $('#chat-message').val();
    chatCollection.insert({
      userId: 'me',
      message: message
    });
    $('#chat-message').val('');

    // == HERE COMES THE CHANGE ==
    //add the message to the stream
    chatStream.emit('chat', message);
  }
});

Then we need to listen to the stream for the chat event and add the message to the chatCollection which is being rendered in the UI, reactively. Append the following code to the client/ui.js file:

chatStream.on('chat', function(message) {
  chatCollection.insert({
    userId: this.userId, //this is the userId of the sender
    subscriptionId: this.subscriptionId, //this is the subscriptionId of the sender
    message: message
  });
});

Now we need to modify the logic which generates the value for the user helper in the chatMessage template as follows:

  • Logged in user - user-<userId>
  • Anonymous user - anonymous-<subscriptionId>

Modify the code for the user helper in the chatMessage template to reflect the above changes (in client/ui.js):

Template.chatMessage.helpers({
  "user": function() {
    var nickname = (this.userId)? 'user-' + this.userId : 'anonymous-' + this.subscriptionId;
    return nickname;
  }
});

Displaying the Username Instead of the userId

Showing just the userId is not very useful. So let's change it to display the actual username. Here, we'll be using Meteor Pub/Sub to get the username for a given userId.

First of all, lets configure Meteor Accounts to accept the username when creating the user. Add the following code to client/users.js:

Accounts.ui.config({
  passwordSignupFields: "USERNAME_ONLY"
});

Then let's create the publication for getting the user. Add the following code to server/users.js. It simply returns the username for a given userId.

Meteor.publish("user-info", function(id) {
  return Meteor.users.find({_id: id}, {fields: {username: 1}});
});

Now we need to create a subscription on the client for each user we are interested in. We'll do this inside a method. Additionally, after we get the username, it needs to be assigned to a session variable. Then we can use the session variable inside the user helper to get the username reactively.

Append the following code into client/users.js:

getUsername = function(id) {
  Meteor.subscribe('user-info', id);
  Deps.autorun(function() {
    var user = Meteor.users.findOne(id);
    if(user) {
      Session.set('user-' + id, user.username);
    }
  });
}

Finally, let's modify the user helper in the chatMessage template to get the username from the session (in client/ui.js):

Template.chatMessage.helpers({
  "user": function() {
    if(this.userId == 'me') {
      return this.userId;
    } else if(this.userId) {
      getUsername(this.userId);
      return Session.get('user-' + this.userId);
    } else {
      return 'anonymous-' + this.subscriptionId;
    }
  }
});

Filtering Out Bad Words

Our chat app will make sure to hide any profanity. If someone tries to send a message with some bad words, we need to filter those out. Meteor Stream has a feature called filters, which is designed for this. Let's see how we can filter out the word fool from any chat message.

Add the following code into server/filters.js:

chatStream.addFilter(function(eventName, args) {
  if(eventName == 'chat') {
    var message = args[0];
    if(message) {
      message = message.replace(/fool/ig, '****');
    }
    return [message];
  } else {
    return args;
  }
});

Feel free to add in your own filters.

Our chat app is now complete. You can see a live version of the app at http://streams-chat.meteor.com. Additionally, the Source code for the app is available on Github.


Conclusion

In this tutorial we built a chat application using local only collections for adding in reactivity and used Meteor Pub/Sub for getting the username of a user. Hopefully you can see how nicely Meteor Streams can work with existing Meteor functionality. Still, this is just an introduction to Meteor Streams, for additional resources, check out the following links: