WhatsApp-like chat app in JS and PHP

WhatsApp-like chat app in JS and PHP

Compliments of the season to you, in the spirit of Christmas celebration, hashnode came up with this wonderful idea of a Christmas hackathon which is the reason for this article. What I will be building or actually what I have built, I'm done building it and you can get codes at this repo Xchat github, is a chat app in Javascript and PHP with the look, feel, and some features that make it function same way WhatsApp does (X for Xmas, dumb name right?). my motivation for building this project is to explore possible ways to implement a chat system without using real-time services like Pusher (which offers very good functionalities anyways) and web sockets, my aim was to create everything from the ground up to understand how it works. this project was more of a learning experience for me and here, I will explain step by step how the project went from the ground up to completion. in order to achieve real-time data exchange and update, javascript ajax was heavily used to communicate between the server and client.

I assume the reader already has at least a basic knowledge of PHP and jquery. the reasons for using these languages is because of it's simplicity and for the fact that you can easily replicate the same features in other languages or frameworks such as Vue, Laravel, React e.t.c when the working principle of the code is understood. I did not intend to build a fully functional JS app that functions exactly like Whatsapp, that of course would take a lot of resources. what I intended to achieve was a JS app that shares similar features as Whatsapp chat. these features include:

  • Native and responsive UI design.
  • Emoji slide panel.
  • Group chats according to date.
  • Typing indicator(you see when the other person is typing a message).
  • Online and offline status(Thanks to an answer from StackOverflow).
  • Message read green indicator.
  • Know when a new message comes in while chatting and updates the UI.

  • And everything has to happen in the background, to make sure there's no page refresh.

I will not focus much on the UI design with the help of bootstrap, but it's very important that I mention the pen I got the inspiration for the UI CodePen Home Chat UI With Emoji keyboard. with a little bit of feature engineering, I made the chat UI with some part of this pen. rather than talk much about the UI, let's move immediately to see how the features listed above was implemented. but before we do that lets take a rundown of how the app works, using pictures.

User visits the app and has to sign up or login

signup.PNG

login.PNG

After login, user will be directed to the homepage where they can select another user to chat with or view new messages.

index.PNG

messages.PNG

when a user clicks on a person or message, the chat page will open showing the previous messages and allowing the user to continue chatting.

online view.PNG

emoji view.PNG

Let's proceed with the implementation of the features.

Native and responsive UI design

the UI was carefully made to match the look and feel of a native app while considering responsiveness for mobile and desktop devices. the chat.php and chat-bs.php (bs = big screen) files ensure this works, with chat-bs.php functioning as a message list view for mobile devices and also as a chatting view for desktop devices. output of chat-bs.php

messages.PNG

emoji desktop.PNG

Emoji slide panel

I added Emoji to the app by utilizing the open source emoji resource hosted at github. Emoji CSS

all that was needed to copy the links and paste them exactly where you need the emoji to appear

    <link href="https://afeld.github.io/emoji-css/emoji.css" rel="stylesheet">

    <li class="emoji" data-clipboard-text="shrug"><i class="em em-shrug"></i></li>
                    <li class="emoji" data-clipboard-text="shushing_face"><i class="em em-shushing_face"></i></li>
                    <li class="emoji" data-clipboard-text="smile"><i class="em em-smile"></i></li>

clicking and inserting emoji into a content editable div input was handled with jquery

chats.js,chats-bs.js

$('.emoji-dashboard li .em').click(function () {
    var emo = $(this).css('background-image').split('"')[1];
    $('.chat-inp .input').find('div').remove();
    $('.chat-inp .input').append('<img style="width:40px;height:40px;" src="' + emo + '">');
    //$(".emoji-dashboard").slideUp('fast');

});

Group chats according to date

With the help of moment js date formating, messages are grouped according to sent date. the piece of code that implements this algorithm is shown below.

 data = JSON.parse(data);
        if (data.length > 0) {
            let index = 0;
            let previousDate = "";
            let currentDate = "";
            // create an initial div with date class for later use if needed, else the date is today
            $('.conversation-container').append(`<div class="text-center message date">today</div>`);

            data.forEach(function (res) {

                if (index > 0) {
                    previousDate = moment(data[index - 1].time).format(
                        "L"
                    );

                    currentDate = moment(res.time).format("L");
                }

                if (previousDate && !moment(currentDate).isSame(previousDate, "day")) {

                //if the dates are not the same , format and display date in the last div with date class
                    $('.date:last').html(moment(previousDate).format("Do MMM YY"));
                    //create another date div immediately to replace the used up one
                    $('.conversation-container').append(`<div class="text-center message date">today</div>`);

                }
                else if (previousDate && moment(currentDate).isSame(previousDate, "day")) {
                    // the date is today
                    if (moment(currentDate).isSame(new Date, "day")) {

                        $('.date:last').html('today');
                    } else {
                        $('.date:last').html(moment(currentDate).format("Do MMM YY"));
                    }
                }

from the code above when the chats of a particular user is retrieved and iterated through for display, the dates are compared and the messages grouped according to their dates by creating divs with class date on the fly and inserting the dates on the last date div with jquery $('.date:last') when a change in date is detected. if there's no change in date then the date is today. the piece of code above can be found in buildMSG funtion in the files chats.js and chats-bs.js from the github repository i shared above.

Typing indicator(see when the other person is typing a message).

The idea to implement this was derived from a related blog by pusher. the process involves listening to the keypress event on the text input and sending an ajax request to the PHP backend responsible for updating the database by setting a typing indicator column to true. at the other end a javascript ajax request is made on intervals to check if the other user is typing a messaging and thereby updating the UI with a typing message. Javascript checks for keypress and makes ajax request, throttleTime and canPublish variables controls wait time before the next ajax request is made. sid and rid variables refers to the sender and receiver of the message. see the complete code in chats.js and chats-bs.js

// check if a user is typing and send it to server
var messageTextField = $('#text-input');
var canPublish = true;
var throttleTime = 200; //0.2 seconds

messageTextField.on('keyup', function (event) {

    if (canPublish) {
        $.post('user-typing.php', { sid: sid, rid: rid });
        canPublish = false;

        setTimeout(function () {
            $.post('not-typing.php', { sid: sid, rid: rid });
            canPublish = true;
        }, throttleTime);
    }
});

user-typing.php

<?
include_once('DB.class.php');
$con = new DB();
$con = $con->open(); 
$sid = isset($_POST['sid']) ? $_POST['sid'] : null;
$rid = isset($_POST['rid']) ? $_POST['rid'] : null;

$query=mysqli_query($con,"select * from typing_indicator where (sender='$sid' and  reciever='$rid') or (sender='$rid' and reciever='$sid')")or die(mysqli_error($con));
if(mysqli_num_rows($query)> 0){
$row = mysqli_fetch_assoc($query);
$row_id = $row['id'];
$query=mysqli_query($con,"update typing_indicator set sender='$sid', reciever='$rid', typing = 1 where id='$row_id'")or die(mysqli_error($con));

}else{
$query=mysqli_query($con,"insert into typing_indicator(sender,reciever,typing) values('$sid','$rid',1)")or die(mysqli_error($con));
$con->close();
}

?>

not-typing.php

<?

include_once('DB.class.php');
$con = new DB();
$con = $con->open();

$sid = isset($_POST['sid']) ? $_POST['sid'] : null;
$rid = isset($_POST['rid']) ? $_POST['rid'] : null;
$query=mysqli_query($con,"update typing_indicator set typing = 0 where (sender='$sid' and reciever='$rid')")or die(mysqli_error($con));
$con->close();
?>

javascript function checkTyping checks to see if the other user is typing a message every 2000ms

var clearInterval = 2000; 
var clearTimerId;

function checkTyping() {

    $.post('check-typing.php', { sid: sid, rid: rid }, function (data, status) {
        data = JSON.parse(data);
        if (data.length > 0) {
            var res = data[0];

            if (res.typing == 1 && res.reciever == sid && $("#typing").length == 0) {
              //create and display a 'typing' text info.
                $('.conversation-container').append(`<div class="message recieved"  id="typing" style="background: #fff;
  border - radius: 0px 5px 5px 5px; float: left; border - width: 0px 10px 10px 0;
            border - color: transparent #fff transparent transparent;
            top: 0;
            left: -10px;">typing...</div>`);
               //scroll to the last message
                scrollToTop();
                //restart timeout timer
                clearTimeout(clearTimerId);
                clearTimerId = setTimeout(function () {
                    //clear user is typing message
                    $('#typing').remove();
                }, clearInterval);
            }
        }
    });
}

Online and offline status(Thanks to an answer from StackOverflow).

To set a user online after searching for the best way to do it. i found that I could update a last_active column as long as the user is logged in with the browser tab open, by sending an ajax request to the server every 10000ms with the current time.

xchat.js

var userID = $('#sid').val();
var request;
setInterval(function () {
    request = $.ajax({
        url: "set_active.php",
        type: "get",
        data: 'user_id=' + userID
    });

    request.done(function (response, textStatus, jqXHR) {

    });

    // Callback handler that will be called on failure
    request.fail(function (jqXHR, textStatus, errorThrown) {
        console.log('active set error ' + errorThrown);
    });
}, 10000)

set_active.php updates a users' last_active data to the current time with MySQL NOW funtion

<?php

include_once('DB.class.php');
$con = new DB();
$con = $con->open();

$id = $_GET['user_id'];

$query = mysqli_query($con, "UPDATE user SET last_active=NOW() WHERE id='$id'");

To know if a user is online or offline all we need to do is compare the current time with their last_active value from database for a given time interval say 20secs. if it is <= the time interval, then they are offline, else they are online.

xchat.js

//check online status of user on intervals
setInterval(function () {
    var userID = $('#rid').val();
    request = $.ajax({
        url: "last_active.php",
        type: "get",
        data: 'user_id=' + userID
    });

    request.done(function (response, textStatus, jqXHR) {
        // Log a message to the console
        var res = JSON.parse(response);
        if (res.status == 'online') {
            $('.online-status').html('<span class="fa fa-circle" style="color:green"></span><span> online</span>')
        }
        else {
            $('.online-status').html(`<span class="fa fa-circle" style="color:grey"></span><span>offline</span>`)
        }
    });

    // Callback handler that will be called on failure
    request.fail(function (jqXHR, textStatus, errorThrown) {
        console.log('active check set error ' + errorThrown);
    });
}, 1000)

last_active.php

<?php

include_once('DB.class.php');
$con = new DB();
$con = $con->open();

$id = $_GET['user_id'];
$query = mysqli_query($con,"SELECT * FROM user WHERE id ='$id' and last_active <= (NOW() - INTERVAL 20 SECOND)"); 

if(mysqli_num_rows($query) > 0){
$con->close();
$data = array();
$data['status'] ='offline';
echo json_encode($data);
}
else {
$data = array();
$data['status'] = 'online';
 echo json_encode($data);
}
?>

Message read green indicator.

when a message is sent to a user the OK marker remains grey, signifying that the message is not yet opened by the receiver, it turns green immediately it is read. the function setSeen and buildMSG below from chats.js and chats-bs.js shows the implementation of this feature, and other features responsible for accurately rendering a message to the view

function checkSeen() {
    var lastId = parseInt($('.sent:last').attr('id'));
    var seenLast = $('.sent:last').attr('seen');
    if (seenLast == '0') {
        $.post('retrieve-seen.php', { cid: lastId }, function (data, status) {

            data = JSON.parse(data);
            var res = data[0];
            if (res.seen == '1' && res.sender == sid) {
                console.log(data);
                $('.msg-dblcheck-ack').attr('fill', '#4fc3f7');
                $('.sent:last').attr('seen', '1');
                //buildMSG();
            }
        });
    }
}
function buildMSG() {
    $('.conversation-container').empty();

    $.post('retrieve.php', { sid: sid, rid: rid }, function (data, status) {
        // Log the response to the console

        $('.loading').remove();
        data = JSON.parse(data);
        if (data.length > 0) {
            let index = 0;
            let previousDate = "";
            let currentDate = "";
            // create an initial div with date class for later use if needed, else the date is today
            $('.conversation-container').append(`<div class="text-center message date">today</div>`);

            data.forEach(function (res) {

                if (index > 0) {
                    previousDate = moment(data[index - 1].time).format(
                        "L"
                    );

                    currentDate = moment(res.time).format("L");
                }

                if (previousDate && !moment(currentDate).isSame(previousDate, "day")) {

                //if the dates are not the same , format and display date in the last div with date class
                    $('.date:last').html(moment(previousDate).format("Do MMM YY"));
                    //create another date div immediately to replace the used up one
                    $('.conversation-container').append(`<div class="text-center message date">today</div>`);

                }
                else if (previousDate && moment(currentDate).isSame(previousDate, "day")) {
                    // the date is today
                    if (moment(currentDate).isSame(new Date, "day")) {

                        $('.date:last').html('today');
                    } else {
                        $('.date:last').html(moment(currentDate).format("Do MMM YY"));
                    }
                }

                 // check if a message is read or not
                if (res.sender == sid) {
                    if (res.seen == 1)
                        var fill_color = '#4fc3f7';
                    else
                        var fill_color = 'grey';

                    $('.conversation-container').append(`<div class="message sent" id="${res.id}" seen="${res.seen}" >${res.msg}<span class="metadata">
                <span class="time">${moment(res.time).format('h:mm A')}</span><span class="tick">
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="15" id="msg-dblcheck-ack"  x="2063" y="2076"><path class="msg-dblcheck-ack" d="M15.01 3.316l-.478-.372a.365.365 0 0 0-.51.063L8.666 9.88a.32.32 0 0 1-.484.032l-.358-.325a.32.32 0 0 0-.484.032l-.378.48a.418.418 0 0 0 .036.54l1.32 1.267a.32.32 0 0 0 .484-.034l6.272-8.048a.366.366 0 0 0-.064-.512zm-4.1 0l-.478-.372a.365.365 0 0 0-.51.063L4.566 9.88a.32.32 0 0 1-.484.032L1.892 7.77a.366.366 0 0 0-.516.005l-.423.433a.364.364 0 0 0 .006.514l3.255 3.185a.32.32 0 0 0 .484-.033l6.272-8.048a.365.365 0 0 0-.063-.51z" fill="${fill_color}"/></svg></span></span></div>`);

                } else {

                    $('.conversation-container').append(`<div class="message recieved" id="${res.id}" seen="${res.seen}" style="background: #fff;
  border-radius: 0px 5px 5px 5px;float: left;  border-width: 0px 10px 10px 0;
  border-color: transparent #fff transparent transparent;
  top: 0;
  left: -10px;" >${res.msg}<span style="padding: 0 0 0 16px;" class="metadata"><span class="time">${moment(res.time).format('h:mm A')}</span></span></div>`);

                }

                index = index + 1;
            });
            scrollToTop();
        } else {
            $('.conversation-container').html('<div class="text-center"><i>Be the first to say hi..</i></div>');
        }
    });

}

Know when a new message comes in while chatting and updates the UI.

Checking for new messages in the background without obstructing or letting the user know what is happening is a feature in every mobile chat app. inorder to achieve same in this web app, I had to use an algorithm which gets the id of the last received and displayed message in the UI with jquery, and then compare it with id of the last unread message of that particular user from the database.

    var lastId = parseInt($('.recieved:last').attr('id'));

Next, make an ajax request to the database to retrieve a list of unread messages in descending order, if the id of the last message from database is greater than lastId then a new message came in, render it to UI. complete code for retrieving new messages

function checkNewMessage() {

    checkSeen();
    checkTyping();
    var lastId = parseInt($('.recieved:last').attr('id'));
    $.post('retrieve-new.php', { sid: rid, rid: sid }, function (data, status) {
        console.log(data);
        data = JSON.parse(data);
        if (data.length > 0) {
            // Log the response to the console
            var lastItem = data[data.length - 1];
            var lastIndex = parseInt(lastItem.id);
            if (lastIndex > lastId) {
                if (lastItem.reciever == sid) {
                    setSeen(lastIndex);

                }
            }
        }
    });
}

all there is to do is set intervals at which this functions execute repeatedly

setInterval(checkNewMessage, 500);

I have shared with you the steps I took in getting this app working like a native chat app. it was fun building it, and I hope it helps you in a project where you might need to implement something like this. once again the complete code and instructions to get it up and running can be found at my repo Xchat github. Thanks for taking your time to reach the end of this article, and Happy new year! in advance.