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):

1 ....
2     <div class="priv_dock_wrap"></div>
3     {priv_js}
4 </body>
5 </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

001 ....
002 /* chat block */
003 .chat_messages {
004     border1px solid #888;
005     color#000;
006     padding10px;
007 }
008 .chat_messages a, .priv_conv a {
009     color#000;
010 }
011 .chat_messages a img, .priv_conv a img {
012     margin-right10px;
013     vertical-alignmiddle;
014     width22px;
015 }
016 .chat_messages .message, .priv_conv .message {
017     background-color#fff;
018     margin5px;
019     padding5px;
020     -moz-border-radius: 5px;
021     -ms-border-radius: 5px;
022     -o-border-radius: 5px;
023     -webkit-border-radius: 5px;
024     border-radius: 5px;
025 }
026 .chat_messages .message span, .priv_conv .message span {
027     color#444;
028     font-size10px;
029     margin-left10px;
030 }
031 .chat_submit_form {
032     margin10px 0px;
033     overflowhidden;
034 }
035 .chat_submit_form .error, .chat_submit_form .success, .chat_submit_form .protect {
036     displaynone;
037 }
038 .chat_submit_form .error {
039     color#f55;
040 }
041 .chat_submit_form .success {
042     color#5f5;
043 }
044 .chat_submit_form .protect {
045     color#55f;
046 }
047 /* profiles */
048 .profiles {
049     overflowhidden;
050 }
051 .profiles a {
052     displayblock;
053 }
054 .profiles div {
055     overflowhidden;
056 }
057 .profiles div a {
058     color#333333;
059     displayblock;
060     padding2px 22px 2px 10px;
061     positionrelative;
062 }
063 .profiles div a:hover {
064     background-color#E0E4EE;
065     box-shadow: 2px 0 2px -2px #B2B9C9 inset;
066 }
067 .profiles div img {
068     border0;
069     floatleft;
070     height48px;
071     margin-right8px;
072     width48px;
073 }
074 .profiles div img.pchat {
075     border0;
076     height16px;
077     positionabsolute;
078     right5px;
079     top5px;
080     width16px;
081 }
082 .profiles div p {
083     displayblock;
084     line-height48px;
085     overflowhidden;
086     text-overflow: ellipsis;
087     white-spacenowrap;
088 }
089 .profiles div img.status_img {
090     border0;
091     displayblock;
092     height7px;
093     margin-top-6px;
094     positionabsolute;
095     right5px;
096     top50%;
097     width7px;
098 }
099 /* customize profile page */
100 .customizer_buttons #preview, .customizer_buttons #pick {
101     border1px solid #888;
102     border-radius: 3px 3px 3px 3px;
103     box-shadow: 2px 3px 3px #888;
104     height40px;
105     margin-bottom10px;
106     width80px;
107 }
108 /* private messaging */
109 .priv_dock_wrap {
110     bottom0;
111     leftauto;
112     positionfixed;
113     right275px;
114     z-index300;
115 }
116 .priv_chat_tab {
117     background-color#FFFFFF;
118     border1px solid #000000;
119     color#000000;
120     floatleft;
121     height285px;
122     margin0 5px;
123     max-height342px;
124     width260px;
125 }
126 .priv_title {
127     background-color#6D84B4;
128     color#FFFFFF;
129     cursorpointer;
130     font-weightbold;
131     line-height18px;
132     overflowhidden;
133     padding3px 15px 4px;
134     positionrelative;
135     text-overflow: ellipsis;
136     white-spacenowrap;
137 }
138 .priv_title img {
139     positionabsolute;
140     right4px;
141     top5px;
142 }
143 .priv_conv {
144     height234px;
145     overflow-y: auto;
146 }
147 .priv_input {
148     border-top1px solid #888888;
149 }
150 .priv_input input[type=text] {
151     border0 none;
152     displayblock;
153     height16px;
154     margin0;
155     max-height77px;
156     min-height16px;
157     outlinemedium none;
158     overflow-x: hidden;
159     overflow-y: auto;
160     padding5px 4px 3px 20px;
161     resize: none;
162     width234px;
163     -moz-border-radius: 0;
164     -ms-border-radius: 0;
165     -o-border-radius: 0;
166     -webkit-border-radius: 0;
167     border-radius: 0;
168 }

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

01 <?php
02 // set error reporting level
03 if (version_compare(phpversion(), '5.3.0''>=') == 1)
04   error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
05 else
06   error_reporting(E_ALL & ~E_NOTICE);
07 require_once('classes/Services_JSON.php');
08 require_once('classes/CMySQL.php'); // including service class to work with database
09 require_once('classes/CLogin.php'); // including service class to work with login processing
10 require_once('classes/CProfiles.php'); // including service class to work with profiles
11 $sErrors '';
12 // join processing
13 if (! isset($_SESSION['member_id']) && $_POST['Join'] == 'Join') {
14     $GLOBALS['CProfiles']->registerProfile();
15 }
16 // login system init and generation code
17 $sLoginForm $GLOBALS['CLogin']->getLoginBox();
18 $sChat '<h2>You do not have rights to use chat</h2>';
19 $sInput $sPrivChatJs '';
20 if ($_SESSION['member_id'] && $_SESSION['member_status'] == 'active' && $_SESSION['member_role']) {
21     if ($_GET['action'] == 'update_last_nav') { // update last navigate time
22         $iPid = (int)$_SESSION['member_id'];
23         if ($iPid) {
24             $GLOBALS['MySQL']->res("UPDATE `cs_profiles` SET `date_nav` = NOW() WHERE `id` = '{$iPid}'");
25         }
26         exit;
27     }
28     require_once('classes/CChat.php'); // including service class to work with chat
29     if ($_GET['action'] == 'check_new_messages') { // check for new messages
30         $iPid = (int)$_SESSION['member_id'];
31         $iSender $GLOBALS['MainChat']->getRecentMessage($iPid);
32         if ($iSender) {
33             $aSender $GLOBALS['CProfiles']->getProfileInfo($iSender);
34             $sName = ($aSender['first_name'] && $aSender['last_name']) ? $aSender['first_name'] . ' ' $aSender['last_name'] : $aSender['name'];
35             $oJson new Services_JSON();
36             header('Content-type: application/json');
37             echo $oJson->encode(array('id' => $iSender'name' => $sName));
38         }
39         exit;
40     }
41     if ($_GET['action'] == 'get_private_messages') { // regular updating of messages in chat
42         $sChat $GLOBALS['MainChat']->getMessages((int)$_GET['recipient']);
43         $oJson new Services_JSON();
44         header('Content-type: application/json');
45         echo $oJson->encode(array('messages' => $sChat));
46         exit;
47     }
48     // get last messages
49     $sChat $GLOBALS['MainChat']->getMessages();
50     if ($_GET['action'] == 'get_last_messages') { // regular updating of messages in chat
51         $oJson new Services_JSON();
52         header('Content-type: application/json');
53         echo $oJson->encode(array('messages' => $sChat));
54         exit;
55     }
56     // add avatar
57     if ($_POST['action'] == 'add_avatar') {
58         $iAvRes $GLOBALS['CProfiles']->addAvatar();
59         header('Content-Type: text/html; charset=utf-8');
60         echo ($iAvRes == 1) ? '<h2 style="text-align:center">New avatar has been accepted, refresh main window to see it</h2>' '';
61         exit;
62     }
63     // get input form
64     $sInput $GLOBALS['MainChat']->getInputForm();
65     if ($_POST['message']) { // POST-ing of message
66         $iRes $GLOBALS['MainChat']->acceptMessage();
67         $oJson new Services_JSON();
68         header('Content-type: application/json');
69         echo $oJson->encode(array('result' => $iRes));
70         exit;
71     }
72     if ($_POST['priv_message']) { // POST-ing of private messages
73         $iRes $GLOBALS['MainChat']->acceptPrivMessage();
74         $oJson new Services_JSON();
75         header('Content-type: application/json');
76         echo $oJson->encode(array('result' => $iRes));
77         exit;
78     }
79     $sPrivChatJs '<script src="js/priv_chat.js"></script>';
80 }
81 // get profiles lists
82 $sProfiles $GLOBALS['CProfiles']->getProfilesBlock();
83 $sOnlineMembers $GLOBALS['CProfiles']->getProfilesBlock(10, true);
84 // get profile avatar
85 $sAvatar $GLOBALS['CProfiles']->getProfileAvatarBlock();
86 // draw common page
87 $aKeys array(
88     '{form}' => $sLoginForm $sErrors,
89     '{chat}' => $sChat,
90     '{input}' => $sInput,
91     '{profiles}' => $sProfiles,
92     '{online_members}' => $sOnlineMembers,
93     '{avatar}' => $sAvatar,
94     '{priv_js}' => $sPrivChatJs
95 );
96 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

01 <?php
02 // set error reporting level
03 if (version_compare(phpversion(), '5.3.0''>=') == 1)
04   error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
05 else
06   error_reporting(E_ALL & ~E_NOTICE);
07 require_once('classes/CMySQL.php');
08 require_once('classes/CLogin.php');
09 require_once('classes/CProfiles.php');
10 $iPid = (int)$_GET['id'];
11 $sPrivChatJs '';
12 if ($_SESSION['member_id'] && $_SESSION['member_status'] == 'active' && $_SESSION['member_role']) {
13     if ($_GET['action'] == 'change_color') {
14         $iRes $GLOBALS['CProfiles']->changeColor($_GET['color']);
15         header('Content-Type: text/html; charset=utf-8');
16         echo ($iRes == 1) ? '<h2 style="text-align:center">New color has been accepted, refresh main window to see it</h2>' '';
17         exit;
18     }
19     $sPrivChatJs '<script src="js/priv_chat.js"></script>';
20 }
21 $aInfo $GLOBALS['CProfiles']->getProfileInfo($iPid);
22 $sName $aInfo['name'];
23 $sFName $aInfo['first_name'];
24 $sLName $aInfo['last_name'];
25 $sAbout $aInfo['about'];
26 $sDate $aInfo['date_reg'];
27 $sRole $GLOBALS['CProfiles']->getRoleName($aInfo['role']);
28 $sAvatar $GLOBALS['CProfiles']->getProfileAvatar($iPid);
29 $sCustomBG = ($aInfo['color']) ? 'background-color:#'.$aInfo['color'] : '';
30 // get profiles lists
31 $sProfiles $GLOBALS['CProfiles']->getProfilesBlock();
32 $sOnlineMembers $GLOBALS['CProfiles']->getProfilesBlock(10, true);
33 // draw common page
34 $aKeys array(
35     '{id}' => $iPid,
36     '{name}' => $sName,
37     '{fname}' => $sFName,
38     '{lname}' => $sLName,
39     '{about}' => $sAbout,
40     '{datereg}' => $sDate,
41     '{role}' => $sRole,
42     '{avatar}' => $sAvatar,
43     '{custom_styles}' => $sCustomBG,
44     '{cust_visible}' => ($_SESSION['member_id'] == $iPid) ? '' 'style="display:none"',
45     '{profiles}' => $sProfiles,
46     '{online_members}' => $sOnlineMembers,
47     '{priv_js}' => $sPrivChatJs
48 );
49 echo strtr(file_get_contents('templates/profile_page.html'), $aKeys);

The next updated file:

classes/CChat.php

01 <?php
02 class CChat {
03     // constructor
04     function CChat() {}
05     // add a message to database
06     function acceptMessage() {
07         $sName $GLOBALS['MySQL']->escape($_SESSION['member_name']);
08         $iPid = (int)$_SESSION['member_id'];
09         $sMessage $GLOBALS['MySQL']->escape($_POST['message']);
10         if ($iPid && $sName != '' && $sMessage != '') {
11             $sSQL = "
12                 SELECT `id`
13                 FROM `cs_messages`
14                 WHERE `sender` = '{$iPid}' AND UNIX_TIMESTAMP( ) - `when` < 5
15                 LIMIT 1
16             ";
17             $iLastId $GLOBALS['MySQL']->getOne($sSQL);
18             if ($iLastIdreturn 2; // as protection from very often messages
19             $bRes $GLOBALS['MySQL']->res("INSERT INTO `cs_messages` SET `sender` = '{$iPid}', `message` = '{$sMessage}', `when` = UNIX_TIMESTAMP()");
20             return ($bRes) ? 1 : 3;
21         }
22     }
23     // add a private message to database
24     function acceptPrivMessage() {
25         $sName $GLOBALS['MySQL']->escape($_SESSION['member_name']);
26         $iPid = (int)$_SESSION['member_id'];
27         $iRecipient = (int)$_POST['recipient'];
28         $sMessage $GLOBALS['MySQL']->escape($_POST['priv_message']);
29         if ($iPid && $iRecipient && $sName != '' && $sMessage != '') {
30             $sSQL = "
31                 SELECT `id`
32                 FROM `cs_messages`
33                 WHERE `sender` = '{$iPid}' AND `recipient` = '{$iRecipient}' AND UNIX_TIMESTAMP( ) - `when` < 5
34                 LIMIT 1
35             ";
36             $iLastId $GLOBALS['MySQL']->getOne($sSQL);
37             if ($iLastIdreturn 2; // as protection from very often messages
38             $bRes $GLOBALS['MySQL']->res("INSERT INTO `cs_messages` SET `sender` = '{$iPid}', `recipient` = '{$iRecipient}', `message` = '{$sMessage}', `when` = UNIX_TIMESTAMP()");
39             return ($bRes) ? 1 : 3;
40         }
41     }
42     // return input text form
43     function getInputForm() {
44         return file_get_contents('templates/chat.html');
45     }
46     // get last 10 messages
47     function getMessages($iRecipient = 0) {
48         $sRecipientSQL 'WHERE `recipient` = 0';
49         if ($iRecipient > 0) {
50             $iPid = (int)$_SESSION['member_id'];
51             $sRecipientSQL "WHERE (`sender` = '{$iRecipient}' && `recipient` = '{$iPid}') || (`recipient` = '{$iRecipient}' && `sender` = '{$iPid}')";
52         }
53         $sSQL = "
54             SELECT `a` . * , `cs_profiles`.`name`,  `cs_profiles`.`id` as 'pid' , UNIX_TIMESTAMP( ) - `a`.`when` AS 'diff'
55             FROM `cs_messages` AS `a`
56             INNER JOIN `cs_profiles` ON `cs_profiles`.`id` = `a`.`sender`
57             {$sRecipientSQL}
58             ORDER BY `a`.`id` DESC
59             LIMIT 10
60         ";
61         $aMessages $GLOBALS['MySQL']->getAll($sSQL);
62         asort($aMessages);
63         // create list of messages
64         $sMessages '';
65         foreach ($aMessages as $i => $aMessage) {
66             $sExStyles $sExJS '';
67             $iDiff = (int)$aMessage['diff'];
68             if ($iDiff < 7) { // less than 7 seconds
69                 $sExStyles 'style="display:none;"';
70                 $sExJS "<script> $('#message_{$aMessage['id']}').fadeIn('slow'); </script>";
71             }
72             $sWhen date("H:i:s"$aMessage['when']);
73             $sAvatar $GLOBALS['CProfiles']->getProfileAvatar($aMessage['pid']);
74             $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;
75         }
76         return $sMessages;
77     }
78     function getRecentMessage($iPid) {
79         if ($iPid) {
80             $sSQL = "
81                 SELECT `a` . * , `cs_profiles`.`name`,  `cs_profiles`.`id` as 'pid' , UNIX_TIMESTAMP( ) - `a`.`when` AS 'diff'
82                 FROM `cs_messages` AS `a`
83                 INNER JOIN `cs_profiles` ON `cs_profiles`.`id` = `a`.`sender`
84                 WHERE `recipient` = '{$iPid}'
85                 ORDER BY `a`.`id` DESC
86                 LIMIT 1
87             ";
88             $aMessage $GLOBALS['MySQL']->getRow($sSQL);
89             $iDiff = (int)$aMessage['diff'];
90             if ($iDiff < 7) { // less than 7 seconds, = new
91                 return (int)$aMessage['sender'];
92             }
93             return;
94         }
95     }
96 }
97 $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

01 function getProfilesBlock($iLim = 10, $bOnlineOnly = false) {
02     $iPLimit = PROFILE_TIMEOUT;
03     $sOnlineSQL = ($bOnlineOnly) ? 'AND (`date_nav` > SUBDATE(NOW(), INTERVAL ' $iPLimit ' MINUTE))' '';
04     $sSQL = "
05         SELECT `cs_profiles`.*,
06         if (`date_nav` > SUBDATE(NOW(), INTERVAL {$iPLimit} MINUTE ), 1, 0) AS `is_online`
07         FROM `cs_profiles`
08         WHERE `status` = 'active'
09         {$sOnlineSQL}
10         ORDER BY `date_reg` DESC
11         LIMIT {$iLim}
12     ";
13     $aProfiles $GLOBALS['MySQL']->getAll($sSQL);
14     $bCanChat = ($_SESSION['member_id'] && $_SESSION['member_status'] == 'active' && $_SESSION['member_role']);
15     // create list of messages
16     $sCode '';
17     foreach ($aProfiles as $i => $aProfile) {
18         $sName = ($aProfile['first_name'] && $aProfile['last_name']) ? $aProfile['first_name'] . ' ' $aProfile['last_name'] : $aProfile['name'];
19         $sSName = (strlen($sName) > 32) ? mb_substr($sName, 0, 28) . '...' $sName;
20         $iPid $aProfile['id'];
21         $sAvatar $this->getProfileAvatar($iPid);
22         $sOnline = ($aProfile['is_online'] == 1) ? '<img alt="Powerful Chat System – Lesson 7" src="images/online.png" class="status_img" />' '';
23         $sChat = ($bCanChat /*&& $aProfile['is_online'] == 1*/) ? '<img id="'.$iPid.'" alt="chat" src="images/chat.png" class="pchat" title="'.$sName.'" />' '';
24         $sCode .= '<div id="'.$iPid.'" title="'.$sName.'"><a href="profile.php?id='.$iPid.'"><img src="'.$sAvatar.'" alt="'.$sName.'"><p>'.$sSName.$sChat.'</p>'.$sOnline.'</a></div>';
25     }
26     $sClass = ($bOnlineOnly) ? 'profiles online_profiles' 'profiles';
27     return '<div class="'.$sClass.'">' $sCode '</div>';
28 }

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:

01 $(function() {
02     // variables
03     var aPChatTimers = [];
04     // remove private chat tab
05     closePchat = function(id) {
06         $('.priv_dock_wrap .priv_chat_tab#pcid'+id).remove();
07     }
08     // initiate private chat
09     initiatePrivateChat = function(id, name) {
10         var oPChat = $('.priv_dock_wrap .priv_chat_tab#pcid'+id);
11         if (! oPChat.length) { // create new chat dialog
12             var sPCTemplate = '<div class="priv_chat_tab" id="pcid'+id+'">'+
13 '    <div class="priv_title">'+name+'<img src="images/close.png" /></div>'+
14 '    <div class="priv_conv"></div>'+
15 '    <div class="priv_input">'+
16 '        <form class="priv_chat_submit_form">'+
17 '            <input type="hidden" name="recipient" value="'+id+'" />'+
18 '            <input type="text" name="message" />'+
19 '        </form>'+
20 '    </div>'+
21 '</div>';
22             $('.priv_dock_wrap').append(sPCTemplate);
23             // bind onclick at close icon to close form
24             $('.priv_chat_tab#pcid'+id+' .priv_title img').bind('click'function() {
25                 clearTimeout(aPChatTimers[id])
26                 $('.priv_dock_wrap .priv_chat_tab#pcid'+id).remove();
27             });
28             // bind onsubmit at input form to send message
29             $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form').bind('submit'function() {
30                 $.post('index.php', { priv_message: $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form input[name=message]').val(),
31                     recipient: $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form input[name=recipient]').val() },
32                     function(data){
33                         $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form input[name=message]').val('');
34                         if (data.result == 1) {
35                             $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form .success').fadeIn('slow'function () {
36                                 $(this).delay(1000).fadeOut('slow');
37                             });
38                         else if (data.result == 2) {
39                             $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form .protect').fadeIn('slow'function () {
40                                 $(this).delay(1000).fadeOut('slow');
41                             });
42                         else {
43                             $('.priv_chat_tab#pcid'+id+' .priv_chat_submit_form .error').fadeIn('slow'function () {
44                                 $(this).delay(1000).fadeOut('slow');
45                             });
46                         }
47                     }
48                 );
49                 return false;
50             });
51         }
52         // start collecting private messages
53         getPrivateMessages(id);
54     }
55     // create private messages
56     getPrivateMessages = function(iRecipient) {
57         $.getJSON('index.php?action=get_private_messages&recipient=' + iRecipient, function(data) {
58             if (data.messages) {
59                 $('.priv_chat_tab#pcid'+iRecipient+' .priv_conv').html(data.messages);
60             }
61             // get recent chat messages in loop
62             aPChatTimers[iRecipient] = setTimeout(function() {
63                getPrivateMessages(iRecipient);
64             }, 5000);
65         });
66     }
67     // initiate private chats by click 'chat' icon
68     $('.profiles .pchat').click(function(event) {
69         event.stopPropagation();
70         event.preventDefault();
71         initiatePrivateChat(this.id, this.title);
72     });
73     initiateNewChatsPeriodically = function() {
74         $.getJSON('index.php?action=check_new_messages'function(data) {
75             if (data != undefined && data.id) {
76                 initiatePrivateChat(data.id, data.name);
77             }
78             // refresh last nav time
79             setTimeout(function(){
80                initiateNewChatsPeriodically();
81             }, 6000); // 1 mins
82         });
83     }
84     initiateNewChatsPeriodically();
85 });

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!