In our last article, we went over peer to peer (P2P) connections and how we can leverage WebRTC for sending data between two browsers. We will now apply this knowledge and create a P2P chat room where none of the data sent between the two browsers is stored except for on the client’s machines themselves.

The Chat Room Demo

Before we continue into the code of what we are going to build, you can view a working demo at the OfflineChatRoomExample on our Github IO Pages. Open up two browsers and visit the page in each browser. Then get a key in one browser and have the other browser connect to that key. You should see something like this:

Chat Room Example

 

Code Walkthrough

We will be using Meteor to quickly prototype our application – see our tutorial on getting started with Meteor if you are unfamiliar with it. We’ll also make our Meteor application a client-side only application by detaching it from the server – you can read our article on how we do that on our blog as well.

Add PeerJS

First, let’s add the PeerJS file to our project. It’s not in Atmosphere, so we’ll just take the built file and add it to client/shims.  The contents of the peer.js file can be found here.

Add the Database

We will run meteor add ground:db and use a local Ground database to store messages in. We will create a local database that will not sync its data with a central server and will store the data on the browser’s local storage. In lib/collections/Messages.js, add:

Messages = Ground.Collection('messages', { connection: null });

Create the Data Stores

We will use data stores to separate out our event dispatching logic, our database queries, and our views. They provide a great way to separate concerns, which you can read more about in our post about React Data Stores in Meteor. We’ve covered how to make data stores in that article, so we won’t go into the specifics, but the part that we will point out is that when a Message is posted to our MessageStore, we will also tell another store – our PeerStore – that it needs to send a message to any connected clients.

Before we create our stores, we will need to run meteor add react forquet:reflux.

We will create two data stores to make our life easier: a MessageStore which takes care of storing and listening for messages, and a PeerStore which takes care of keeping track of connected peers through PeerJS.

Here is our code for lib/stores/MessageStore.js:

MessageActions = Reflux.createActions([
  'post'
]);

MessageStore = Reflux.createStore({
  listenables: [MessageActions],

  init: function () {
    this._messages = [];

    if (Meteor.isClient) {
      Tracker.autorun(Meteor.bindEnvironment(function (computation) {
        this._messages = Messages.find({}, {
          sort: { createdAt: 1 }
        }).fetch();

        this.trigger(this.getInitialState());
      }.bind(this)));
    }
  },

  post: function (text) {
    this._messages.push({
      text: text,
      poster: 'me'
    });

    Messages.insert({
      text: text,
      poster: 'me',
      createdAt: new Date()
    });

    PeerStore.send(text);
  },

  getInitialState: function () {
    return {
      messages: this._messages
    }
  }
});

The PeerStore is a little trickier. As stated before, it needs to keep track of connections. Thankfully, a data store is a good way to separate out this concern and make our code reusable. Our app is fairly simple in that a connection to a peer and a closing of that connection only happen a few places in our app. But as we add features to our Chat Room and the app grows in scale, it can be very hard to keep track of the connection state.

The PeerStore we have mainly just keeps track of if the Peer is setup, and whether there is a connection. It will also bind event handlers onto the connection for when new data is sent via the connection or whether it is closed.

Here is our code for lib/stores/PeerStore.js:

  'open',
  'connect',
  'disconnect',
  'send'
]);

PeerStore = Reflux.createStore({
  init: function () {
    this._key = null;
    this._connectedToKey = null;
    this._otherKey = null;
    this._peer = null;
    this._connection = null;
  },

  _data: function(data) {
    Messages.insert({
      text: data,
      poster: 'other',
      createdAt: new Date()
    });
  },

  open: function(key) {
    this._key = key;
    this._peer = new Peer(key, {key: 'mx9f1w0bidmibe29'});

    this._peer.on('connection', function (connection) {
      this._connection = connection;
      this._connectedToKey = connection.peer;

      this._connection.on('data', this._data.bind(this));
      this._connection.on('close', function() {
        this._connectedToKey = null;
        this._connection = null;

        this.trigger(this.getInitialState()); 
      }.bind(this));
      

      this.trigger(this.getInitialState());
    }.bind(this));


    this.trigger(this.getInitialState());
  },

  connect: function (otherKey) {
    this._connection = this._peer.connect(otherKey);
    this._connectedToKey = otherKey;

    this._connection.on('data', this._data.bind(this));

    this._connection.on('open', function () {
      this._connection.send(this._key + ' has connected');
    }.bind(this));

    this._connection.on('close', function() {
      this._connectedToKey = null;
      this._connection = null;

      this.trigger(this.getInitialState()); 
    }.bind(this));

    this.trigger(this.getInitialState());
  },

  disconnect: function () {
    this._connection.close();

    this._connectedToKey = null;
    this._connection = null; 

    this.trigger(this.getInitialState());
  },

  send: function (text) {
    this._connection.send(text);
  },

  getInitialState: function() {
    return {
      hasOpenPeer: ( this._peer != null ),
      hasOpenConnection: ( this._connection != null ),
      key: this._key,
      connectedToKey: this._connectedToKey
    }
  }

})

 Connecting to our Data Stores via Containers

Next up, we are going to create our React Containers that will connect our data stores to our views. We have gone over how containers work in React, and we also have an article on using Reflux to content data stores, containers, and views – so, if you are unfamiliar with these concepts, or just a need a refresher, you can read those articles as well.

Before we write our containers, let’s run the following command in our Terminal: meteor add mantarayar:shortid.

We will have three containers. One for our Chat Form, one for our Chat Messages, and one for our Chat Connection.

The client/_containers/ChatFormContainer.jsx is fairly straight forward. Our ChatForm will only need to know whether there is an open connection or not:

ChatFormContainer = React.createClass({
  mixins: [Reflux.connect(PeerStore, 'peer')],

  render() {
    return (
      <ChatForm hasOpenConnection={this.state.peer.hasOpenConnection} />
    );
  }
});

Likewise, the client/_containers/ChatMessagesContainer.jsx file is also very straight forward:

ChatMessagesContainer = React.createClass({
  mixins: [Reflux.connect(MessageStore, 'messages')],

  render() {
    return (
      <ChatMessages messages={this.state.messages.messages} />
    );
  }
});

The ChatConnectionContainer is a bit more involved. The point of that container is to pass the connection state to the different parts of the app. We have a bit of state here to use:

  • If there is no “peer” initialized, show the button that opens up the browser to accept connections, this will also generate an id for the browser.
  • If there is a no connection and the “peer” is set up, then we show the browser’s id and a form for the user to enter an id into to set up a collection with that client.
  • If there is a connection, present a disconnect button to the client.

Here is our client/_containers/ChatConnectionContainer.jsx:

ChatConnectionContainer = React.createClass({
  mixins: [Reflux.connect(PeerStore, 'peer')],

  render() {
    return (
      <div>
        <ChatOpenPeerButton 
            hasOpenPeer={this.state.peer.hasOpenPeer}/>
        <ChatConnection
            connectionKey={this.state.peer.key}
            hasOpenPeer={this.state.peer.hasOpenPeer}
            hasOpenConnection={this.state.peer.hasOpenConnection} />
        <ConnectedTo
            connectedToKey={this.state.peer.connectedToKey}
            hasOpenConnection={this.state.peer.hasOpenConnection} />
      </div>
    );
  }
});

Created our Components

This is where is gets fun and we make our views. We will add Bootstrap to help us style the project: meteor add twbs:bootstrap.

Alright, we have a lot of views to cover, but they mainly just present HTML and have a few onClick or onSubmit callbacks, so there is not much point in going through each view one by one. Here are the views and links to their source:

Tying it Together!

The last thing we need to do is set up our HTML file and a JSX to render our app with. Here is ChatRoom.html:

<head>
  <title>Chat Room</title>
</head>

<body>
  <h1>Chat Room</h1>

  <div id="render-target"></div>
</body>

And ChatRoom.jsx:

if (Meteor.isClient) {
  Meteor.startup(function () {
    Meteor.disconnect();
    ReactDOM.render(<ChatRoom />, document.getElementById('render-target'));
  });
}

And now you can run meteor and see the app live!

Our Github Repo with the Source for this app!