Building a Slack Clone in Meteor.js (Part 4): Channels and Chat Rooms

Free Course

Build Your First Node.js Website

Node is a powerful tool to get JavaScript on the server. Use Node to build a great website.

This is the fourth of a five-part series on building a Slack clone using Meteor. The aim of these tutorials are not just for you to blindly follow instructions, but it’s our hope that you’ll understand the thought process and reasoning behind the architecture.

We have already dealt with publish and subscribe functions before when dealing with security, but now we will use the same logic to create different channels (or rooms) and private messaging.

We will also take a look at using sessions to keep track of which channel we are on. Lastly, we will explore using a router to allow us to permalink the channels.

Creating Channels

So, let’s create a new collection of channels and seed it. Since every Slack room always have two default channels – general and random, we will create those too.

/lib/collections/channels.js


Channels = new Mongo.Collection("channels");

/server/seeder.js


Channels.remove({});
Channels.insert({
  name: "general"
});
Channels.insert({
  name: "random"
});

Updating Seeded Messages

Now that we have two channels, we must associate our seeded messages with a channel. For simplicity, the seeded messages will all be in the general channel.


Factory.define('message', Messages, {
    text: function() {
        return Fake.sentence();
    },
    user: Meteor.users.findOne()._id,
    timestamp: Date.now(),
    channel: 'general'
});

Updating UI

Now we have channels, let’s update our UI. We will publish our Channels collection and subscribe to it on our client.

server/publications.js


Meteor.publish('channels', function () {
    return Channels.find();
});

client/startup/subscribe.js


Meteor.subscribe('channels');

We’d now use that subscription to list out each channel on the left sidebar.

client/app.js


Template.listings.helpers({
    channels: function () {
        return Channels.find();
    }
});

client/components/listings.html


{{#each channels}}
    {{> channel name=name}}
{{/each}}

/client/components/channel.html


<span>
    <span class="prefix">#</span>
    <span class="channel-name">{{name}}</span>
</span>

Now we can see our channels on the sidebar listing.

channels-sidebar

Switching Channels

Now we have two channels, there needs to be a mechanism to allow users to switch between them. We will explore two new (to you) features – Sessions and Routing.

With sessions, we’d store a session variable representing our current channel and use that to display our messages at the template-level. We’d then use the Router to update our URL.

Sessions

Sessions is a global object which is meant to store temporary UI states of the application. The tyoe of data you can store are simple key-value pairs.

When you open a new browser tab and go to the application, you’d get a new session object. So different tabs will each have a session object of its own.

Basic Sessions

You set a session variable by running


Session.set(key, value)

And retrieve the value of a session variable by running


Session.get(key)

So we can use a session variable called channel to keep track of which channel we are currently on.

For simplicity’s sake, let’s set the default channel to general; this means whenever someone goes to our application, they’ll always land on the general channel first.


Meteor.startup(function() {
    Session.set('channel', 'general');
});

We can also make it so that whenever a channel is clicked, it will update the channel session variable with the name of the channel being clicked.


Template.channel.events({
    'click .channel': function (e) {
        Session.set('channel', this.name);
    }
});

Now if you click on ‘general’, and then check for the channel session variable, you’ll find.


> Session.get('channel')
"general"

And if you click on ‘random’, the session variable has changed.


> Session.get('channel')
"random"
Updating UI

Slack shows a green background only behind the current channel, so let’s update our UI to do the same.

client/app.js

We will create a new template helper that will return the string “active” if the channel’s name matches the value in our channel session variable.


Template.channel.helpers({
    active: function () {
        if (Session.get('channel') === this.name) {
            return "active";
        } else {
            return "";
        }
    }
});

We will then add the class into our client/components/channel.html


<li class="channel {{active}}">

And now you can see the active channel has a green background, I’ve also changed the template so the name reflects the current channel.

channel-active-split

The above image also illustrates the fact that different tabs on the same browser and client have different sessions object. Where the channel session variable in one tab is set to general, the other is set to random.

Persistent Sessions

If you’d like to persist the session after the page has been refreshed, or if the browser has been closed, I’d recommend using the u2622:persistent-session package.

Just install it and whenever you want to set a persistent Session variable, use Session.setPersistent(key, value) instead of Session.set(key, value). You can use the normal Session.get(key) to retrieve the value.

This can be great of user experience. If your website has an age gate feature, where users under the legal age cannot access, you’d want the age-verification page to appear only on the first visit. If it shows up every time someone visits, the bounce rate will be terrible.

In this case, you should utilize persistent session to store a variable that says “This visitor has verified their age before”, so your application knows not to display the age-verification page again.

Updating Our Messages

Getting the Messages

Previously, we published all our messages, but now we have channels, we should publish, and subscribe to, only messages in the current channel. To do this, we must pass a parameter into the Meteor.publish method and also provide the argument in the subscription call.

Instead of


Meteor.publish('messages', function () {
    return Messages.find();
}

We now have


Meteor.publish('messages', function (channel) {
    return Messages.find({channel: channel});
});

And in our subscription call, we pass the current channel session variable as the argument.


Template.messages.onCreated(function() {
  var self = this;
  self.autorun(function() {
    self.subscribe('messages', Session.get('channel'));
  });
});
Template-level Subscription

You may have noticed that this template.subscribe() method didn’t occur in the global scope but rather inside a Template.messages.onCreated() method.

The onCreated() method of a template gets ran after the template created, but before the template logic gets evaluated. So the collection we are subscribing to here will be available inside the template.

But the significance is that the subscription occurred on the template-level. This is neat because the subscription is associated with our template. When our template gets destroyed (e.g. removed from the page), the subscription goes with it.

Template-level subscription is a relatively new thing, introduced at the beginning of 2015.

We can even display a loading message while our subscription is getting ready.


{{#if Template.subscriptionsReady}}
    {{#each messages}}
        {{> message text=text timestamp=timestamp user=user }}
    {{/each}}
{{else}}
    Loading…
{{/if}}
autorun

You’ll notice that I wrapped the subscribe call inside a self.autorun() function. This uses the Tracker.

Basically, whenever any variables inside the autorun function changes, the whole autorun function is reran. This means when we switch channels, the messages template will re-subscribe using the new channel session variable, and we only get the messages from the new, current channel.

Inserting New Messages

Each new message must be associated with a channel, so let’s add a new property to our Messages collection.


Meteor.call('newMessage', {
    text: $('.input-box_text').val(),
    channel: Session.get('channel')
});

channels-new-messages

Router

The most popular router package out there is Iron Router. Let’s add the package.


$ meteor add iron:router

For most cases, this is how you’d use Iron Router:

  1. Designate a template as a layout
  2. Define your routes, which will catch the request URL and insert the appropriate template into the layout

Designating a Layout

We have our room.html, which contains the <head> and <body> tags; that would be our layout. So let’s wrap that inside a template named app, and configure our router to use app as the layout. We’d also need to separate our head with our body. We can omit the <body> tag because Meteor will automatically add that in for us once it has concatenated everything together.

client/room.html


<template name="app">
    {{> header}}
    <div class="main">
        {{> loginButtons}}
        {{> listings}}
        {{> yield}}
    </div>
    {{> footer}}
</template>

client/head.html


 <head>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <title></title>
     <link href='http://fonts.googleapis.com/css?family=Lato:400,700,900' rel='stylesheet' type='text/css'>
 </head>

client/routes.js


Router.configure({
  layoutTemplate: 'app'
});

Notice the {{> yield}} inside our app layout. This is where Iron Router will insert the template into.

Define Routes

Slack uses the URL structure teamname.slack.com/messages/channel-name. But let’s keep things simple so our URL will just be example.com/general or example.com/random.

Because our subscription has already been taken care of in the template level, all we need to worry about in the router-level are simply rendering the correct template – the messages template.


Router.route('/:channel', function () {
    this.render('messages');
});

Here the :channel is a variable. So if our request URL is example.com/helloworld, the :channel variable will have the value helloworld. As you’ll see later, we can access this variable using this.params.channel.

But now, if we go to example.com, we’d see an error.

home-not-found

That’s because there are no routes for just /. So let’s create a redirect from / to general.


Router.route('/', function () {
    this.redirect('/general');
});

And now if we go to example.com, it will automatically redirect to example.com/general.

Switching Channels

Now if we switch channels, the URL doesn’t get updated. Let’s change that. All we have to do is to provide the link with an href attribute, just like any other link.

From


<a class="channel_name">

To


<a class="channel_name" href="/{{name}}">

We should also update the CSS styles.


.channel_name {
  text-decoration: none;
  color: inherit;
}

Setting the Session from URL

Previously, we set our channel session variable on startup.


Meteor.startup(function() {
  Session.set('channel', 'general');
});

That means if we open a new tab and type in http://localhost:3000/random, we’d still be on the general channel.

Now, we should also override this default by setting the channel session variable to the channel defined in the URL. If you remember, we can get the channel name using this.params.channel.


Router.route('/:channel', function () {
    Session.set('channel', this.params.channel);
    this.render('messages');
});

This allows allows me to get rid of other Session-setting code, as it’s all being handled at the router-level now. We can now get rid of:

client/input.js


Template.channel.events({
    'click .channel': function (e) {
    Session.set('channel', this.name);
}

client/startup/subscribe.js


Meteor.startup(function() {
    Session.set('channel', 'general');
});

Going Further

So in this article, while practising our publish/subscribe skills by creating channels, we also explored using Iron Router, sessions and template-level subscription.

But Iron Router isn’t the only router for Meteor out there. Recently, a new kid on the block – Flow Router by Arunoda Susiripala. Check out the discussion at forums.meteor.com and join the discussion!

Daniel Li

I am a full-stack web developer in Hong Kong. Do check out my profile, blog and projects, many of which are open-source! I get a real buzz from hearing words like Meteor and Node, if you do too, throw me a tweet!