Saturday, May 4, 2013

Multiple notification system using PHP, jQuery/JavaScript and Twitter Bootstrap.





It's pretty simple to create notifications for one type, but if you are in the same position as I was, you need multiple such as mail, comment, friend requests and probably more. I created a quiz site (http://www.presskick.com) that requires a manifold of notifications and it had me stumped for a while.

In the beginning, I just had mail and friend request notifications. What I had set up effectively notified the user, but it wasn't really a notification system per se. It was just pulling data from a friend table and mail table where the friend request hasn't been handled and the mail hasn't been read. That would work, but it wasn't very flexible.

What I needed to do was make notifications its own entity. A good place to start is to get the database structure squared away so that you'll know what needs to be retrieved and inserted.

Database Schema

 CREATE TABLE IF NOT EXISTS `notification` (<br />  
 `id` int(10) NOT NULL AUTO_INCREMENT,<br />  
 `to_user` int(10) NOT NULL DEFAULT '0',<br />  
 `from_user` int(10) NOT NULL DEFAULT '0',<br />  
 `reference` int(10) NOT NULL DEFAULT '0',<br />  
 `type` enum('friend_request','mail','profile_comment','photo_comment') NOT NULL,<br />  
 `seen` tinyint(4) NOT NULL DEFAULT '0',<br />  
 `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,<br />  
 PRIMARY KEY (`id`),<br />  
 KEY `to_user` (`to_user`),<br />  
 KEY `from_user` (`from_user`),<br />  
 KEY `reference` (`reference`)<br />  
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8  

Most of these fields should be self explanatory except for maybe "reference." The "reference" will be the key from another table like mail id or comment id. It could also be a profile id, but that would be ambiguous since that would most likely be the same as "from_user." "timestamp" is obviously to indicate when the notification was added. "seen" is used to determine if the notification has been seen. If it's been seen, it can still be shown, but they won't be alerted about it on the site. I would also like to add, you can replace the "reference" field with a field that contains all the html of the notification. This would be the easiest, most laziest method, however, it would be a waste of disk space.
Next, we should probably create a Notification class. This class will handle retrieving, adding and deleting notifications.

Notification Class

 <?php  
 class Notification {  
   var $type;  
   var $to_user;  
   var $from_user;  
   var $reference;  
   var $timestamp;  
   var $newcount;  
   public function getAllNotifications() {  
     $this->newcount = Notification::newCount($this->to_user);  
     $sql = "SELECT n.*,u.defaultpic,u.username FROM notification n INNER JOIN user u ON u.user_id = n.to_user ORDER BY `timestamp` DESC LIMIT 10";  
     $result = mysql_query($sql);  
     echo mysql_error();  
     if ($result) {  
       return $result;  
     }  
     return false; //none found  
   }  
   public function Add() {  
     $sql = "INSERT INTO notification (to_user,from_user,reference,type) VALUES ({$this->to_user},{$this->from_user},{$this->reference},'{$this->type}')";  
     mysql_query($sql);  
   }  
   static function Seen($id) {  
     if (!isset($_SESSION['id'])) exit;  
     $sql = "UPDATE notification SET seen = 1 WHERE id = {$id} AND to_user = {$_SESSION['id']}";  
     mysql_query($sql);  
   }  
   static function newCount($user) {  
     $sqlcnt = "SELECT count(*) FROM notification WHERE to_user = {$user} AND seen = 0";  
     $result = mysql_query($sqlcnt);  
     $row = mysql_fetch_row($result);  
     return $row[0];  
   }  
   static function deleteNotification($id) {  
     if (!isset($_SESSION['id'])) exit;  
     $sql = "DELETE FROM notification WHERE id = {$id} AND to_user = {$_SESSION['id']}";  
     mysql_query($sql);  
   }  
 }  
 ?>  

The getAllNotifications method returns everything in the notification table and the default user pic and username. Of course, the structure of your own user table may be different, so edit it as needed.

The Seen method will be called to update all the notifications "seen" field when the notifications menu is opened.

The deleteNotification method will delete the notification permanently, and it will never be displayed again.
Now, we need a file that will utilize this class. This file will be called asynchronously and output the html for recent notifications.

The Add notification adds a new notification. To add a notification for a new mail message, for example, do the following:

             $notification = new Notification();  
             $notification->to_user = $to_user_id;  
             $notification->from_user = $from_user_id;  
             $notification->type = "mail";  
             $notification->reference = $mail_id;  
             $notification->Add();  

The Notifications Ajax File

 <?php  
 if (!isset($_SESSION['id'])) exit;  
 include("Notification.php");  
 $id = $_SESSION['id'];  
 $notification = new Notification();  
 $notification->to_user = $id;  
 $notifications = $notification->getAllNotifications();  
 if ($notifications) {  
   echo $notification->newcount . "|";  
   $unseen_ids = array();  
   while ($object = mysql_fetch_object($notifications)) {  
     if ($object->seen == 0) $unseen_ids[] = $object->id;  
     switch($object->type) {  
       case "friend_request":  
         ?>  
         <li id="notification_<?=$object->id;?>">  
           <div style="width:350px;padding:5px;">  
             <a href="profile.php?id=<?=$object->from_user;?>"><img src="<?=$object->defaultpic;?>" style="float:left;" width="50px" height="50px"/>&nbsp;<?=$displayName;?></a> would like to be your friend!<br />  
             &nbsp;<a href="#" onclick="HandleRequest('accept','<?=$object->from_user;?>');">Accept</a>&nbsp;&nbsp;<a href="#" onclick="HandleRequest('deny','<?=$object->from_user;?>');">Deny</a>  
           </div><br style="clear:both;"/>  
         </li>  
         <?php  
         break;  
       case "mail":  
         ?>  
         <li id="notification_<?=$object->id;?>">  
           <div style="width:350px;padding:5px;">  
             <a href="profile.php?id=<?=$object->from_user;?>"><img src="<?=$object->defaultpic;?>" width="50px" height="50px"/></a>&nbsp;<a href="message.php?id=<?=$object->reference;?>"><?=$displayName;?> has sent you a message!</a>  
             <a href="javascript:void(0)" onclick="DeleteNotification(<?=$object->id;?>)" style="float:right;"><i class="icon-trash"></i></a>  
           </div>  
         </li>  
         <?php  
         break;  
                     //TODO: add cases for other notifications  
     }  
   }  
   echo "|".json_encode($unseen_ids);  
 }  
 ?>  
This file retrieves the notifications and outputs the html. The count of new notifications is outputted followed by a pipe (|) symbol. I'm using the pipe to separate the count and the html of the notifications, and our js file will handle parsing it. After the count is outputted, we loop through the notifications, check the type and output the html appropriately. Finally, after the loop, we output the possible unseen ids.

The Update Notifications Ajax file

 <?php    
 if (!isset($_SESSION['id'])) exit; 
 include("Notification.php"); 
 $id = $_SESSION['id'];  
 $action = $_REQUEST['action'];  
 switch($action) {  
   case "seen":  
     if (isset($_REQUEST['notifications'])) {  
       $notifications = json_decode($_REQUEST['notifications']);  
       foreach ($notifications as $notification) {  
         if (is_numeric($notification)) Notification::Seen($notification);  
       }  
     }  
     break;  
   case "delete":  
     $notification = $_REQUEST['notification'];  
     if (is_numeric($notification)) Notification::deleteNotification($notification);  
     break;  
 }  
 ?>  

Depending on the action sent, this file deletes a notification or updates the "seen" field of a list of notifications.

The JS File

 var unseen_id_array = new Array();  
 function CheckUpdates()  
 {  
   jQuery.ajax({url:"ajax/notifications.php", dataType:"html", success: function(msg) {  
       if (msg != "") {  
         var result = msg.split("|");  
         var unseen = parseInt(result[0]);  
         var notifications = result[1];  
         var unseen_ids = result[2];  
         if (unseen > 0) {  
           $('#notification-badge').css("display", "inline");  
           $('#notification-badge').html(unseen);  
           for (i = 0; i < unseen_ids.length; i++) {  
             unseen_id_array.push(unseen_ids[i]);  
           }  
         }  
         jQuery('#notifications').html(notifications);  
       } else {jQuery('#notifications').html("No notifications...");}  
     }  
   })  
 }  
 function DeleteNotification(id) {  
   jQuery.ajax({url:"ajax/update_notifications.php", data:"notification="+id+"&action=delete", dataType:"html", success: function(msg) {  
       $("#notification_"+id).hide();  
     }  
   });  
 }  
 function SeenNotification() {  
   jQuery.ajax({url:"ajax/update_notifications.php", data:"notifications="+JSON.stringify(unseen_id_array)+"&action=seen", dataType:"html", success: function(msg) {  
       setTimeout(function() {  
         $('#notification-badge').css("display", "none");  
         $('#notification-badge').html("");  
       },1000);  
     }  
   });  
 }  
 $(document).ready(function() {  
   $('.notifications').click(function() {  
     //TODO: stop CheckUpdates interval and restart menu closes  
     SeenNotification();  
   });  
   $('.dropdown-menu').click(function(event){  
      event.stopPropagation();  
   });  
 })  
 CheckUpdates();  
 var intervalId = setInterval(CheckUpdates,60000);  

function CheckUpdates()

This function utilizes the very useful jquery ajax method to call our ajax file and store its results in the "msg" variable. At the start, you can see that the result variable is an array that contains the unseen count, notifications and unseen ids. We then check if "unseen" is greater than 0, and if it is, we display our badge (Labels and Badges) with the count. The global array, unseen_id_array
is populated after. "#notifications" is the id of an empty UL element, which we embed the list of LI elements - containing our notifications - in it.

function DeleteNotification(id)

This function makes a call to our update_notifications (or whatever you call it) file and passes the notification id and action type. Then it simply removes that li tag using it's id.


function SeenNotification()

Like DeleteNotification, this function makes a call to update_notifications, passing a list of ids and the action type. If it's successful, it will remove the html from the badge and hide it. I placed that in a setTimeout function so the badge is remove a second after. That is a personal preference of mine.

I binded a couple of events to trigger after the document is loaded. SeenNotification() is called when the notifications link is clicked. Then I prevented the menu from closing by using event.stopPropagation(). Without that, the menu will close when you click anywhere in the menu like the trash icon.

Relevant HTML

      <ul class="nav">  
        <li class="dropdown"><a class="dropdown-toggle notifications" data-toggle="dropdown" href="#"><strong>Notifications</strong> <span id="notification-badge" class="badge badge-important"></span></a>  
          <ul id="notifications" class="dropdown-menu" role="menu" aria-labelledby="dLabel"></ul>  
        </li>  
      </ul>  
As the title mentions, I'm using twitter bootstrap, so the bulk of the front-end comes from it. I just added an empty menu item to server as our notifications. Visit the Bootstrap Navigation section to learn more about it. I also added the trash icon (See bootstrap icons) and when clicked, it calls DeleteNotification(id).


Conclusion

This is definitely not something that can be cut and pasted and expect to work. Everyone's project is different. If bootstrap isn't part of your project, the front-end might be a little difficult, however, a lot of the menu systems out there are set up similar. Use this code as a guideline and tweak it to your needs. If you need help or just want to give a piece of your mind, comment below. I hope this code will be helpful. Updates are surely to come.


11 comments:

  1. Hi,

    You have did some awesome work.

    But i need this to make it work with Codeigniter.Please send me if you have code in codeigniter.

    Thanks & Regards,
    Zeeshan.

    ReplyDelete
  2. hey i can't get about the use of twitter bootstrap here in this code.can u please mention me where it is used and how it is implemented?

    ReplyDelete
    Replies
    1. Have you added the bootstrap framework? http://getbootstrap.com/

      Delete
  3. I believe that I have for the most part everything setup correctly. The issue that I have is that "No notifications…" is displayed even though I have a test notification in my database. What would you recommend?

    ReplyDelete
    Replies
    1. The "ajax/notification.php" file assumes you are a logged in user. $_SESSION['id'] is supposed to be your user id. I think that may be the problem. Try going directly to that page and see if what errors you get. You probably already resolved this, but I'm responding anyway.

      Delete
  4. Sorry if I misunderstood your class. But in the getAllNotifications function, in your select query, don't you need a 'where' clause to list only notification to a given user?

    Thanks,

    ReplyDelete
  5. Yes, you're correct, there needs to be a where clause. Also, the "echo mysql_error()" was debugging code I left in there and should be removed.

    Sorry for extremely late response. I'm just getting back to the grind, and will be around more often now.

    ReplyDelete
  6. How will i get this working on adminlte?

    ReplyDelete
  7. How will i get this working on adminlte?

    ReplyDelete