Introduction
In this part we will add some new events to the chat application. More precisely, we will start sending an event when a new user joins the chat and when a user leaves the chat. Both will be displayed in the chat div, together with the messages.
Since we have already added timestamping to the messages in part 4 of our series of tutorials, we will leverage what was already implemented to also timestamp these two new events.
It’s important to mention that we won’t need to change anything in the ESP32 Arduino code. Our implementation is very generic and basically parses a websocket message, adds a timestamp and broadcasts it to all the clients. Thus, with the implementation we are going to follow, the ESP32 doesn’t need to know which chat event it is handling. Note however that this is a simplification and, in a real application scenario, it would be a good idea to check the actual content of the messages on the server side.
In our case, it will be each client that will be responsible for understanding the type of chat event received and display it adequately in the chat div.
In order to simplify the interpretation of the events, we will assign a number to each type of chat event:
- 1 – Client connected
- 2 – Client disconnected
- 3 – Message received
The client that triggers the event will be responsible for setting a field called “type” in the websocket JSON messages. Then, after the ESP32 broadcasts the message, all the clients (including the one that originally triggered the event) will use this value to know how to handle the event.
The tests shown below were performed on a ESP32-E FireBeetle board from DFRobot. The Arduino core version used was 2.0.0 and the Arduino IDE version was 1.8.15, working on Windows 8.1. The code was also tested on Platformio.
The CSS code
To keep our code changes to a minimum, we will reuse the “chat-tag” class we have used in the previous tutorials, which is shown below:
.chat-tag{
background-color: #aee5cb;
border-radius: var(--border-radius);
display: inline-block;
margin: 10px;
padding: 4px;
}
Nonetheless, if we use the same background color for all the 3 events (user joined, user left and message received), it will be visually harder to distinguish these events. As such, we will assign a different color to each of them.
To reuse the code, we will keep all the properties in the “chat-tag” class, except the “background-color“. This class will represent the base properties of the entity and thus we must add the “chat-tag” class to all of the 3 types of events.
Then, we will create a distinct class per each of the 3 events, which will only set the background color of the tag. Naturally, each one of these modifier classes will set a different background color. You can read more about this approach of organizing your CSS classes here.
.chat-tag{
border-radius: var(--border-radius);
display: inline-block;
margin: 10px;
padding: 4px;
}
.chat-tag--message{
background-color: #aee5cb;
}
.chat-tag--join{
background-color: #37fdfd;
}
.chat-tag--left{
background-color: #8f989f;
}
This will be the only change in our CSS, but below you can see the full code with the change, as a reminder from what we covered in the previous tutorials.
:root {
--border-radius: 10px;
}
.u-display--flex{
display:flex;
}
.u-flex-grow--1{
flex-grow: 1;
}
.connect-row{
margin-bottom: 20px;
}
.main-container{
border-radius: var(--border-radius);
box-shadow: 0px 1px 34px #999;
max-width: 500px;
margin: 50px auto;
padding: 30px 54px;
}
.btn{
background-color:#4eb5f1;
border-radius: var(--border-radius);
border: none;
color: #ffffff;
margin: 0 4px 4px 0;
min-width: 120px;
padding: 8px 12px;
}
.btn:disabled{
background-color: #cccccc;
}
.btn:hover:enabled{
background-color: #4095c6;
}
.text-input{
border: 1px solid #ced4da;
border-radius: var(--border-radius);
margin: 0 4px 4px 0;
padding: 4px 12px;
}
.text-input:disabled{
background-color: #e9ecef;
}
.text-input:focus{
outline: 1px solid #ced4da;
}
.chat-container{
border: 1px solid #ced4da;
border-radius: var(--border-radius);
margin: 0 4px 4px 0;
padding: 6px 2px;
}
.chat{
height: 300px;
overflow: auto;
}
.chat-tag{
border-radius: var(--border-radius);
display: inline-block;
margin: 10px;
padding: 4px;
}
.chat-tag--message{
background-color: #aee5cb;
}
.chat-tag--join{
background-color: #37fdfd;
}
.chat-tag--left{
background-color: #8f989f;
}
.title{
margin-top: 50px;
text-align: center;
}
The JavaScript code
We will start by changing the onopen websocket event handling function. There, besides the code we already had to enable / disable the UI elements, we will now send a new message to the websocket server indicating a new user has joined.
This message will be very simple and will contain the type of event (new user joined) and the name of the new user. Accordingly to what we have defined before, this event type will correspond to the value 1. Also, when the user clicked the “Connect” button, the name introduced in the user name text box was already assigned to the JavaScript “name” variable, which we will use here to populate our message.
Note that, as we have been seen in previous tutorials, we build our message as a JavaScript object, which we serialize after to send using the websocket.
const objToSend = {
type: 1,
name: name
}
const serializedObj = JSON.stringify(objToSend);
ws.send(serializedObj);
The complete handling function for the websocket connected event is shown below.
ws.onopen = function() {
document.getElementById("inputText").disabled = false;
document.getElementById("sendButton").disabled = false;
document.getElementById("disconnectButton").disabled = false;
document.getElementById("connectButton").disabled = true;
document.getElementById("nameText").disabled = true;
const objToSend = {
type: 1,
name: name
}
const serializedObj = JSON.stringify(objToSend);
ws.send(serializedObj);
};
Next we will adapt the CloseWebsocket function, which is invoked when the user clicks the “Disconnect” button. Similarly to what we have done before, we will send a message to the server using the websocket, this time with type 2.
const objToSend = {
type: 2,
name: name
}
const serializedObj = JSON.stringify(objToSend);
ws.send(serializedObj);
The complete CloseWebsocket function is shown below.
function CloseWebsocket(){
const objToSend = {
type: 2,
name: name
}
const serializedObj = JSON.stringify(objToSend);
ws.send(serializedObj);
ws.close();
}
We will also need to adapt the SendData function, which is responsible for sending the message of the user. Since we were previously already building a message and sending it, we just need to add the type 3.
const objToSend = {
type: 3,
msg: msg,
name: name
}
The complete function is shown below.
function SendData(){
const inputTextElement = document.getElementById("inputText");
const msg = inputTextElement.value;
inputTextElement.value = '';
const objToSend = {
type: 3,
msg: msg,
name: name
}
const serializedObj = JSON.stringify(objToSend);
ws.send(serializedObj);
}
To finalize, we will adapt the onmessage websocket event handler. This event handler will now need to be able to change the UI depending on the event type received on the message.
Like before, we start by parsing the incoming wbesocket message.
const receivedObj = JSON.parse(event.data);
Then, we will invoke a function called GetChatLine (which we will implement below) that receives as input the parsed message and returns the elements that correspond to a new line of the chat. These elements will already be returned with the correct structure and style, depending on the received message type.
const chatLine = GetChatLine(receivedObj);
Then we will append the received chat line elements to the chat div.
const chatDiv = document.getElementById("chatDiv");
chatDiv.appendChild(chatLine);
The complete message handling function is shown below.
ws.onmessage = function(event) {
const receivedObj = JSON.parse(event.data);
const chatLine = GetChatLine(receivedObj);
const chatDiv = document.getElementById("chatDiv");
chatDiv.appendChild(chatLine);
};
We will now analyze the implementation of the GetChatLine function.
function GetChatLine(event){
// Handle event type
}
All the chat events (connect, disconnect, message) will have a tag. So, we will create a span element.
const newTag = document.createElement('span');
Only the message event will need an element to display the message. So, we will define a variable that is initialized with null and later, in case of a chat message received, we will assign it an element.
let newMsg = null;
All tags will have the “chat-tag” class name, since the CSS we defined in this class is common to all the events.
newTag.classList.add('chat-tag');
Now we will handle the different events in different IF conditions. For a new user connected or a user disconnected events, we will simply add the corresponding modifier class to the tag element and set the text of the tag with an informative message, which includes the name of the user and the timestamp, plus the description of what happened.
In the case of the chat message received, we will also set the tag modifier class and its content (name of the user and timestamp). Additionally, we will create a span element to hold the actual content of the message, and assign it to the newMsg variable we previously initialized to null.
if(event.type === 1){
newTag.classList.add('chat-tag--join');
newTag.textContent = `${event.timestamp} ${event.name} has joined the chat.`;
}else if(event.type === 2){
newTag.classList.add('chat-tag--left');
newTag.textContent = `${event.timestamp} ${event.name} has left the chat.`;
}else{
newTag.classList.add('chat-tag--message');
newTag.textContent = `${event.timestamp} ${event.name}`;
newMsg = document.createElement('span');
newMsg.textContent = event.msg;
}
Now we will create a div, which will be the top level element of our chat line. Then, we will add as children the tag and, in case the message span was created, the message span.
const newChatEntryElement = document.createElement('div');
newChatEntryElement.appendChild(newTag);
if(newMsg != null){
newChatEntryElement.appendChild(newMsg);
}
Finally, we will return the element tree we just built.
return newChatEntryElement;
The complete function is shown below.
function GetChatLine(event){
const newTag = document.createElement('span');
let newMsg = null;
newTag.classList.add('chat-tag');
if(event.type === 1){
newTag.classList.add('chat-tag--join');
newTag.textContent = `${event.timestamp} ${event.name} has joined the chat.`;
}else if(event.type === 2){
newTag.classList.add('chat-tag--left');
newTag.textContent = `${event.timestamp} ${event.name} has left the chat.`;
}else{
newTag.classList.add('chat-tag--message');
newTag.textContent = `${event.timestamp} ${event.name}`;
newMsg = document.createElement('span');
newMsg.textContent = event.msg;
}
const newChatEntryElement = document.createElement('div');
newChatEntryElement.appendChild(newTag);
if(newMsg != null){
newChatEntryElement.appendChild(newMsg);
}
return newChatEntryElement;
}
The complete code with all the changes can be seen below.
var ws = null;
var name = null;
function OpenWebsocket() {
const nameTextElement = document.getElementById("nameText");
name = nameTextElement.value;
nameTextElement.value = '';
const url = `ws://${location.hostname}/chat`;
ws = new WebSocket(url);
ws.onopen = function() {
document.getElementById("inputText").disabled = false;
document.getElementById("sendButton").disabled = false;
document.getElementById("disconnectButton").disabled = false;
document.getElementById("connectButton").disabled = true;
document.getElementById("nameText").disabled = true;
const objToSend = {
type: 1,
name: name
}
const serializedObj = JSON.stringify(objToSend);
ws.send(serializedObj);
};
ws.onclose = function() {
document.getElementById("inputText").disabled = true;
document.getElementById("sendButton").disabled = true;
document.getElementById("disconnectButton").disabled = true;
document.getElementById("connectButton").disabled = false;
document.getElementById("nameText").disabled = false;
document.getElementById("chatDiv").innerHTML = '';
};
ws.onmessage = function(event) {
const receivedObj = JSON.parse(event.data);
const chatLine = GetChatLine(receivedObj);
const chatDiv = document.getElementById("chatDiv");
chatDiv.appendChild(chatLine);
};
}
function CloseWebsocket(){
const objToSend = {
type: 2,
name: name
}
const serializedObj = JSON.stringify(objToSend);
ws.send(serializedObj);
ws.close();
}
function SendData(){
const inputTextElement = document.getElementById("inputText");
const msg = inputTextElement.value;
inputTextElement.value = '';
const objToSend = {
type: 3,
msg: msg,
name: name
}
const serializedObj = JSON.stringify(objToSend);
ws.send(serializedObj);
}
function GetChatLine(event){
const newTag = document.createElement('span');
let newMsg = null;
newTag.classList.add('chat-tag');
if(event.type === 1){
newTag.classList.add('chat-tag--join');
newTag.textContent = `${event.timestamp} ${event.name} has joined the chat.`;
}else if(event.type === 2){
newTag.classList.add('chat-tag--left');
newTag.textContent = `${event.timestamp} ${event.name} has left the chat.`;
}else{
newTag.classList.add('chat-tag--message');
newTag.textContent = `${event.timestamp} ${event.name}`;
newMsg = document.createElement('span');
newMsg.textContent = event.msg;
}
const newChatEntryElement = document.createElement('div');
newChatEntryElement.appendChild(newTag);
if(newMsg != null){
newChatEntryElement.appendChild(newMsg);
}
return newChatEntryElement;
}
Testing the code
To test the new changes to the system, we can simply do what we have been doing before. Basically, we can connect to the chat application with two different clients (for example, we can open two browser windows) and start sending messages from them both. Besides the timestamped messages we were already seeing in the past tutorial, now we should see the connection and disconnection events, like shown in figure 1.