1. Introduction

In the following tutorial, we'll learn how to use the sync plugin to create a simple chat-room application.
You could achieve a similar result using plain WebSockets (see the WebSocket tutorial). But in this tutorial, we'll take a different approach, and use Crails' Channels.

Channels are based on WebSocket, and allow both the server and each client to broadcast messages to both the server and every other connected client. The read/write access of each channel can also be restricted depending on the user. Sounds perfect for a chat application ! Let's dive in !

1.1 Preparations

Create the chatnnel application with the following command:

$ crails new -n chatnnel -p html
cd chatnnel

Then add the sync plugin to the application:

$ crails plugins sync install

2. Public chatroom

2.1 HTML

We'll start by creating a simple view to render our chatroom.

app/views/chatroom.html
std::string @room_id;
// END LINKING
<html>
  <head>
    <meta charset="utf-8" />
    <title>Chatnell room - <%= room_id %></title>
  </head>
  <body data-room-id="">
    <h1>Chatnnel room</h1>
    <h2><%= room_id %></h2>

    <textarea placeholder="Write your message..."></textarea>
    <button id="send-button">Send</button>

    <table>
      <tbody id="message-container">
      </tbody>
    </table>
  </body>
</html>

Pretty simple stuff: a textarea to input text, and a table to display the message received.

We also declared a room_id shared property: that's because we are going to allow the application to host as many rooms as the user may create.

2.2 JavaScript

We also need some JavaScript to connect to the channel and update the message list. This is achieved using WebSockets. Open our html view again and add the following javascript below the <table> element:

app/views/chatroom.html
    <script>
      const socket = new WebSocket("ws://localhost:3001/chatroom/<%= room_id %>/channel");
      socket.onmessage = function(event) {
        const list = document.querySelector("#message-container");
        const row = document.createElement("tr");
        const messageCell = document.createElement("td");
        messageCell.textContent = event.data;
        row.appendChild(messageCell);
        list.prepend(row);
      };

      document.querySelector("#send-button").addEventListener("click", function() {
        const input = document.querySelector("textarea");
        const message = input.value;
        socket.send(message);
        input.value = "";
      });
    </script>

Simple stuff again: we connect a chatroom to the /chatroom/:room_id/channel UID, which we will bind later in our application's router. When the user press the button, we send the textarea content to the WebSocket, and when the WebSocket sends us a message, we add a new row to #message-container displaying the received message.

Messages sent to a channel are broadcasted to every connected client, including the one who sent the message. Hence why we don't need to append the message in the button click event handler: it will be appended when it comes back through the WebSocket's message event handler.

2.2 Controller

At this point, the chatroom client is almost ready: we just need a simple controller to render the HTML we created earlier. Generate a basic controller using the following command:

$ crails scaffold controller -n chatroom

Then declare a show action method:

app/controllers/chatroom.hpp
#pragma once
#include "app/controllers/application.hpp"

class ChatroomController : public ApplicationController
{
public:
  ...

  // Declare the show action:
  void show();
};

And add the corresponding implementation in the controller source file:

app/controllers/chatroom.cpp
#include "chatroom.hpp"
#include <crails/params.hpp>

...

void ChatroomController::show()
{
  render("chatroom", {
    {"room_id", params["room_id"].as<std::string>()}
  });
}

2.3 Router

Lastly, we need to two routes to our router: one to serve the chatroom html, the other to serve as the endpoint for the client's WebSockets:

app/routes.cpp
#include <crails/router.hpp>
#include <crails/sync/channel_actions.hpp> // header for channel routes
#include "controllers/chatroom.hpp" // our controller

void Crails::Router::initialize()
{
  match_action("GET", "/chatroom/:room_id", ChatroomController, show);
  match_sync_channel("/chatroom/:room_id/channel", Crails::Sync::ChannelClient);
}

And we're done ! Build and start the server, then go at http://localhost:3001/chatroom/test-room to try it out !

3. Idenitfy clients

In our current implementations, messages are broadcasted to each client, but there are no way to tell who emitted which messages. In this chapter, we'll update our chatroom application so that each client is identified with a username.

4. Private chatroom

All our chatrooms are publicly accessible. But what if you want your channels to be only accessible to some users ? Channels can be protected so that users cannot read or/and write, using passwords.