Lesson 33. Capstone: Adding a Chat Feature to Confetti Cuisinex

At this stage, my application’s foundation is complete. I can continue to improve existing functionality or build new features. Before the application is released to production and made available for everyone to use, Confetti Cuisine asked me to add an interesting feature to engage users. Without hesitation, I tell them that this is a perfect opportunity to build a chat feature within their Node.js application. Because I don’t want to complicate the application too much before deployment, I’ll keep the chat simple.

The chat will allow only users with accounts to communicate with one another. Every time a message is sent, I’ll save the message and associate it with the sender behind the scenes. Also, I’ll take advantage of socket.io to maintain an open connection between connected clients and the server for real-time communication. Through this library’s event-driven tools, I can emit events from the server to individual clients or all clients and from the client to the server. I could also emit events to a select group of clients, but I won’t need to implement that feature for this application.

Later, I’ll connect a chat icon in the navigation bar to animate whenever a chat message is sent. All users see this icon animate whenever a message is emitted. This icon doubles as a link to the chat page. It’s time to put the finishing touches on the Confetti Cuisine application.

33.1. Installing socket.io

First, I need to install the socket.io package. socket.io offers a JavaScript library that helps me build a real-time communication portal through its use of web sockets and long polling to maintain open connections between the client and the server. To install this package as a dependency, I run npm i socket.io -S in my project’s terminal window.

With this package installed, I need to require it in my main application file and on the client side.

33.2. Setting up socket.io on the server

Before I require socket.io, I need to save the server instance I’m creating with Express.js by assigning my app.listen line in main.js to a constant called server. Below this line, I’ll require socket.io in my project by adding const io = require("socket.io")(server). In this line, I’m simultaneously requiring the socket.io module and passing it the instance of my HTTP server used by Express.js. This way, the connection used by socket.io will share the same HTTP server as my main application. With my socket.io instance stored in the io constant, I can start using io to build out my chat functionality.

First, I set up a new controller for chat functionality. Though all the socket.io code can exist in main.js, it’s easier to read and maintain in its own controller. I start by requiring a new controller in main.js and passing it the io object by adding const chatController = require("./controllers/chatController")( io ) to the bottom of main.js. Next, I create chatController.js in my controllers folder. In this file, I add the code from listing 33.1.

I use the same io object created in main.js to listen for specific socket events. io.on ("connection") reacts when a new client connects to my socket server. client.on ("disconnect") reacts when a connected client disconnects. client.on("message") reacts when a client socket sends a custom message event to the server. I can name this event whatever I want. Because I’m working with chat messages, this event name seems to be appropriate. Within that last block, I use io.emit to send a message event back to all connected clients with the same data I received from an individual client. This way, everyone gets the same message that a single user submits.

Listing 33.1. Adding a chat action in chatController.js
module.exports = io => {                    1
  io.on("connection", client => {           2
    console.log("new connection");

    client.on("disconnect", () => {         3
      console.log("user disconnected");
    });

    client.on("message", (data) => {        4
      let messageAttributes = {
        content: data.content,
        userName: data.userName,
        user: data.userId
      };
      io.emit("message");                   5
    });
  });
};

  • 1 Export the chat controller contents.
  • 2 Listen for new user connections.
  • 3 Listen for when the user disconnects.
  • 4 Listen for a custom message event.
  • 5 Broadcast a message to all connected users.

The last line of code sends a specific set of message attributes that I expect to receive from the client. That is, I expect the client to emit a message event along with content, user name, and user ID. I need to send those three attributes from the view.

33.3. Setting up socket.io on the client

To build a successful chat connection, I need a view that facilitates the socket connection from the client side. I want to build my chat box in a view called chat.ejs that’s reachable at the /chat URL path. I create a new route for this path in my homeRoutes.js by adding router.get("/chat", homeController.chat).

Then I add the controller action to match this route by adding the code in the next listing to homeController.js. This code renders my chat.ejs view.

Listing 33.2. Adding a chat action in homeController.js
chat: (req, res) => {
  res.render("chat");          1
}

  • 1 Render a chat view.

To render my chat view, I need to build the view. I create a new file in my views folder called chat.ejs and add the code in listing 33.3. In this Embedded JavaScript (EJS) code, I first check for a currentUser in the view. Earlier, I set up the currentUser as a local variable to reflect an active user session through Passport.js. If a user is logged in, I display the chat form. The form contains three inputs. Two of the inputs are hidden but carry the user’s name and ID. I’ll use these inputs later to send the identity of the message author to the server. The first input is for the actual message content. Later, I’ll grab the value of this input as the content that I submit to the server.

Listing 33.3. Adding hidden fields in chat form in chat.ejs
<% if (currentUser) { %>                                 1
  <h1>Chat</h1>
  <form id="chatForm">
    <input id="chat-input" type="text">
    <input id="chat-user-id" type="hidden" value="<%=
 currentUser._id %>">
    <input id="chat-user-name" type="hidden" value="<%=
 currentUser.fullName %>">                             2
    <input type="submit" value="Send">
  </form>
  <div id="chat"></div>
<% } %>

  • 1 Check for a logged-in user.
  • 2 Add hidden fields containing user data.

The last pieces of this puzzle are adding some client-side JavaScript to monitor user interaction on this chat page and submitting the socket.io events needed to notify the server of new messages. In my public folder, I locate confettiCuisine.js and add to it the code in listing 33.4. In this code, I import socket.io for the client and add logic to interact over web sockets with my server. In the first code block, I use jQuery to handle my form’s submission and grab all the values from my form’s three inputs. I expect to receive these same three attributes in my server’s client.on("message") event handler.

The second block of code uses the socket object to represent the specific client on which this code will run. socket.on("message") sets up the client to listen for the message event, which emits from the server. When that event is emitted, each client takes the message delivered with that event and passes it to a custom displayMessage function that I created. This function locates my chat box in the view and prepends the message to the screen.

Listing 33.4. Adding socket.io on the client in confettiCuisine.js
const socket = io();                         1

$("#chatForm").submit(() => {                2
  let text = $("#chat-input").val(),
    userName = $("#chat-user-name").val(),
    userId = $("#chat-user-id").val();
  socket.emit("message", {
    content: text,
    userName: userName,
    userId: userId
  });                                        3
  $("#chat-input").val("");
  return false;
});

socket.on("message", (message) => {          4
  displayMessage(message);
});

let displayMessage = (message) => {          5
  $("#chat").prepend( $("<li>").html(message.content));
};

  • 1 Initialize socket.io on the client.
  • 2 Listen for a submit event in the chat form.
  • 3 Emit an event when the form is submitted.
  • 4 Listen for an event, and populate the chat box.
  • 5 Display messages in the chat box.

Before my application can use the io object in this file, I need to require it within my layout.ejs by adding the following script tag above my confettiCuisine.js import line: <script src="/socket.io/socket.io.js"></script>. This line loads socket.io for the client from my node_modules folder.

I’m ready to launch my application and see chat messages stream from one user to the next. With some styling, I can make it easier for users to distinguish their messages from others. I can also use the user’s name in the chat box so the sender’s name and message appear side by side. To do so, I modify my displayMessage function to print the user’s name, as shown in the next listing. I check whether the message being displayed belongs to that user by comparing the current user’s ID with the ID in the message object.

Listing 33.5. Pulling hidden field values from chat form in confettiCuisine.js
let displayMessage = (message) => {
  $("#chat").prepend( $("<li>").html(`
  <div class='message ${getCurrentUserClass(message.user)}'>
  <span class="user-name">
  ${message.userName}:
  </span>
  ${message.content}
  </div>
  `));                                         1
};

let getCurrentUserClass = (id) => {
  let userId = $("#chat-user-id").val();
  if (userId === id) return "current-user";    2
  else return "";
};

  • 1 Display the user’s name along with the message.
  • 2 Check whether the message belongs to the current user.

Next, I need to preserve these messages in my database by creating a Message model.

33.4. Creating a Message model

To ensure that my chat feature is worth using and a practical tool for users on the Confetti Cuisine application, the messages can’t disappear every time a user refreshes the page. To fix this problem, I’ll build a Message model to contain the message attributes in the chat form. I create a new message.js file in my project’s models folder and add the code in listing 33.6 to that file.

In this code, I’m defining a message schema that contains content, userName, and user properties. The content of the chat message is required, as are the user’s name and ID. In essence, every message needs some text and an author. If someone tries to save a message somehow without logging in and authenticating, the database won’t allow the data to save. I also set timestamps to true so that I can keep track of when the chat message was added to the database. This feature allows me to show the timestamp in the chat box, if I want.

Listing 33.6. Creating the message schema in message.js
const mongoose = require("mongoose"),
  { Schema } = require("mongoose");

const messageSchema = new Schema({
  content: {
    type: String,
    required: true
  },                                  1
  userName: {
    type: String,
    required: true
  },                                  2
  user: {
    type: Schema.Types.ObjectId,
    ref: "User",
    required: true
  }                                   3
}, { timestamps: true });             4

module.exports = mongoose.model("Message", messageSchema);

  • 1 Require content in each message.
  • 2 Require the user’s name with each message.
  • 3 Require a user ID with each message.
  • 4 Save the timestamp with each message.

This Mongoose model is ready for use in my chat controller. Effectively, when a new message arrives in my chat controller, I attempt to save it and then emit it to other users’ chats. I require this new model in chatController.js by adding const Message = require ("../models/message") to the top of the file. The code in my chatController.js block for client.on("message") is shown in listing 33.7. I start by using the same message-Attributes from earlier in the controller to create a new Message instance. Then I try to save that message. If the message saves successfully, I emit it to all connected sockets; otherwise, I log the error, and the message never gets sent out from the server.

Listing 33.7. Saving a message in chatController.js
client.on("message", (data) => {
  let messageAttributes = {
      content: data.content,
      userName: data.userName,
      user: data.userId
    },
    m = new Message(messageAttributes);       1
  m.save()                                    2
    .then(() => {
      io.emit("message",
 messageAttributes);                        3
    })
    .catch(error => console.log(`error: ${error.message}`));
});

  • 1 Create a new message object with messageAttributes.
  • 2 Save the message.
  • 3 Emit the message values if save is successful, and log any errors.

This code allows messages to save to my database, but chat message history still doesn’t appear for users who are connecting for the first time. I’ll correct that problem by loading older messages into my database.

33.5. Loading messages on connection

The second task in preserving messages in the chat box is maintaining a consistent number of messages from the chat’s history in the chat box. I decide to allow the chat box to contain the ten most recent chats at any given moment. To do so, I need to load those ten most recent chats from my database and emit them to every client as soon as they connect to the chat.

Within chatController.js, I add the code in listing 33.8 to find the ten most recent chat messages and emit them with a new custom event. I use sort({createdAt: -1}) to sort my database results in descending order. Then I append limit(10) to limit those results to the ten most recent. By emitting the custom "load all messages" event on the client socket, only newly connected users will have their chat boxes refresh with the latest chat messages. Then, I reverse the list of messages with messages.reverse() so that I can prepend them in the view.

Listing 33.8. Loading most recent messages in chatController.js
Message.find({})
  .sort({
    createdAt: -1
  })
  .limit(10)
  .then(messages => {                    1
    client.emit("load all messages",
 messages.reverse());                  2
  });

  • 1 Query the ten most recent messages.
  • 2 Emit a custom event with ten messages to the new socket only.

To handle the "load all messages" event on the client side, I add the event handler in the next listing to confettiCuisine.js. In this block of code, I listen for the "load all messages" event to occur. When it does emit, I cycle through the messages received on the client and individually display them in the chat box through the displayMessage function.

Listing 33.9. Displaying most recent messages in confettiCuisine.js
socket.on("load all messages", (data) => {      1
  data.forEach(message => {
    displayMessage(message);                    2
  });
});

  • 1 Handle ‘load all messages’ by parsing incoming data.
  • 2 Send each message to displayMessage to display in the chat box.

The chat is finally complete and ready to test locally. To mimic two separate users communicating, I relaunch my application and log in on two separate web browsers. I navigate to the chat page and see that my chats are being sent in real time over my Node.js application with socket.io.

33.6. Setting up the chat icon

I want to make one final addition to this application: an icon that lets users elsewhere in the application know when the chat is active. I can easily add this feature with the existing socket.io event set up. All I need to do is add an icon to the navigation bar in my application by adding <a href="/chat" class="chat-icon">@</a> to layout.ejs. With this line alone, I have an icon in my navigation bar that links to the /chat route.

Next, I animate the icon by having it flash twice whenever a chat message is sent. Because I’m already emitting the message event from the server every time a new message is submitted, I can add the icon animation to the client’s handler for that event.

In confettiCuisine.js, I modify the socket.on("message") code block to look like the code in the following listing. In this code, I display the message in the chat box as usual and additionally target an element with the chat-icon class. This element represents my chat icon in the navigation bar. Then I rapidly fade the icon out and back in, twice.

Listing 33.10. Animating chat icon when messages are sent in confettiCuisine.js
socket.on("message", (message) => {
  displayMessage(message);
  for (let i = 0; i < 2; i++) {
    $(".chat-icon").fadeOut(200).fadeIn(200);        1
  }
});

  • 1 Animate the chat icon to flash when a message is sent.

With this extra feature, users have some indication that conversations are taking place on the chat page.

I could add to this chat feature in plenty of ways. I could create separate chats for each Confetti Cuisine class, for example, or use socket.io events to notify users when they’ve been tagged in a chat. I’ll consider implementing these features in the future.

Summary

In this capstone exercise, I added a real-time chat feature to my Confetti Cuisine application. I used socket.io to simplify connections between the server and multiple clients. I used some built-in and custom events to transfer data between open sockets. At the end, I added a feature to notify users who aren’t in the chat room that others are actively communicating. With this feature added, I’m ready to deploy the application.

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

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