# Further additions to the Node.js/socket.io chat app

Source: https://tpiros.dev/blog/further-additions-to-the-node-jssocket-io-chat-app

Merry Christmas and Happy Holidays! I'm using this quiet stretch to ship some projects I've been sitting on. And yes, that means another update to the node.js/socket.io chat application.

This project started as a learning exercise to get a grip on node.js and websockets. Eight months later, I'm still building on the same foundation. The project is [available on GitHub](https://github.com/tpiros/advanced-chat) and now has these features:

- People join the chat server after entering their names
- Usernames are unique. If one's taken, a new suggestion gets generated
- User agent and geo location are both detected
- People can set up a room. Room names are unique. One person can create one room and join one room
- Users have to join a room to chat, except for the whisper feature
- Whisper messages are private messages sent between two users
- With a WebSpeech-enabled browser, users can record their messages
- Users can leave a room and/or disconnect from the server anytime
- **New:** People joining a room will see the past 10 messages (chat history)
- **New:** People will see an 'is typing' message when someone is typing

That list gives away what I'm covering here: chat history and a typing indicator.

Before getting into the details, a quick thank you to the people reading my articles. Last week someone reposted my article on Google+ and wrote: "A great article ... Also, check his past posts and the source code. He's writing a chat application, which is now able to detect GeoLocation and User Agents as well. Very nicely refactored using underscore.js."

Let's start with chat history. The mechanism is simple. I created a global variable `var chatHistory = {};` (a plain JavaScript collection). When a room gets created, inside the `createRoom` function, I add an array keyed by the room name. This keeps historical messages encapsulated per room:

```js
chatHistory[socket.room] = [];
```

Messages get pushed into this array inside the `chat` function. I'm using [underscore.js](http://underscorejs.org) to check the array size, and if it exceeds 10, old messages get sliced off with `splice`:

```js
if (_.size(chatHistory[socket.room]) > 10) {
  chatHistory[socket.room].splice(0, 1);
} else {
  chatHistory[socket.room].push(people[socket.id].name + ': ' + msg);
}
```

One thing I nearly forgot: deleting the messages when a room gets destroyed. Easy enough:

```js
delete chatHistory[room.name];
```

The 'is typing' feature was trickier. It required changes on both the frontend and the backend. Let's tackle the frontend first, since it's more involved.

Here's the gist:

- A `typing` variable starts as false
- When a user hits a key, a message fires to the backend, a timeout starts, and a visual indicator appears
- If the timeout hits its limit (5000 milliseconds), the backend gets contacted again, and we assume the user stopped typing
- If the user keeps pressing keys, we assume they're still typing

The code:

```js
var typing = false;
var timeout = undefined;

function timeoutFunction() {
  typing = false;
  socket.emit('typing', false);
}

$('#msg').keypress(function (e) {
  if (e.which !== 13) {
    if (typing === false && myRoomID !== null && $('#msg').is(':focus')) {
      typing = true;
      socket.emit('typing', true);
    } else {
      clearTimeout(timeout);
      timeout = setTimeout(timeoutFunction, 5000);
    }
  }
});

socket.on('isTyping', function (data) {
  if (data.isTyping) {
    if ($('#' + data.person + '').length === 0) {
      $('#updates').append(
        "<li id='" +
          data.person +
          "'><span class='text-muted'><small><i class='fa fa-keyboard-o'></i>" +
          data.person +
          ' is typing.</small></li>'
      );
      timeout = setTimeout(timeoutFunction, 5000);
    }
  } else {
    $('#' + data.person + '').remove();
  }
});
```

That's not enough on its own, though. The timer doesn't reset when the user hits enter. To fix that, the existing `chat` function needed extending (still on the frontend):

```js
socket.on('chat', function (person, msg) {
  $('#msgs').append(
    "<li><strong><span class='text-success'>" +
      person.name +
      '</span></strong>: ' +
      msg +
      '</li>'
  );
  //clear typing field
  $('#' + person.name + '').remove();
  clearTimeout(timeout);
  timeout = setTimeout(timeoutFunction, 0);
});
```

I'm using connected users' names as unique IDs for the `li` elements. Not best practice, but it works for now.

On the backend, I added a single short function. It re-emits the `true` and `false` values via the `isTyping` event and checks that a valid user is online. Without that first `if` statement, the function would fire for people who haven't entered a username yet but are connected. Think of it as a safety net:

```js
socket.on('typing', function (data) {
  if (typeof people[socket.id] !== 'undefined')
    io.sockets
      .in(socket.room)
      .emit('isTyping', { isTyping: data, person: people[socket.id].name });
});
```

I've added one more tweak. Previously, a function triggered the message container to auto-scroll when someone sent a message (i.e. the `chatForm` was submitted). The problem? It only scrolled for the sender's window, not other clients. I pulled that function out of the `submit` method and made it global by binding to the `DOMSubtreeModified` event. Now all divs scroll simultaneously for all connected clients. The bad news? No support before IE8:

```js
$('#conversation').bind('DOMSubtreeModified', function () {
  $('#conversation').animate({
    scrollTop: $('#conversation')[0].scrollHeight,
  });
});
```

Here are a few screenshots:

The [latest codebase is on GitHub](https://github.com/tpiros/advanced-chat) as always.
