Powerful Chat System – Lesson 7

Powerful Chat System – Lesson 7

5 51530
Powerful Chat System

Powerful Chat System – Lesson 7

Today we continue a series of articles on the creation of powerful chat system. In our sevenths lesson I have added a very important feature – private messaging. Now you can send your private messages to another members (optional – to online members). Functionality looks like facebook’s chat. We have to click small icon near recipient’s name in the list of members to start talking in private mode. Welcome to start reading.

Today I will publish updated sources of our growing project. Whole project is well structured: system classes is in ‘classes’ folder, all javascript files in ‘js’ folder, stylesheets in ‘css’ folder, all custom avatars in ‘data’ folder, images in ‘images’ folder, template files in ‘templates’ folder (as usual).

Live Demo
download in package

Now – download the source files and lets start coding !


Step 1. HTML

I updated two template files:

templates/main_page.html

templates/profile_page.html

Please add just 2 lines (as at code below) right before closed BODY tag (for both template files):

....
    <div class="priv_dock_wrap"></div>
    {priv_js}
</body>
</html>

It is our new private messages dock (fixed element at the bottom of page).

Step 2. CSS

I updated the second half of our main CSS file (from line 180 till the end of file):

css/main.css

....
/* chat block */
.chat_messages {
    border: 1px solid #888;
    color: #000;
    padding: 10px;
}
.chat_messages a, .priv_conv a {
    color: #000;
}
.chat_messages a img, .priv_conv a img {
    margin-right: 10px;
    vertical-align: middle;
    width: 22px;
}
.chat_messages .message, .priv_conv .message {
    background-color: #fff;
    margin: 5px;
    padding: 5px;
    -moz-border-radius: 5px;
    -ms-border-radius: 5px;
    -o-border-radius: 5px;
    -webkit-border-radius: 5px;
    border-radius: 5px;
}
.chat_messages .message span, .priv_conv .message span {
    color: #444;
    font-size: 10px;
    margin-left: 10px;
}
.chat_submit_form {
    margin: 10px 0px;
    overflow: hidden;
}
.chat_submit_form .error, .chat_submit_form .success, .chat_submit_form .protect {
    display: none;
}
.chat_submit_form .error {
    color: #f55;
}
.chat_submit_form .success {
    color: #5f5;
}
.chat_submit_form .protect {
    color: #55f;
}
/* profiles */
.profiles {
    overflow: hidden;
}
.profiles a {
    display: block;
}
.profiles div {
    overflow: hidden;
}
.profiles div a {
    color: #333333;
    display: block;
    padding: 2px 22px 2px 10px;
    position: relative;
}
.profiles div a:hover {
    background-color: #E0E4EE;
    box-shadow: 2px 0 2px -2px #B2B9C9 inset;
}
.profiles div img {
    border: 0;
    float: left;
    height: 48px;
    margin-right: 8px;
    width: 48px;
}
.profiles div img.pchat {
    border: 0;
    height: 16px;
    position: absolute;
    right: 5px;
    top: 5px;
    width: 16px;
}
.profiles div p {
    display: block;
    line-height: 48px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.profiles div img.status_img {
    border: 0;
    display: block;
    height: 7px;
    margin-top: -6px;
    position: absolute;
    right: 5px;
    top: 50%;
    width: 7px;
}
/* customize profile page */
.customizer_buttons #preview, .customizer_buttons #pick {
    border: 1px solid #888;
    border-radius: 3px 3px 3px 3px;
    box-shadow: 2px 3px 3px #888;
    height: 40px;
    margin-bottom: 10px;
    width: 80px;
}
/* private messaging */
.priv_dock_wrap {
    bottom: 0;
    left: auto;
    position: fixed;
    right: 275px;
    z-index: 300;
}
.priv_chat_tab {
    background-color: #FFFFFF;
    border: 1px solid #000000;
    color: #000000;
    float: left;
    height: 285px;
    margin: 0 5px;
    max-height: 342px;
    width: 260px;
}
.priv_title {
    background-color: #6D84B4;
    color: #FFFFFF;
    cursor: pointer;
    font-weight: bold;
    line-height: 18px;
    overflow: hidden;
    padding: 3px 15px 4px;
    position: relative;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.priv_title img {
    position: absolute;
    right: 4px;
    top: 5px;
}
.priv_conv {
    height: 234px;
    overflow-y: auto;
}
.priv_input {
    border-top: 1px solid #888888;
}
.priv_input input[type=text] {
    border: 0 none;
    display: block;
    height: 16px;
    margin: 0;
    max-height: 77px;
    min-height: 16px;
    outline: medium none;
    overflow-x: hidden;
    overflow-y: auto;
    padding: 5px 4px 3px 20px;
    resize: none;
    width: 234px;
    -moz-border-radius: 0;
    -ms-border-radius: 0;
    -o-border-radius: 0;
    -webkit-border-radius: 0;
    border-radius: 0;
}

Now it contains new styles for our private messaging system.

Step 3. PHP

Now, its time to check changes in our PHP sources.

index.php

<?php
// set error reporting level
if (version_compare(phpversion(), '5.3.0', '>=') == 1)
  error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
else
  error_reporting(E_ALL & ~E_NOTICE);
require_once('classes/Services_JSON.php');
require_once('classes/CMySQL.php'); // including service class to work with database
require_once('classes/CLogin.php'); // including service class to work with login processing
require_once('classes/CProfiles.php'); // including service class to work with profiles
$sErrors = '';
// join processing
if (! isset($_SESSION['member_id']) && $_POST['Join'] == 'Join') {
    $GLOBALS['CProfiles']->registerProfile();
}
// login system init and generation code
$sLoginForm = $GLOBALS['CLogin']->getLoginBox();
$sChat = '<h2>You do not have rights to use chat</h2>';
$sInput = $sPrivChatJs = '';
if ($_SESSION['member_id'] && $_SESSION['member_status'] == 'active' && $_SESSION['member_role']) {
    if ($_GET['action'] == 'update_last_nav') { // update last navigate time
        $iPid = (int)$_SESSION['member_id'];
        if ($iPid) {
            $GLOBALS['MySQL']->res("UPDATE `cs_profiles` SET `date_nav` = NOW() WHERE `id` = '{$iPid}'");
        }
        exit;
    }
    require_once('classes/CChat.php'); // including service class to work with chat
    if ($_GET['action'] == 'check_new_messages') { // check for new messages
        $iPid = (int)$_SESSION['member_id'];
        $iSender = $GLOBALS['MainChat']->getRecentMessage($iPid);
        if ($iSender) {
            $aSender = $GLOBALS['CProfiles']->getProfileInfo($iSender);
            $sName = ($aSender['first_name'] && $aSender['last_name']) ? $aSender['first_name'] . ' ' . $aSender['last_name'] : $aSender['name'];
            $oJson = new Services_JSON();
            header('Content-type: application/json');
            echo $oJson->encode(array('id' => $iSender, 'name' => $sName));
        }
        exit;
    }
    if ($_GET['action'] == 'get_private_messages') { // regular updating of messages in chat
        $sChat = $GLOBALS['MainChat']->getMessages((int)$_GET['recipient']);
        $oJson = new Services_JSON();
        header('Content-type: application/json');
        echo $oJson->encode(array('messages' => $sChat));
        exit;
    }
    // get last messages
    $sChat = $GLOBALS['MainChat']->getMessages();
    if ($_GET['action'] == 'get_last_messages') { // regular updating of messages in chat
        $oJson = new Services_JSON();
        header('Content-type: application/json');
        echo $oJson->encode(array('messages' => $sChat));
        exit;
    }
    // add avatar
    if ($_POST['action'] == 'add_avatar') {
        $iAvRes = $GLOBALS['CProfiles']->addAvatar();
        header('Content-Type: text/html; charset=utf-8');
        echo ($iAvRes == 1) ? '<h2 style="text-align:center">New avatar has been accepted, refresh main window to see it</h2>' : '';
        exit;
    }
    // get input form
    $sInput = $GLOBALS['MainChat']->getInputForm();
    if ($_POST['message']) { // POST-ing of message
        $iRes = $GLOBALS['MainChat']->acceptMessage();
        $oJson = new Services_JSON();
        header('Content-type: application/json');
        echo $oJson->encode(array('result' => $iRes));
        exit;
    }
    if ($_POST['priv_message']) { // POST-ing of private messages
        $iRes = $GLOBALS['MainChat']->acceptPrivMessage();
        $oJson = new Services_JSON();
        header('Content-type: application/json');
        echo $oJson->encode(array('result' => $iRes));
        exit;
    }
    $sPrivChatJs = '<script src="js/priv_chat.js"></script>';
}
// get profiles lists
$sProfiles = $GLOBALS['CProfiles']->getProfilesBlock();
$sOnlineMembers = $GLOBALS['CProfiles']->getProfilesBlock(10, true);
// get profile avatar
$sAvatar = $GLOBALS['CProfiles']->getProfileAvatarBlock();
// draw common page
$aKeys = array(
    '{form}' => $sLoginForm . $sErrors,
    '{chat}' => $sChat,
    '{input}' => $sInput,
    '{profiles}' => $sProfiles,
    '{online_members}' => $sOnlineMembers,
    '{avatar}' => $sAvatar,
    '{priv_js}' => $sPrivChatJs
);
echo strtr(file_get_contents('templates/main_page.html'), $aKeys);

I added here several more cases for our chat: checking for fresh messages, get private messages, posting of private messages. Our next updated file is profile view file (where I had to add our new js file priv_chat.js):

profile.php

<?php
// set error reporting level
if (version_compare(phpversion(), '5.3.0', '>=') == 1)
  error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
else
  error_reporting(E_ALL & ~E_NOTICE);
require_once('classes/CMySQL.php');
require_once('classes/CLogin.php');
require_once('classes/CProfiles.php');
$iPid = (int)$_GET['id'];
$sPrivChatJs = '';
if ($_SESSION['member_id'] && $_SESSION['member_status'] == 'active' && $_SESSION['member_role']) {
    if ($_GET['action'] == 'change_color') {
        $iRes = $GLOBALS['CProfiles']->changeColor($_GET['color']);
        header('Content-Type: text/html; charset=utf-8');
        echo ($iRes == 1) ? '<h2 style="text-align:center">New color has been accepted, refresh main window to see it</h2>' : '';
        exit;
    }
    $sPrivChatJs = '<script src="js/priv_chat.js"></script>';
}
$aInfo = $GLOBALS['CProfiles']->getProfileInfo($iPid);
$sName = $aInfo['name'];
$sFName = $aInfo['first_name'];
$sLName = $aInfo['last_name'];
$sAbout = $aInfo['about'];
$sDate = $aInfo['date_reg'];
$sRole = $GLOBALS['CProfiles']->getRoleName($aInfo['role']);
$sAvatar = $GLOBALS['CProfiles']->getProfileAvatar($iPid);
$sCustomBG = ($aInfo['color']) ? 'background-color:#'.$aInfo['color'] : '';
// get profiles lists
$sProfiles = $GLOBALS['CProfiles']->getProfilesBlock();
$sOnlineMembers = $GLOBALS['CProfiles']->getProfilesBlock(10, true);
// draw common page
$aKeys = array(
    '{id}' => $iPid,
    '{name}' => $sName,
    '{fname}' => $sFName,
    '{lname}' => $sLName,
    '{about}' => $sAbout,
    '{datereg}' => $sDate,
    '{role}' => $sRole,
    '{avatar}' => $sAvatar,
    '{custom_styles}' => $sCustomBG,
    '{cust_visible}' => ($_SESSION['member_id'] == $iPid) ? '' : 'style="display:none"',
    '{profiles}' => $sProfiles,
    '{online_members}' => $sOnlineMembers,
    '{priv_js}' => $sPrivChatJs
);
echo strtr(file_get_contents('templates/profile_page.html'), $aKeys);

The next updated file:

classes/CChat.php

<?php
class CChat {
    // constructor
    function CChat() {}
    // add a message to database
    function acceptMessage() {
        $sName = $GLOBALS['MySQL']->escape($_SESSION['member_name']);
        $iPid = (int)$_SESSION['member_id'];
        $sMessage = $GLOBALS['MySQL']->escape($_POST['message']);
        if ($iPid && $sName != '' && $sMessage != '') {
            $sSQL = "
                SELECT `id`
                FROM `cs_messages`
                WHERE `sender` = '{$iPid}' AND UNIX_TIMESTAMP( ) - `when` < 5
                LIMIT 1
            ";
            $iLastId = $GLOBALS['MySQL']->getOne($sSQL);
            if ($iLastId) return 2; // as protection from very often messages
            $bRes = $GLOBALS['MySQL']->res("INSERT INTO `cs_messages` SET `sender` = '{$iPid}', `message` = '{$sMessage}', `when` = UNIX_TIMESTAMP()");
            return ($bRes) ? 1 : 3;
        }
    }
    // add a private message to database
    function acceptPrivMessage() {
        $sName = $GLOBALS['MySQL']->escape($_SESSION['member_name']);
        $iPid = (int)$_SESSION['member_id'];
        $iRecipient = (int)$_POST['recipient'];
        $sMessage = $GLOBALS['MySQL']->escape($_POST['priv_message']);
        if ($iPid && $iRecipient && $sName != '' && $sMessage != '') {
            $sSQL = "
                SELECT `id`
                FROM `cs_messages`
                WHERE `sender` = '{$iPid}' AND `recipient` = '{$iRecipient}' AND UNIX_TIMESTAMP( ) - `when` < 5
                LIMIT 1
            ";
            $iLastId = $GLOBALS['MySQL']->getOne($sSQL);
            if ($iLastId) return 2; // as protection from very often messages
            $bRes = $GLOBALS['MySQL']->res("INSERT INTO `cs_messages` SET `sender` = '{$iPid}', `recipient` = '{$iRecipient}', `message` = '{$sMessage}', `when` = UNIX_TIMESTAMP()");
            return ($bRes) ? 1 : 3;
        }
    }
    // return input text form
    function getInputForm() {
        return file_get_contents('templates/chat.html');
    }
    // get last 10 messages
    function getMessages($iRecipient = 0) {
        $sRecipientSQL = 'WHERE `recipient` = 0';
        if ($iRecipient > 0) {
            $iPid = (int)$_SESSION['member_id'];
            $sRecipientSQL = "WHERE (`sender` = '{$iRecipient}' && `recipient` = '{$iPid}') || (`recipient` = '{$iRecipient}' && `sender` = '{$iPid}')";
        }
        $sSQL = "
            SELECT `a` . * , `cs_profiles`.`name`,  `cs_profiles`.`id` as 'pid' , UNIX_TIMESTAMP( ) - `a`.`when` AS 'diff'
            FROM `cs_messages` AS `a`
            INNER JOIN `cs_profiles` ON `cs_profiles`.`id` = `a`.`sender`
            {$sRecipientSQL}
            ORDER BY `a`.`id` DESC
            LIMIT 10
        ";
        $aMessages = $GLOBALS['MySQL']->getAll($sSQL);
        asort($aMessages);
        // create list of messages
        $sMessages = '';
        foreach ($aMessages as $i => $aMessage) {
            $sExStyles = $sExJS = '';
            $iDiff = (int)$aMessage['diff'];
            if ($iDiff < 7) { // less than 7 seconds
                $sExStyles = 'style="display:none;"';
                $sExJS = "<script> $('#message_{$aMessage['id']}').fadeIn('slow'); </script>";
            }
            $sWhen = date("H:i:s", $aMessage['when']);
            $sAvatar = $GLOBALS['CProfiles']->getProfileAvatar($aMessage['pid']);
            $sMessages .= '<div class="message" id="message_'.$aMessage['id'].'" '.$sExStyles.'><b><a href="profile.php?id='.$aMessage['pid'].'" target="_blank"><img src="'. $sAvatar .'">' . $aMessage['name'] . ':</a></b> ' . $aMessage['message'] . '<span>(' . $sWhen . ')</span></div>' . $sExJS;
        }
        return $sMessages;
    }
    function getRecentMessage($iPid) {
        if ($iPid) {
            $sSQL = "
                SELECT `a` . * , `cs_profiles`.`name`,  `cs_profiles`.`id` as 'pid' , UNIX_TIMESTAMP( ) - `a`.`when` AS 'diff'
                FROM `cs_messages` AS `a`
                INNER JOIN `cs_profiles` ON `cs_profiles`.`id` = `a`.`sender`
                WHERE `recipient` = '{$iPid}'
                ORDER BY `a`.`id` DESC
                LIMIT 1
            ";
            $aMessage = $GLOBALS['MySQL']->getRow($sSQL);
            $iDiff = (int)$aMessage['diff'];
            if ($iDiff < 7) { // less than 7 seconds, = new
                return (int)$aMessage['sender'];
            }
            return;
        }
    }
}
$GLOBALS['MainChat'] = new CChat();

I made up my mind to publish whole code of this file. Because I made several changes in existed functions, plus I added two new functions: acceptPrivMessage (to accept private messages) and getRecentMessage (to find, if the member has any fresh messages or hasn’t. If he has any fresh messages and if he doesn’t have opened private chat window – we will initialize new private chat session).

The next updated file:

classes/CProfiles.php

    function getProfilesBlock($iLim = 10, $bOnlineOnly = false) {
        $iPLimit = PROFILE_TIMEOUT;
        $sOnlineSQL = ($bOnlineOnly) ? 'AND (`date_nav` > SUBDATE(NOW(), INTERVAL ' . $iPLimit . ' MINUTE))' : '';
        $sSQL = "
            SELECT `cs_profiles`.*,
            if (`date_nav` > SUBDATE(NOW(), INTERVAL {$iPLimit} MINUTE ), 1, 0) AS `is_online`
            FROM `cs_profiles`
            WHERE `status` = 'active'
            {$sOnlineSQL}
            ORDER BY `date_reg` DESC
            LIMIT {$iLim}
        ";
        $aProfiles = $GLOBALS['MySQL']->getAll($sSQL);
        $bCanChat = ($_SESSION['member_id'] && $_SESSION['member_status'] == 'active' && $_SESSION['member_role']);
        // create list of messages
        $sCode = '';
        foreach ($aProfiles as $i => $aProfile) {
            $sName = ($aProfile['first_name'] && $aProfile['last_name']) ? $aProfile['first_name'] . ' ' . $aProfile['last_name'] : $aProfile['name'];
            $sSName = (strlen($sName) > 32) ? mb_substr($sName, 0, 28) . '...' : $sName;
            $iPid = $aProfile['id'];
            $sAvatar = $this->getProfileAvatar($iPid);
            $sOnline = ($aProfile['is_online'] == 1) ? '<img alt="" src="images/online.png" class="status_img" />' : '';
            $sChat = ($bCanChat /*&& $aProfile['is_online'] == 1*/) ? '<img id="'.$iPid.'" alt="chat" src="images/chat.png" class="pchat" title="'.$sName.'" />' : '';
            $sCode .= '<div id="'.$iPid.'" title="'.$sName.'"><a href="profile.php?id='.$iPid.'"><img src="'.$sAvatar.'" alt="'.$sName.'"><p>'.$sSName.$sChat.'</p>'.$sOnline.'</a></div>';
        }
        $sClass = ($bOnlineOnly) ? 'profiles online_profiles' : 'profiles';
        return '<div class="'.$sClass.'">' . $sCode . '</div>';
    }

I updated only single function: getProfilesBlock here. Now it draws new icon chat.png. We can click at it to start private conversation. Pay attention to commented code (/*&& $aProfile[‘is_online’] == 1*/). You can uncomment it in order to give possibility to chat only for logged in members.

Step 5. Javascript

js/priv_chat.js

New javascript for our private chat feature:

$(function() {
    // variables
    var aPChatTimers = [];
    // remove private chat tab
    closePchat = function(id) {
        $('.priv_dock_wrap .priv_chat_tab#pcid'+id).remove();
    }
    // initiate private chat
    initiatePrivateChat = function(id, name) {
        var oPChat = $('.priv_dock_wrap .priv_chat_tab#pcid'+id);
        if (! oPChat.length) { // create new chat dialog
            var sPCTemplate = '<div class="priv_chat_tab" id="pcid'+id+'">'+
'    <div class="priv_title">'+name+'<img src="images/close.png" /></div>'+
'    <div class="priv_conv"></div>'+
'    <div class="priv_input">'+
'        <form class="priv_chat_submit_form">'+
'            <input type="hidden" name="recipient" value="'+id+'" />'+
'            <input type="text" name="message" />'+
'        </form>'+
'    </div>'+
'</div>';
            $('.priv_dock_wrap').append(sPCTemplate);
            // bind onclick at close icon to close form
            $('.priv_chat_tab#pcid'+id+' .priv_title img').bind('click', function() {
                clearTimeout(aPChatTimers[id])
                $('.priv_dock_wrap .priv_chat_tab#pcid'+id).remove();
            });
            // bind onsubmit at input form to send message
            $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form').bind('submit', function() {
                $.post('index.php', { priv_message: $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form input[name=message]').val(),
                    recipient: $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form input[name=recipient]').val() },
                    function(data){
                        $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form input[name=message]').val('');
                        if (data.result == 1) {
                            $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form .success').fadeIn('slow', function () {
                                $(this).delay(1000).fadeOut('slow');
                            });
                        } else if (data.result == 2) {
                            $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form .protect').fadeIn('slow', function () {
                                $(this).delay(1000).fadeOut('slow');
                            });
                        } else {
                            $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form .error').fadeIn('slow', function () {
                                $(this).delay(1000).fadeOut('slow');
                            });
                        }
                    }
                );
                return false;
            });
        }
        // start collecting private messages
        getPrivateMessages(id);
    }
    // create private messages
    getPrivateMessages = function(iRecipient) {
        $.getJSON('index.php?action=get_private_messages&recipient=' + iRecipient, function(data) {
            if (data.messages) {
                $('.priv_chat_tab#pcid'+iRecipient+' .priv_conv').html(data.messages);
            }
            // get recent chat messages in loop
            aPChatTimers[iRecipient] = setTimeout(function() {
               getPrivateMessages(iRecipient);
            }, 5000);
        });
    }
    // initiate private chats by click 'chat' icon
    $('.profiles .pchat').click(function(event) {
        event.stopPropagation();
        event.preventDefault();
        initiatePrivateChat(this.id, this.title);
    });
    initiateNewChatsPeriodically = function() {
        $.getJSON('index.php?action=check_new_messages', function(data) {
            if (data != undefined && data.id) {
                initiatePrivateChat(data.id, data.name);
            }
            // refresh last nav time
            setTimeout(function(){
               initiateNewChatsPeriodically();
            }, 6000); // 1 mins
        });
    }
    initiateNewChatsPeriodically();
});

Live Demo
download in archive

Conclusion

I hope that our new series of articles of chat system creation is useful and interesting for you. If you want to share your ideas, or you noticed any weakness – don’t hesitate to contact us. Good luck and welcome back!

SIMILAR ARTICLES

Understanding Closures

0 10450

5 COMMENTS

    • Hello Conor,
      Hum, but this is default ordering, look: ORDER BY `a`.`id` DESC (in functions – getMessages and getRecentMessage)

Leave a Reply to admin Cancel reply