diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 1e95dde3ec1c033f51fb70b13456739aafea7cfb..d7f53279963b4272686c7d50a1cf19e0b67bb4bd 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -192,7 +192,6 @@ class TicketsAjaxAPI extends AjaxController {
         include STAFFINC_DIR . 'templates/ticket-preview.tmpl.php';
     }
 
-
     function viewUser($tid) {
         global $thisstaff;
 
diff --git a/include/class.collaborator.php b/include/class.collaborator.php
index 205b5200b151911c757611483fae9560f041bb27..6bc891e5fd9fd53d19789faac4de813fc2ab1dfc 100644
--- a/include/class.collaborator.php
+++ b/include/class.collaborator.php
@@ -35,8 +35,10 @@ implements EmailContact, ITicketUser {
     );
 
     function __toString() {
-        return Format::htmlchars(sprintf('%s <%s>', $this->getName(),
-                $this->getEmail()));
+        return Format::htmlchars($this->toString());
+    }
+    function toString() {
+        return sprintf('%s <%s>', $this->getName(), $this->getEmail());
     }
 
     function getId() {
diff --git a/include/class.orm.php b/include/class.orm.php
index 57029871265b38038f32d5726038a724d39d3de0..b7250ee94a2a18a75cb2cb76e9991b6f673585bf 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -193,7 +193,8 @@ class VerySimpleModel {
                     $fkey[$F ?: $_klas] = ($local[0] == "'")
                         ? trim($local, "'") : $this->ht[$local];
                 }
-                $v = $this->ht[$field] = new InstrumentedList(
+                $manager = @$j['class'] ?: 'InstrumentedList';
+                $v = $this->ht[$field] = new $manager(
                     // Send Model, [Foriegn-Field => Local-Id]
                     array($class, $fkey)
                 );
diff --git a/include/class.thread.php b/include/class.thread.php
index 75d5a14a396bf1830b0708f89f6a6d0f7caeb72d..285bd9f163144b93cb69794d6639e4ee4d40ac4b 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -42,6 +42,10 @@ class Thread extends VerySimpleModel {
             'entries' => array(
                 'reverse' => 'ThreadEntry.thread',
             ),
+            'events' => array(
+                'reverse' => 'ThreadEvent.thread',
+                'class' => 'ThreadEvents',
+            ),
         ),
     );
 
@@ -198,6 +202,7 @@ class Thread extends VerySimpleModel {
         if ($type && is_array($type))
             $entries->filter(array('type__in' => $type));
 
+        $events = $this->getEvents();
         include STAFFINC_DIR . 'templates/thread-entries.tmpl.php';
     }
 
@@ -205,6 +210,10 @@ class Thread extends VerySimpleModel {
         return ThreadEntry::lookup($id, $this->getId());
     }
 
+    function getEvents() {
+        return $this->events;
+    }
+
     /**
      * postEmail
      *
@@ -1406,6 +1415,244 @@ implements TemplateVariable {
 
 RolePermission::register(/* @trans */ 'Tickets', ThreadEntry::getPermissions());
 
+class ThreadEvent extends VerySimpleModel {
+    static $meta = array(
+        'table' => TICKET_EVENT_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            'thread' => array(
+                'constraint' => array('thread_id' => 'Thread.id'),
+            ),
+            'staff' => array(
+                'constraint' => array(
+                    'uid' => 'Staff.staff_id',
+                ),
+                'null' => true,
+            ),
+            'user' => array(
+                'constraint' => array(
+                    'uid' => 'User.id',
+                ),
+                'null' => true,
+            ),
+            'dept' => array(
+                'constraint' => array(
+                    'dept_id' => 'Dept.id',
+                ),
+                'null' => true,
+            ),
+        ),
+    );
+
+    // Valid events for database storage
+    const ASSIGNED  = 'assigned';
+    const CLOSED    = 'closed';
+    const CREATED   = 'created';
+    const COLLAB    = 'collab';
+    const EDITED    = 'edited';
+    const ERROR     = 'error';
+    const OVERDUE   = 'overdue';
+    const REOPENED  = 'reopened';
+    const STATUS    = 'status';
+    const TRANFERRED = 'transferred';
+    const VIEWED    = 'viewed';
+
+    const MODE_STAFF = 1;
+    const MODE_CLIENT = 2;
+
+    var $_data;
+
+    function getUserName() {
+        if ($this->uid && $this->uid_type == 'S')
+            return $this->staff->getName();
+        if ($this->uid && $this->uid_type == 'U')
+            return $this->user->getName();
+        return $this->username;
+    }
+
+    function getIcon() {
+        $icons = array(
+            'assigned' => 'hand-right',
+            'collab' => 'group',
+            'created' => 'magic',
+            'overdue' => 'time',
+        );
+        return @$icons[$this->state] ?: 'chevron-sign-right';
+    }
+
+    function getDescription() {
+        static $descs;
+        if (!isset($descs))
+            $descs = array(
+            'assigned:staff' => __('<b>{username}</b> assigned this to <strong>{<Staff>data.staff}</strong> {timestamp}'),
+            'assigned:team' => __('<b>{username}</b> assigned this to <strong>{<Team>data.team}</strong> {timestamp}'),
+            'assigned:claim' => __('<b>{username}</b> claimed this {timestamp}'),
+            'collab:org' => __('Collaborators for {<Organization>data.org} organization added'),
+            'collab:del' => function($evt) {
+                $data = $evt->getData();
+                $base = __('<b>{username}</b> removed %s from the collaborators.');
+                return $data['del']
+                    ? Format::htmlchars(sprintf($base, implode(', ', $data['del'])))
+                    : 'somebody';
+            },
+            'collab:add' => function($evt) {
+                $data = $evt->getData();
+                $base = __('<b>{username}</b> added %s as collaborators {timestamp}');
+                $collabs = array();
+                if ($data['add']) {
+                    foreach ($data['add'] as $c) {
+                        $collabs[] = '<b>'.Format::htmlchars($c).'</b>';
+                    }
+                }
+                return $collabs
+                    ? sprintf($base, implode(', ', $collabs))
+                    : 'somebody';
+            },
+            'created' => __('<strong>Created</strong> by <b>{username}</b> {timestamp}'),
+            'edited:owner' => __('<b>{username}</b> changed ownership to {<User>data.owner} {timestamp}'),
+            'edited:status' => __('<b>{username}</b> changed the status to <strong>{<TicketStatus>data.status}</strong> {timestamp}'),
+            'overdue' => __('Flagged as overdue by the system {timestamp}'),
+            'transferred' => __('<b>{username}</b> transferred this to {dept} {timestamp}'),
+        );
+        $self = $this;
+        $data = $this->getData();
+        $state = $this->state;
+        if (is_array($data)) {
+            foreach (array_keys($data) as $k)
+                if (isset($descs[$state . ':' . $k]))
+                    $state .= ':' . $k;
+        }
+        $description = $descs[$state];
+        if (is_callable($description))
+            $description = $description($this);
+
+        return preg_replace_callback('/\{(<(?P<type>([^>]+))>)?(?P<key>[^}.]+)(\.(?P<data>[^}]+))?\}/',
+            function ($m) use ($self) {
+                switch ($m['key']) {
+                case 'username':
+                    return $self->getUserName();
+                case 'timestamp':
+                    return sprintf('<time class="relative" datetime="%s" title="%s">%s</time>',
+                        date(DateTime::W3C, Misc::db2gmtime($self->timestamp)),
+                        Format::daydatetime($self->timestamp),
+                        Format::relativeTime(Misc::db2gmtime($self->timestamp))
+                    );
+                case 'dept':
+                    if ($dept = $this->getDept())
+                        return $dept->getLocalName();
+                case 'data':
+                    $val = $self->getData($m['data']);
+                    if ($m['type'] && class_exists($m['type']))
+                        $val = $m['type']::lookup($val);
+                    return (string) $val;
+                }
+                return $m[0];
+            },
+            $description
+        );
+    }
+
+    function getDept() {
+        return $this->dept;
+    }
+
+    function getData($key=false) {
+        if (!isset($this->_data))
+            $this->_data = JsonDataParser::decode($this->data);
+        return ($key) ? @$this->_data[$key] : $this->_data;
+    }
+
+    function render($mode) {
+        if ($mode == self::MODE_STAFF) {
+            $event = $this;
+            include STAFFINC_DIR . 'templates/thread-event.tmpl.php';
+        }
+    }
+
+    static function create($ht=false) {
+        $inst = parent::create($ht);
+        $inst->timestamp = SqlFunction::NOW();
+
+        global $thisstaff, $thisclient;
+        if ($thisstaff) {
+            $inst->uid_type = 'S';
+            $inst->uid = $thisstaff->getId();
+        }
+        else if ($thisclient) {
+            $inst->uid_type = 'U';
+            $inst->uid = $thisclient->getId();
+        }
+
+        return $inst;
+    }
+
+    static function forTicket($ticket, $state) {
+        $inst = static::create(array(
+            'staff_id' => $ticket->getStaffId(),
+            'team_id' => $ticket->getTeamId(),
+            'dept_id' => $ticket->getDeptId(),
+            'topic_id' => $ticket->getTopicId(),
+        ));
+        if (!isset($inst->uid_type) && $state == self::CREATED) {
+            $inst->uid_type = 'U';
+            $inst->uid = $ticket->getOwnerId();
+        }
+        return $inst;
+    }
+}
+
+class ThreadEvents extends InstrumentedList {
+    function annul($event) {
+        $this->queryset
+            ->filter(array('state' => $event))
+            ->update(array('annulled' => 1));
+    }
+
+    function log($object, $state, $data=null, $annul=null, $username=null) {
+        if ($object instanceof Ticket)
+            $event = ThreadEvent::forTicket($object, $state);
+        else
+            $event = ThreadEvent::create();
+
+        # Annul previous entries if requested (for instance, reopening a
+        # ticket will annul an 'closed' entry). This will be useful to
+        # easily prevent repeated statistics.
+        if ($annul) {
+            $this->annul($annul);
+        }
+
+        if ($username === null) {
+            if ($thisstaff) {
+                $username = $thisstaff->getUserName();
+            }
+            else if ($thisclient) {
+                if ($thisclient->hasAccount)
+                    $username = $thisclient->getAccount()->getUserName();
+                if (!$username)
+                    $username = $thisclient->getEmail();
+            }
+            else {
+                # XXX: Security Violation ?
+                $username = 'SYSTEM';
+            }
+        }
+        $event->username = $username;
+        $event->state = $state;
+
+        if ($data) {
+            if (is_array($data))
+                $data = JsonDataEncoder::encode($data);
+            if (!is_string($data))
+                throw new InvalidArgumentException('Data must be string or array');
+            $event->data = $data;
+        }
+
+        $this->add($event);
+
+        // Save event immediately
+        return $event->save();
+    }
+}
 
 class ThreadEntryBody /* extends SplString */ {
 
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 605758114102bcc1dfe959abec689b933a8e4fb9..0f9ed9386d066a649aaeae6c3b1067bb4d467474 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -943,6 +943,8 @@ implements RestrictedAccess, Threadable, TemplateVariable {
         $this->collaborators = null;
         $this->recipients = null;
 
+        $this->logEvent('collab', array('add' => array($c->toString())));
+
         return $c;
     }
 
@@ -959,11 +961,10 @@ implements RestrictedAccess, Threadable, TemplateVariable {
                 if (($c=Collaborator::lookup($cid))
                         && $c->getTicketId() == $this->getId()
                         && $c->delete())
-                     $collabs[] = $c;
+                     $collabs[] = (string) $c;
             }
 
-            $this->logNote(_S('Collaborators Removed'),
-                    implode("<br>", $collabs), $thisstaff, false);
+            $this->logEvent('collab', array('del' => $collabs));
         }
 
         //statuses
@@ -1164,6 +1165,7 @@ implements RestrictedAccess, Threadable, TemplateVariable {
             }
         }
 
+        $hadStatus = $this->getStatusId();
         if ($this->getStatusId() == $status->getId())
             return true;
 
@@ -1196,7 +1198,7 @@ implements RestrictedAccess, Threadable, TemplateVariable {
                 if ($this->isClosed()) {
                     $sql .= ',closed=NULL, lastupdate=NOW(), reopened=NOW() ';
                     $ecb = function ($t) {
-                        $t->logEvent('reopened', 'closed');
+                        $t->logEvent('reopened', false, 'closed');
                     };
                 }
 
@@ -1228,12 +1230,15 @@ implements RestrictedAccess, Threadable, TemplateVariable {
                 $note .= sprintf('<hr>%s', $comments);
                 // Send out alerts if comments are included
                 $alert = true;
+                $this->logNote(__('Status Changed'), $note, $thisstaff, $alert);
             }
-
-            $this->logNote(__('Status Changed'), $note, $thisstaff, $alert);
         }
         // Log events via callback
-        if ($ecb) $ecb($this);
+        if ($ecb)
+            $ecb($this);
+        elseif ($hadStatus)
+            // Don't log the initial status change
+            $this->logEvent('edited', array('status' => $status->getId()));
 
         return true;
     }
@@ -1662,13 +1667,16 @@ implements RestrictedAccess, Threadable, TemplateVariable {
 
         $this->reload();
 
+        $user_comments = (bool) $comments;
         $comments = $comments ?: _S('Ticket assignment');
         $assigner = $thisstaff ?: _S('SYSTEM (Auto Assignment)');
 
         //Log an internal note - no alerts on the internal note.
-        $note = $this->logNote(
-            sprintf(_S('Ticket Assigned to %s'), $assignee->getName()),
-            $comments, $assigner, false);
+        if ($user_comments) {
+            $note = $this->logNote(
+                sprintf(_S('Ticket Assigned to %s'), $assignee->getName()),
+                $comments, $assigner, false);
+        }
 
         //See if we need to send alerts
         if(!$alert || !$cfg->alertONAssignment()) return true; //No alerts!
@@ -1971,8 +1979,11 @@ implements RestrictedAccess, Threadable, TemplateVariable {
         /*** log the transfer comments as internal note - with alerts disabled - ***/
         $title=sprintf(_S('Ticket transferred from %1$s to %2$s'),
             $currentDept, $this->getDeptName());
-        $comments=$comments?$comments:$title;
-        $note = $this->logNote($title, $comments, $thisstaff, false);
+
+        if ($comments) {
+            $note = $this->logNote($title, $comments, $thisstaff, false);
+        }
+        $comments = $comments ?: $title;
 
         $this->logEvent('transferred');
 
@@ -2004,11 +2015,13 @@ implements RestrictedAccess, Threadable, TemplateVariable {
             if($cfg->alertDeptManagerONTransfer() && $dept && ($manager=$dept->getManager()))
                 $recipients[]= $manager;
 
-            $sentlist=array();
-            $options = array(
-                'inreplyto'=>$note->getEmailMessageId(),
-                'references'=>$note->getEmailReferences(),
-                'thread'=>$note);
+            $sentlist = $options = array();
+            if ($note) {
+                $options += array(
+                    'inreplyto'=>$note->getEmailMessageId(),
+                    'references'=>$note->getEmailReferences(),
+                    'thread'=>$note);
+            }
             foreach( $recipients as $k=>$staff) {
                 if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
@@ -2030,9 +2043,7 @@ implements RestrictedAccess, Threadable, TemplateVariable {
         if ($dept->assignMembersOnly() && !$dept->isMember($thisstaff))
             return false;
 
-        $comments = sprintf(_S('Ticket claimed by %s'), $thisstaff->getName());
-
-        return $this->assignToStaff($thisstaff->getId(), $comments, false);
+        return $this->assignToStaff($thisstaff->getId(), null, false);
     }
 
     function assignToStaff($staff, $note, $alert=true) {
@@ -2044,7 +2055,14 @@ implements RestrictedAccess, Threadable, TemplateVariable {
             return false;
 
         $this->onAssign($staff, $note, $alert);
-        $this->logEvent('assigned');
+
+        global $thisstaff;
+        $data = array();
+        if ($staff->getId() == $thisstaff->getId())
+            $data['claim'] = true;
+        else
+            $data['staff'] = $staff->getId();
+        $this->logEvent('assigned', $data);
 
         return true;
     }
@@ -2063,7 +2081,7 @@ implements RestrictedAccess, Threadable, TemplateVariable {
             $this->setStaffId(0);
 
         $this->onAssign($team, $note, $alert);
-        $this->logEvent('assigned');
+        $this->logEvent('assigned', array('team' => $team->getId()));
 
         return true;
     }
@@ -2134,18 +2152,15 @@ implements RestrictedAccess, Threadable, TemplateVariable {
         $this->collaborators = null;
         $this->recipients = null;
 
-        //Log an internal note
-        $note = sprintf(_S('%s changed ticket ownership to %s'),
-                $thisstaff->getName(), $user->getName());
-
-        //Remove the new owner from list of collaborators
+        // Remove the new owner from list of collaborators
         $c = Collaborator::lookup(array(
-                    'user_id' => $user->getId(),
-                    'thread_id' => $this->getThreadId()));
-        if ($c && $c->delete())
-            $note.= ' '._S('(removed as collaborator)');
+            'user_id' => $user->getId(),
+            'thread_id' => $this->getThreadId()
+        ));
+        if ($c)
+            $c->delete();
 
-        $this->logNote('Ticket ownership changed', $note);
+        $this->logEvent('edited', array('owner' => $user->getId()));
 
         return true;
     }
@@ -2184,18 +2199,11 @@ implements RestrictedAccess, Threadable, TemplateVariable {
 
                 if (($user=User::fromVars($recipient)))
                     if ($c=$this->addCollaborator($user, $info, $errors))
-                        $collabs[] = sprintf('%s%s',
-                            (string) $c,
-                            $recipient['source']
-                                ? " ".sprintf(_S('via %s'), $recipient['source'])
-                                : ''
-                            );
+                        $collabs[] = array((string)$c, $recipient['source']);
             }
             //TODO: Can collaborators add others?
             if ($collabs) {
-                //TODO: Change EndUser to name of  user.
-                $this->logNote(_S('Collaborators added by end user'),
-                        implode("<br>", $collabs), _S('End User'), false);
+                $this->logEvent('collab', array('add' => $collabs));
             }
         }
 
@@ -2411,31 +2419,8 @@ implements RestrictedAccess, Threadable, TemplateVariable {
     }
 
     // History log -- used for statistics generation (pretty reports)
-    function logEvent($state, $annul=null, $staff=null) {
-        global $thisstaff;
-
-        if ($staff === null) {
-            if ($thisstaff) $staff=$thisstaff->getUserName();
-            else $staff='SYSTEM';               # XXX: Security Violation ?
-        }
-        # Annul previous entries if requested (for instance, reopening a
-        # ticket will annul an 'closed' entry). This will be useful to
-        # easily prevent repeated statistics.
-        if ($annul) {
-            db_query('UPDATE '.TICKET_EVENT_TABLE.' SET annulled=1'
-                .' WHERE ticket_id='.db_input($this->getId())
-                  .' AND state='.db_input($annul));
-        }
-
-        return db_query('INSERT INTO '.TICKET_EVENT_TABLE
-            .' SET ticket_id='.db_input($this->getId())
-            .', staff_id='.db_input($this->getStaffId())
-            .', team_id='.db_input($this->getTeamId())
-            .', dept_id='.db_input($this->getDeptId())
-            .', topic_id='.db_input($this->getTopicId())
-            .', timestamp=NOW(), state='.db_input($state)
-            .', staff='.db_input($staff))
-            && db_affected_rows() == 1;
+    function logEvent($state, $data=null, $annul=null, $staff=null) {
+        $this->getThread()->getEvents()->log($this, $state, $data, $annul, $staff);
     }
 
     //Insert Internal Notes
@@ -3262,10 +3247,7 @@ implements RestrictedAccess, Threadable, TemplateVariable {
             }
             //TODO: Can collaborators add others?
             if ($collabs) {
-                //TODO: Change EndUser to name of  user.
-                $ticket->logNote(sprintf(_S('Collaborators for %s organization added'),
-                        $org->getName()),
-                    implode("<br>", $collabs), $org->getName(), false);
+                $ticket->logEvent('collab', array('org' => $org->getId()));
             }
         }
 
@@ -3418,11 +3400,6 @@ implements RestrictedAccess, Threadable, TemplateVariable {
             }
             $ticket->logNote(_S('New Ticket'), $vars['note'], $thisstaff, false);
         }
-        else {
-            // Not assignment and no internal note - log activity
-            $ticket->logActivity(_S('New Ticket by Agent'),
-                sprintf(_S('Ticket created by agent - %s'), $thisstaff->getName()));
-        }
 
         $ticket->reload();
 
@@ -3493,9 +3470,8 @@ implements RestrictedAccess, Threadable, TemplateVariable {
 
         if(($res=db_query($sql)) && db_num_rows($res)) {
             while(list($id)=db_fetch_row($res)) {
-                if(($ticket=Ticket::lookup($id)) && $ticket->markOverdue())
-                    $ticket->logActivity(_S('Ticket Marked Overdue'),
-                        _S('Ticket flagged as overdue by the system.'));
+                if ($ticket=Ticket::lookup($id))
+                    $ticket->markOverdue();
             }
         } else {
             //TODO: Trigger escalation on already overdue tickets - make sure last overdue event > grace_period.
diff --git a/include/class.user.php b/include/class.user.php
index 1b4e309d8af92aae4aef4e277ae23df2c0ffb894..02e45ae14b20d5dca88df1c25078c0109ab110ca 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -39,7 +39,7 @@ class UserModel extends VerySimpleModel {
     static $meta = array(
         'table' => USER_TABLE,
         'pk' => array('id'),
-        'select_related' => array('default_email'),
+        'select_related' => array('account', 'default_email'),
         'joins' => array(
             'emails' => array(
                 'reverse' => 'UserEmailModel.user',
@@ -123,6 +123,9 @@ class UserModel extends VerySimpleModel {
         return $this->default_email;
     }
 
+    function hasAccount() {
+        return !is_null($this->account);
+    }
     function getAccount() {
         return $this->account;
     }
diff --git a/include/staff/faq-category.inc.php b/include/staff/faq-category.inc.php
index afc5194bb7c08ebfe4ea4caeed74072a5b434db4..b6287bcb9eb3779a9aab6aa5a49dc244683e1856 100644
--- a/include/staff/faq-category.inc.php
+++ b/include/staff/faq-category.inc.php
@@ -11,7 +11,7 @@ if(!defined('OSTSTAFFINC') || !$category || !$thisstaff) die('Access Denied');
 <div>
     <strong><?php echo $category->getName() ?></strong>
     <span>(<?php echo $category->isPublic()?__('Public'):__('Internal'); ?>)</span>
-    <time> <?php echo __('Last updated').' '. Format::daydatetime($category->getUpdateDate()); ?></time>
+    <time class="faq"> <?php echo __('Last updated').' '. Format::daydatetime($category->getUpdateDate()); ?></time>
 </div>
 <div class="cat-desc">
 <?php echo Format::display($category->getDescription()); ?>
diff --git a/include/staff/templates/thread-entries.tmpl.php b/include/staff/templates/thread-entries.tmpl.php
index 2e7da2c389cf2b276b139a5573e040fb0e6590a9..c9febda653f646ecae1030f3fdc188945ca0a3b3 100644
--- a/include/staff/templates/thread-entries.tmpl.php
+++ b/include/staff/templates/thread-entries.tmpl.php
@@ -1,90 +1,26 @@
 <?php
-$entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note');
-if ($entries) {
-    foreach ($entries as $entry) { ?>
-    <table class="thread-entry <?php echo $entryTypes[$entry->type]; ?>" cellspacing="0" cellpadding="1" width="940" border="0">
-        <tr>
-            <th colspan="4" width="100%">
-            <div>
-                <span class="pull-left">
-                <span style="display:inline-block"><?php
-                    echo Format::datetime($entry->created);?></span>
-                <span style="display:inline-block;padding:0 1em;max-width: 500px" class="faded title truncate"><?php
-                    echo $entry->title; ?></span>
-                </span>
-            <div class="pull-right">
-<?php           if ($entry->hasActions()) {
-                $actions = $entry->getActions(); ?>
-                <span class="action-button pull-right" data-dropdown="#entry-action-more-<?php echo $entry->getId(); ?>">
-                    <i class="icon-caret-down"></i>
-                    <span ><i class="icon-cog"></i></span>
-                </span>
-                <div id="entry-action-more-<?php echo $entry->getId(); ?>" class="action-dropdown anchor-right">
-            <ul class="title">
-<?php               foreach ($actions as $group => $list) {
-                    foreach ($list as $id => $action) { ?>
-                <li>
-                <a class="no-pjax" href="#" onclick="javascript:
-                        <?php echo str_replace('"', '\\"', $action->getJsStub()); ?>; return false;">
-                    <i class="<?php echo $action->getIcon(); ?>"></i> <?php
-                        echo $action->getName();
-            ?></a></li>
-<?php                   }
-                } ?>
-            </ul>
-            </div>
-<?php           } ?>
-                <span style="vertical-align:middle">
-                    <span style="vertical-align:middle;" class="textra">
-        <?php if ($entry->flags & ThreadEntry::FLAG_EDITED) { ?>
-                <span class="label label-bare" title="<?php
-        echo sprintf(__('Edited on %s by %s'), Format::datetime($entry->updated), 'You');
-                ?>"><?php echo __('Edited'); ?></span>
-        <?php } ?>
-                    </span>
-                    <span style="vertical-align:middle;"
-                        class="tmeta faded title"><?php
-                        echo Format::htmlchars($entry->getName()); ?></span>
-                </span>
-            </div>
-            </th>
-        </tr>
-        <tr><td colspan="4" class="thread-body" id="thread-id-<?php
-            echo $entry->getId(); ?>"><div><?php
-            echo $entry->getBody()->toHtml(); ?></div></td></tr>
-        <?php
-        $urls = null;
-        if ($entry->has_attachments
-            && ($urls = $entry->getAttachmentUrls())) { ?>
-        <tr>
-            <td class="info" colspan="4"><?php
-                foreach ($entry->attachments as $A) {
-                    if ($A->inline) continue;
-                    $size = '';
-                    if ($A->file->size)
-                        $size = sprintf('<em>(%s)</em>',
-                            Format::file_size($A->file->size));
-?>
-            <a class="Icon file no-pjax" href="<?php echo $A->file->getDownloadUrl();
-                ?>" download="<?php echo Format::htmlchars($A->file->name); ?>"
-                target="_blank"><?php echo Format::htmlchars($A->file->name);
-            ?></a><?php echo $size;?>&nbsp;
-<?php               } ?>
-            </td>
-        </tr> <?php
+$events = $events->order_by('id');
+$events = $events->getIterator();
+$events->rewind();
+$event = $events->current();
+
+if (count($entries)) {
+    foreach ($entries as $entry) {
+        // Emit all events prior to this entry
+        while ($event && $event->timestamp <= $entry->created) {
+            $event->render(ThreadEvent::MODE_STAFF);
+            $events->next();
+            $event = $events->current();
         }
-        if ($urls) { ?>
-            <script type="text/javascript">
-                $('#thread-id-<?php echo $entry->getId(); ?>')
-                    .data('urls', <?php
-                        echo JsonDataEncoder::encode($urls); ?>)
-                    .data('id', <?php echo $entry->getId(); ?>);
-            </script>
-<?php
-        } ?>
-    </table>
-    <?php
+        include STAFFINC_DIR . 'templates/thread-entry.tmpl.php';
+    }
+    // Emit all other events
+    while ($event) {
+        $event->render(ThreadEvent::MODE_STAFF);
+        $events->next();
+        $event = $events->current();
     }
-} else {
+}
+else {
     echo '<p><em>'.__('No entries have been posted to this thread.').'</em></p>';
-}?>
+}
diff --git a/include/staff/templates/thread-entry.tmpl.php b/include/staff/templates/thread-entry.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..d24af728b0cf1608412a8391f763406f54771703
--- /dev/null
+++ b/include/staff/templates/thread-entry.tmpl.php
@@ -0,0 +1,86 @@
+<?php
+$entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note');
+?>
+
+<table class="thread-entry <?php echo $entryTypes[$entry->type]; ?>" cellspacing="0" cellpadding="1" width="940" border="0">
+    <tr>
+        <th colspan="4" width="100%">
+        <div>
+            <span class="pull-left">
+            <span style="display:inline-block"><?php
+                echo Format::datetime($entry->created);?></span>
+            <span style="display:inline-block;padding:0 1em;max-width: 500px" class="faded title truncate"><?php
+                echo $entry->title; ?></span>
+            </span>
+        <div class="pull-right">
+<?php           if ($entry->hasActions()) {
+            $actions = $entry->getActions(); ?>
+            <span class="action-button pull-right" data-dropdown="#entry-action-more-<?php echo $entry->getId(); ?>">
+                <i class="icon-caret-down"></i>
+                <span ><i class="icon-cog"></i></span>
+            </span>
+            <div id="entry-action-more-<?php echo $entry->getId(); ?>" class="action-dropdown anchor-right">
+        <ul class="title">
+<?php               foreach ($actions as $group => $list) {
+                foreach ($list as $id => $action) { ?>
+            <li>
+            <a class="no-pjax" href="#" onclick="javascript:
+                    <?php echo str_replace('"', '\\"', $action->getJsStub()); ?>; return false;">
+                <i class="<?php echo $action->getIcon(); ?>"></i> <?php
+                    echo $action->getName();
+        ?></a></li>
+<?php                   }
+            } ?>
+        </ul>
+        </div>
+<?php           } ?>
+            <span style="vertical-align:middle">
+                <span style="vertical-align:middle;" class="textra">
+        <?php if ($entry->flags & ThreadEntry::FLAG_EDITED) { ?>
+                <span class="label label-bare" title="<?php
+        echo sprintf(__('Edited on %s by %s'), Format::datetime($entry->updated), 'You');
+                ?>"><?php echo __('Edited'); ?></span>
+        <?php } ?>
+                </span>
+                <span style="vertical-align:middle;"
+                    class="tmeta faded title"><?php
+                    echo Format::htmlchars($entry->getName()); ?></span>
+            </span>
+        </div>
+        </th>
+    </tr>
+    <tr><td colspan="4" class="thread-body" id="thread-id-<?php
+        echo $entry->getId(); ?>"><div><?php
+        echo $entry->getBody()->toHtml(); ?></div></td></tr>
+    <?php
+    $urls = null;
+    if ($entry->has_attachments
+        && ($urls = $entry->getAttachmentUrls())) { ?>
+    <tr>
+        <td class="info" colspan="4"><?php
+            foreach ($entry->attachments as $A) {
+                if ($A->inline) continue;
+                $size = '';
+                if ($A->file->size)
+                    $size = sprintf('<em>(%s)</em>',
+                        Format::file_size($A->file->size));
+?>
+        <a class="Icon file no-pjax" href="<?php echo $A->file->getDownloadUrl();
+            ?>" download="<?php echo Format::htmlchars($A->file->name); ?>"
+            target="_blank"><?php echo Format::htmlchars($A->file->name);
+        ?></a><?php echo $size;?>&nbsp;
+<?php               } ?>
+        </td>
+    </tr> <?php
+    }
+    if ($urls) { ?>
+        <script type="text/javascript">
+            $('#thread-id-<?php echo $entry->getId(); ?>')
+                .data('urls', <?php
+                    echo JsonDataEncoder::encode($urls); ?>)
+                .data('id', <?php echo $entry->getId(); ?>);
+        </script>
+<?php
+    } ?>
+</table>
+
diff --git a/include/staff/templates/thread-event.tmpl.php b/include/staff/templates/thread-event.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..8bbf70e5c95b19b0aed8da4629bc4679c7aaa161
--- /dev/null
+++ b/include/staff/templates/thread-event.tmpl.php
@@ -0,0 +1,8 @@
+<div class="thread-event">
+    <span class="type-icon">
+      <i class="faded icon-<?php echo $event->getIcon(); ?>"></i>
+    </span>
+    <span class="faded" style="display:inline-block">
+        <?php echo $event->getDescription(); ?>
+    </span>
+</div>
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index 74407ea1952c951da73f9345baa0dc6fe88ddb5a..baf81ca4927c0f88ac121a4ca1763601b2f4c0f5 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -422,7 +422,7 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) {
 <?php
 $tcount = $ticket->getThreadEntries($types)->count();
 ?>
-<ul  class="tabs threads" id="ticket_tabs" >
+<ul  class="tabs clean threads" id="ticket_tabs" >
     <li class="active"><a href="#ticket_thread"><?php echo sprintf(__('Ticket Thread (%d)'), $tcount); ?></a></li>
     <li><a id="ticket_tasks" href="#tasks"
             data-url="<?php
@@ -437,6 +437,7 @@ $tcount = $ticket->getThreadEntries($types)->count();
     <?php
     $ticket->getThread()->render(array('M', 'R', 'N'));
     ?>
+    </div>
 <div class="clear" style="padding-bottom:10px;"></div>
 <?php if($errors['err']) { ?>
     <div id="msg_error"><?php echo $errors['err']; ?></div>
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 4dd7cef3afd1c4cf0b48512b451375e6146e447f..e2709e74a17034203f0902251e589a903f31af18 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -55,10 +55,21 @@ div#header a {
     color: #666;
     color: rgba(0,0,0,0.5);
 }
+.faded b {
+    color: #333;
+    color: rgba(0,0,0,0.75);
+}
+.faded strong {
+    color: #444;
+    color: rgba(0,0,0,0.6);
+}
 .faded-more {
     color: #aaa;
     color: rgba(0,0,0,0.4);
 }
+time[title]:hover {
+    text-decoration: underline;
+}
 
 .small[class^="icon-"],
 .small[class*=" icon-"] {
@@ -912,7 +923,7 @@ table.thread-entry th, #ticket_notes table th {
 }
 
 #response_options {
-    margin-top:30px;
+    margin-top:10px;
 }
 
 #response_options > form {
@@ -1403,7 +1414,7 @@ h2 > i.help-tip {
   background-color:#e9f5ff;
 }
 
-time {
+time.faq {
     display:inline-block;
     float:right;
     color:#777;
@@ -2217,3 +2228,17 @@ td.indented {
 .sticky.bar .content {
   margin: auto;
 }
+
+.thread-event {
+    padding: 15px 5px 5px;
+}
+.thread-event .type-icon {
+    border-radius: 8px;
+    background-color: #f4f4f4;
+    padding: 5px 8px;
+    margin-right: 5px;
+    text-align: center;
+    display: inline-block;
+    font-size: 1.2em;
+    border: 1px solid #eee;
+}
diff --git a/scp/tickets.php b/scp/tickets.php
index 4f0dcec4e3642c8628ed5bc0f743926d746becc2..e78104f0c5f294b8ef3eef02651c034b9918b1df 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -186,14 +186,6 @@ if($_POST && !$errors):
                          $errors['assignId']=__('Ticket already assigned to the team.');
                  }
 
-                 //Comments are not required on self-assignment (claim)
-                 if($claim && !$_POST['assign_comments'])
-                     $_POST['assign_comments'] = sprintf(__('Ticket claimed by %s'),$thisstaff->getName());
-                 elseif(!$_POST['assign_comments'])
-                     $errors['assign_comments'] = __('Assignment comments required');
-                 elseif(strlen($_POST['assign_comments'])<5)
-                         $errors['assign_comments'] = __('Comment too short');
-
                  if(!$errors && $ticket->assign($_POST['assignId'], $_POST['assign_comments'], !$claim)) {
                      if($claim) {
                          $msg = __('Ticket is NOW assigned to you!');