Сегодня мы выложим для вас обновленные исходники нашего развивающегося проекта. Весь проект разложен по полочкам: системные классы будут в папке «classes», javascript-файлы в папке «js», таблицы стилей в папке «css», файлы шаблона в папке «templates», а все пользовательские аватары будут помещены в папку под названием «data».

Этап 1 – SQL
2 файла шаблона были обновлены:
templates/main_page.html
templates/profile_page.html
Пожалуйста, добавьте всего 2 строки (как показано в коде ниже) сразу перед закрывающим тэгом body (в оба указанных файла шаблона):
....
<div class="priv_dock_wrap"></div>
{priv_js}
</body>
</html>
Это и будет наша новая панель для сообщений (фиксированный элемент на дне страницы).
Этап 2 – CSS-код
Мы обновили вторую часть нашего CSS-файла (от строки 180 до самого конца файла):
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;
}
Теперь он содержит новые стили для системы обмена личными сообщениями.
Этап 3 – PHP
Теперь пришло время проверить изменения в исходном коде PHP.
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);
Сюда мы добавили несколько новых положений для нашего чата: проверка новых личных сообщений, получение личных сообщений, отправка личных сообщений. Наш следующий обновленный файл – это файл просмотра профиля (куда мы добавили новый js-файл 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);
Следующий обновленный файл:
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();
Мы решили выложить полный код данного файла, так как в него было внесено несколько изменений, и к тому же мы добавили две новые функции: acceptPrivMessage (для приема личных сообщений) и getRecentMessage (для проверки, не получал ли пользователь новые личные сообщений. Если будут обнаружены новые сообщений, и пользователь при этом не открывал окно личного чата – мы инициализируем новую сессию чата с личными сообщениями).
Следующий обновленный файл:
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>';
}
Мы обновили лишь одну функцию: getProfilesBlock. Теперь можно наблюдать новую иконку chat.png. Мы можем кликнуть по ней для того, чтобы начать диалог в чате. Обратите внимание на прокомментированный код (/*&& $aProfile['is_online'] == 1*/). Вы можете исключить комментарий для того, чтобы дать возможность переписываться только авторизованным пользователям.
Этап 4 – javascript
js/priv_chat.js
Новый javascript-код для функционала нашего чата с личными сообщениями:
$(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();
});
Посмотреть демо | Скачать архив
Внимание! У вас нет прав для просмотра скрытого текста.
Завершение
Надеемся, что вы следите за нашей серией статей о создании чата, и также надеемся, что вам интересно. Если у вас есть какие-либо идеи, то мы обязательно обсудим их с вами и другими читателями.