Inter-user chat and commenting for Notes

This is cool, we now have real-time updates in Notes as we edit or delete or create notes. Let's now take it to the next level and implement something akin to inter-user chatting.

It's possible to pivot our Notes application concept and take it in the direction of a social network. In most such networks, users post thing (notes, pictures, videos, and so on), and other users comment on those things. Done well, these basic elements can develop a large community of people sharing notes with each other. While the Notes application is kind of a toy, it's not too terribly far from being a basic social network. Commenting the way we will do now is a tiny step in that direction.

On each note page, we'll have an area to display messages from Notes users. Each message will show the user name, a time stamp, and their message. We'll also need a method for users to post a message, and we'll also allow users to delete messages.

Each of those operations will be performed without refreshing the screen. Instead, code running inside the web page will send commands to/from the server and take actions dynamically.

Let's get started.

Data model for storing messages

We need to start by implementing a data model for storing messages. The basic fields required are a unique ID, the username of the person sending the message, the namespace the message is sent to, their message, and finally a timestamp for when the message was sent. As messages are received or deleted, events must be emitted from the model so we can do the right thing on the webpage.

This model implementation will be written for Sequelize. If you prefer a different storage solution, you can re-implement the same API on other data storage systems by all means.

Create a new file, models/messages-sequelize.js, containing the following:

'use strict';

const Sequelize = require("sequelize");
const jsyaml    = require('js-yaml');
const fs        = require('fs');
const util      = require('util');
const EventEmitter = require('events');

class MessagesEmitter extends EventEmitter {}

const log   = require('debug')('messages:model-messages');
const error = require('debug')('messages:error');

var SQMessage;
var sequlz;

module.exports = new MessagesEmitter();

This sets up the modules being used and also initializes the EventEmitter interface. The module itself will be an EventEmitter interface because the object is assigned to module.exports.

var connectDB = function() {
    if (SQMessage) return SQMessage.sync();
    
    return new Promise((resolve, reject) => {
        fs.readFile(process.env.SEQUELIZE_CONNECT, 'utf8',
        (err, data) => {
            if (err) reject(err);
            else resolve(data);
        });
    })
    .then(yamltext => jsyaml.safeLoad(yamltext, 'utf8'))
    .then(params => {
        if (!sequlz) sequlz = new Sequelize(params.dbname, params.username, params.password, params.params);
        
        if (!SQMessage) SQMessage = sequlz.define('Message', {
            id: { type: Sequelize.INTEGER, autoIncrement: true,
                                           primaryKey: true },
            from: Sequelize.STRING,
            namespace: Sequelize.STRING,
            message: Sequelize.STRING(1024),
            timestamp: Sequelize.DATE
        });
        return SQMessage.sync();
    });
};

This defines our message schema in the database. We'll use the same database that we used for Notes, but the messages will be stored in their own table.

The id field won't be supplied by the caller; instead, it will be autogenerated. Because it is an autoIncrement field, each message that's added will be assigned a new id number by the database:

module.exports.postMessage = function(from, namespace, message) {
    return connectDB()
    .then(SQMessage => SQMessage.create({
        from, namespace, message, timestamp: new Date()
    }))
    .then(newmsg => {
        var toEmit = {
            id: newmsg.id,
            from: newmsg.from,
            namespace: newmsg.namespace,
            message: newmsg.message,
            timestamp: newmsg.timestamp
        };
        module.exports.emit('newmessage', toEmit);
    });
};

This is to be called when a user posts a new comment/message. We first store it in the database, and then we emit an event saying the message was created:

module.exports.destroyMessage = function(id, namespace) {
    return connectDB()
    .then(SQMessage => SQMessage.find({ where: { id } }))
    .then(msg => msg.destroy())
    .then(result => {
        module.exports.emit('destroymessage', { id, namespace });
    });
};

This is to be called when a user requests that a message should be deleted. With Sequelize, we must first find the message and then delete it by calling its destroy method. Once that's done, we emit a message saying the message was destroyed:

module.exports.recentMessages = function(namespace) {
    return connectDB().then(SQMessage => {
        return SQMessage.findAll({
            where: { namespace },
            order: 'timestamp DESC',
            limit: 20
        });
    })
    .then(messages => {
        return messages.map(message => {
            return {
                id: message.id,
                from: message.from,
                namespace: message.namespace,
                message: message.message,
                timestamp: message.timestamp
            };
        });
    });
};

While this is meant to be called when viewing a note, it is generalized to work for any Socket.IO namespace. It finds the most recent 20 messages associated with the given namespace and returns a cleaned up list to the caller.

Adding messages to the Notes router

Now that we can store messages in the database, let's integrate this into the Notes router module.

In routes/notes.js, add this to the require statements:

const messagesModel = require('../models/messages-sequelize');

If you wish to implement a different data storage model for messages, you'll need to change this require statement. One should consider using an environment variable to specify the module name, as we've done elsewhere:

// Save incoming message to message pool, then broadcast it
router.post('/make-comment', usersRouter.ensureAuthenticated,
(req, res, next) => {
    messagesModel.postMessage(req.body.from,
          req.body.namespace, req.body.message)
    .then(results => { res.status(200).json({ }); })
    .catch(err => { res.status(500).end(err.stack); });
});

// Delete the indicated message
router.post('/del-message', usersRouter.ensureAuthenticated,
(req, res, next) => {
    messagesModel.destroyMessage(req.body.id, req.body.namespace)
    .then(results => { res.status(200).json({ }); })
    .catch(err => { res.status(500).end(err.stack); });
});

This pair of routes, /notes/make-comment and /notes/del-message, is used to post a new comment or delete an existing one. Each calls the corresponding data model function and then sends an appropriate response back to the caller.

Remember that postMessage stores a message in the database, and then it turns around and emits that message to other browsers. Likewise, destroyMessage deletes the message from the database, then emits a message to other browsers saying that the message has been deleted. Finally, the results from recentMessages will reflect the current set of messages in the database.

Both of these will be called by AJAX code in the browser.

module.exports.socketio = function(io) {
    var nspView = io.of('/note/view');
    nspView.on('connection', function(socket) {
        // 'cb' is a function sent from the browser, to which we
        // send the messages for the named note.
        socket.on('getnotemessages', (namespace, cb) => {
            messagesModel.recentMessages(namespace)
            .then(cb)
            .catch(err => console.error(err.stack));
        });
    });
 ..
    messagesModel.on('newmessage',  newmsg => {
        forNoteViewClients(socket => {
           socket.emit('newmessage', newmsg); 
        });
    });
    messagesModel.on('destroymessage',  data => {
        forNoteViewClients(socket => {
            socket.emit('destroymessage', data); 
        });
    });
 ..
};

This is the Socket.IO glue code, which we will add to the code we looked at earlier.

The getnotemessages message from the browser requests the list of messages for the given note. This calls the recentMessages function in the model. This uses a feature of Socket.IO where the client can pass a callback function, and server-side Socket.IO code can invoke that callback giving it some data.

We also listen to the newmessage and destroymessage messages emitted by the messages model, sending corresponding messages to the browser. These are sent using the method described earlier.

Changing the note view template for messages

We need to dive back into views/noteview.ejs with more changes so that we can view, create, and delete messages. This time we will add a lot of code, including using a Bootstrap Modal popup to get the message, several AJAX calls to communicate with the server, and of course, more Socket.IO stuff.

Using a Modal window to compose messages

The Bootstrap framework has a Modal component that serves a similar purpose to Modal dialogs in desktop applications. You pop up the Modal, it prevents interaction with other parts of the webpage, you enter stuff into fields in the Modal, and then click a button to make it close.

This new segment of code replaces the existing segment defining the Edit and Delete buttons:

<% if (notekey && user) { %>
<div class="row">
<div class="btn-group col-sm-12">
    <a class="btn btn-default" href="/notes/destroy?key=<%= notekey %>" role="button">Delete</a>
    <a class="btn btn-default" href="/notes/edit?key=<%= notekey %>" role="button">Edit</a>
    <button type="button" class="btn btn-default"
            data-toggle="modal"
            data-target=".notes-comment-modal">Comment</button>
</div>
</div>
    
<div class="modal fade notes-comment-modal" tabindex="-1" role="dialog" aria-labelledby="noteCommentModalLabel">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
                <h4 class="modal-title" id="noteCommentModalLabel">Leave a Comment</h4>
            </div>
            <div class="modal-body">
                <form id="submit-comment" class="well"
                      data-async data-target="#rating-modal"
                      action="/notes/make-comment" method="POST">
                <input type="hidden" name="from" 
                       value="<%= user.id %>">
                <input type="hidden" name="namespace"
                       value="/view-<%= notekey %>">
                <input type="hidden" name="key" 
                       value="<%= notekey %>">
                <fieldset>
                <div class="form-group">
                  <label for="noteCommentTextArea">
                      Your Excellent Thoughts, Please</label>
                  <textarea id="noteCommentTextArea" name="message" class="form-control" rows="3"></textarea>
                </div>
                    
                <div class="form-group">
                    <div class="col-sm-offset-2 col-sm-10">
                        <button id="submitNewComment" type="submit" class="btn btn-default">Make Comment</button>
                    </div>
                </div>
                </fieldset>
                </form>
            </div>
        </div>
    </div>
</div>
<% } %>

This adds support for posting comments on a note. The user will see a Modal pop-up window in which they write their comment.

We added a new button labeled Comment that the user will click to start the process of posting a message. This button is connected to the Modal by way of the class name specified in the data-target attribute. Note that the class name matches one of the class names on the outermost div wrapping the Modal. This structure of div elements and class names are from the Bootstrap website at http://getbootstrap.com/javascript/#modals.

The key portion of this is the HTML form contained within the div.modal-body element. It's a straightforward normal Bootstrap augmented form with a normal Submit button at the bottom. A few hidden input elements are used to pass extra information inside the request.

With the HTML set up this way, Bootstrap will ensure that this Modal is triggered when the user clicks on the Comment button. The user can close the Modal by clicking on the Close button. Otherwise, it's up to us to implement code to handle the form submission using AJAX so that it doesn't cause the page to reload:

<% if (user) { %>
<div id="noteMessages" style="display: none"></div>
<% } %>
<% include footer %>

This gives us a place to display the messages.

Sending, displaying, and deleting messages

Note that these code snippets are wrapped with if statements. The effect is so that certain user interface elements are displayed only to sufficiently privileged users. A user that isn't logged in perhaps shouldn't see the messages, and they certainly shouldn't be able to post a message.

Now we have a lot of code to add to this block:

<% if (notekey) { %>
<script src="/socket.io/socket.io.js"></script>
<script>
$(document).ready(function () {  ..  });
</script>

We need to handle the form submission for posting a new comment, get the recent messages when first viewing a note, listen for events from the server about new messages or deleted messages, render the messages on the screen, and handle requests to delete a message:

$(document).ready(function () {
    io('/view').on('noteupdate', function(note) { .. });
    io('/view').on('notedestroy', function(data) { .. });
    <% if (user) { %>
    // Request the recent list of messages
    io('/view').emit('getnotemessages', '/view-<%= notekey %>',
    function(msgs) {
        console.log("RECEIVE getnotemessages reply");
        $('#noteMessages').empty();
        if (msgs.length > 0) {
            msgs.forEach(function(newmsg) {
                $('#noteMessages').append(formatMessage(newmsg));
            });
            $('#noteMessages').show();
            connectMsgDelButton();
        } else $('#noteMessages').hide();
    });
    // Handler for the .message-del-button's
    var connectMsgDelButton = function() {
        $('.message-del-button').on('click', function(event) {
            $.post('/notes/del-message', {
                id: $(this).data("id"),
                namespace: $(this).data("namespace")
            },
            function(response) { });
            event.preventDefault();
        });
    };
    // Emit the code to show a message, and the 
    // buttons that will sit next to it.
    var formatMessage = function(newmsg) {
        return '<p id="note-message-'
            + newmsg.id +'" class="well"><strong>'
            + newmsg.from +'</strong>: '
            + newmsg.message
            +' <small style="float: right">'
            + newmsg.timestamp +'</small>'
            +' <button style="float: right" type="button" class="btn btn-primary btn-xs message-del-button" data-id="'
            + newmsg.id +'" data-namespace="'
            + newmsg.namespace +'">Delete</button></p>';
    };
    // Act on newmessage and destroymessage events
    io('/view').on('newmessage', function(newmsg) {
        if (newmsg.namespace === '/view-<%= notekey %>') {
            $('#noteMessages').prepend(formatMessage(newmsg));
            connectMsgDelButton();
        }
    });
    io('/view').on('destroymessage', function(data) {
        if (data.namespace === '/view-<%= notekey %>') {
            $('#noteMessages #note-message-'+ data.id).remove();
        }
    });
    // Handle form submission for the comment form
    $('form#submit-comment').submit(function(event) {
        // Abort any pending request
        if (request) { request.abort(); }
        
        var $form = $('form#submit-comment');
        var $target = $($form.attr('data-target'));

        var request = $.ajax({
            type: $form.attr('method'),
            url:  $form.attr('action'),
            data: $form.serialize()
        });
        
        request.done(function (response, textStatus, jqXHR) { });
        request.fail(function (jqXHR, textStatus, errorThrown) {
            alert("ERROR "+ jqXHR.responseText);
        });
        request.always(function () {
            $('.notes-comment-modal').modal('hide');
        });
        
        event.preventDefault();
    });
    <% } %>
});

The code within $('form#submit-comment').submit handles the form submission for the comment form. Because we already have jQuery available, we can use its AJAX support to POST a request to the server without causing a page reload.

Using event.preventDefault, we ensure that the default action does not occur. For the FORM submission, that means the browser page does not reload. What happens is an HTTP POST is sent to /notes/make-comment with a data payload consisting of the values of the form's input elements. Included in those values are three hidden inputs, from, namespace, and key, providing useful identification data.

If you refer to the /notes/make-comment route definition, this calls messagesModel.postMessage to store the message in the database. That function then posts an event, newmessage, which our server-side code forwards to any browser that's connected to the namespace. Shortly, we'll show the response to that event.

When the page is first loaded, we want to retrieve the current messages. This is kicked off with io('/view').emit('getnotemessages', ... This function, as the name implies, sends a getnotemessages message to the server. We showed the implementation of the server-side handler for this message earlier, in routes/notes.js.

If you remember, we said that Socket.IO supports providing a callback function that is called by the server in response to an event. You simply pass a function as the last parameter to a .emit call. That function is made available at the other end of the communication to be called when appropriate. To make this clear, we have a callback function on the browser being invoked by server-side code.

In this case, our callback function takes a list of messages and displays them on the screen. It uses jQuery DOM manipulation to erase any existing messages, then render each message into the messages area using the formatMessage function. The message display area (#noteMessages) is also hidden or shown depending on whether we have messages to display.

The message display template, in formatMessage, is straightforward. It uses a Bootstrap well to give a nice visual effect. And, there is a button for deleting messages.

In formatMessage we created a Delete button for each message. Those buttons need an event handler, and the event handler is set up using the connectMsgDelButton function. In this case, we send an HTTP POST request to /notes/del-message. We again use the jQuery AJAX support again to post that HTTP request.

The /notes/del-message route in turn calls messagesModel.destroyMessage to do the deed. That function then emits an event, destroymessage, which gets sent back to the browser. As you see here, the destroymessage event handler causes the corresponding message to be removed using jQuery DOM manipulation. We were careful to add an id= attribute to every message to make removal easy.

Since the flip side of destruction is creation, we need to have the newmessage event handler sitting next to the destroymessage event handler. It also uses jQuery DOM manipulation to insert the new message into the #noteMessages area.

Running Notes and passing messages

That was a lot of code, but we now have the ability to compose messages, display them on the screen, and delete them, all with no page reloads. Watch out Facebook! We're coming for you now that the Notes application is becoming a social network!

Okay, maybe not. We do need to see what this can do.

You can run the application as we did earlier, first starting the user authentication server in one command-line window and the Notes application in another:

Running Notes and passing messages

This is what a note might look like after entering a few comments.

Running Notes and passing messages

While entering a message, the Modal looks like this.

Try this with multiple browser windows viewing the same note or different notes. This way, you can verify that notes show up only on the corresponding note window.

Other applications of Modal windows

We used a Modal and some AJAX code to avoid one page reload due to a form submission. In the Notes application, as it stands, a similar technique could be used when creating a new note, editing existing notes, and deleting existing notes. In each case, we would use a Modal, some AJAX code to handle the form submission, and some jQuery code to update the page without causing a reload.

But wait, that's not all. Consider the sort of dynamic real-time user interface wizardry on the popular social networks. Imagine what events and/or AJAX calls are required.

When you click on an image in Twitter, it pops up, you guessed it, a Modal window to show a larger version of the image. The Twitter Compose new Tweet window is also a Modal window. Facebook uses many different Modal windows, such as when sharing a post, reporting a spam post, or while doing a lot of other things Facebook's designers deem to require a pop-up window.

Socket.IO, as we've seen, gives us a rich foundation of event passing between server and client, which can build multiuser multichannel communication experiences for your users.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset