diff --git a/docs/manual/extending/extensions/hooks/index.rst b/docs/manual/extending/extensions/hooks/index.rst
index 06b7884b5c2b999895ad49d297d25eb39ff2da6b..271a1ae8cf8db8bdc6b96174a88e94d1c9294cb5 100644
--- a/docs/manual/extending/extensions/hooks/index.rst
+++ b/docs/manual/extending/extensions/hooks/index.rst
@@ -36,5 +36,6 @@ The following hooks are available for use by extensions.
    signal-hook
    template-hook
    url-hook
+   user-infobox-hook
    user-page-sidebar-items-hook
    webapi-capabilities-hook
diff --git a/docs/manual/extending/extensions/hooks/user-infobox-hook.rst b/docs/manual/extending/extensions/hooks/user-infobox-hook.rst
new file mode 100644
index 0000000000000000000000000000000000000000..7bc19e8d46ddbef3b8c627b659addb239797c592
--- /dev/null
+++ b/docs/manual/extending/extensions/hooks/user-infobox-hook.rst
@@ -0,0 +1,50 @@
+.. _user-infobox-hook:
+
+===============
+UserInfoBoxHook
+===============
+
+A :py:class:`~reviewboard.extensions.hooks.UserInfoBoxHook` can be used to add
+custom information to the infobox which appears when the cursor hovers over a
+username. The information falls under two categories: ``user-identity`` and
+``extra-details``. Each instance of this hook adds a single item of information
+and as many hooks and be created as needed.
+
+To use the hook, simply instantiate it with the appropriate category and a
+template name. For example::
+
+.. code-block:: python
+
+   from reviewboard.extensions.base import Extension
+   from reviewboard.extensions.hooks import UserInfoBoxHook
+
+   class SampleInfoBoxExtension(UserInfoBoxHook):
+       def __init__(self, section, template_name):
+           self.section = section
+           self.template_name = template_name
+
+By default, this renders the provided template with the request and user as
+context. However, should custom rendering context be desired, the function
+``get_extra_context`` can be overriden to return a rst :py:class:`dict`.
+The result of this function will be used, in addition to the request and user,
+for the template's rendering context. ``get_etag_data`` should also be
+overriden to return data that will be used to generate the entity tag for the
+infobox. This will help to ensure the infobox is cached correctly.
+
+Example
+=======
+
+.. code-block:: python
+
+   from reviewboard.extensions.base import Extension
+   from reviewboard.extensions.hooks import UserInfoBoxHook
+
+   class SampleInfoBoxExtension(UserInfoBoxHook):
+
+       # ...
+
+       def get_extra_context(self, user, request):
+           return {'foo': 'bar'}
+
+       def get_etag_data(self):
+           return 'foobar'
diff --git a/reviewboard/extensions/hooks.py b/reviewboard/extensions/hooks.py
index b337c268013215c01ac32e96c8799824ccc77124..22bc2f1454517e127b9b3739d70128adc12166f4 100644
--- a/reviewboard/extensions/hooks.py
+++ b/reviewboard/extensions/hooks.py
@@ -1,6 +1,9 @@
 from __future__ import unicode_literals
 
+from django.template.context import RequestContext
+from django.template.loader import render_to_string
 from django.utils import six
+from django.utils.safestring import mark_safe
 from djblets.extensions.hooks import (DataGridColumnsHook, ExtensionHook,
                                       ExtensionHookPoint, SignalHook,
                                       TemplateHook, URLHook)
@@ -263,6 +266,53 @@ class NavigationBarHook(ExtensionHook):
 
 
 @six.add_metaclass(ExtensionHookPoint)
+class UserInfoBoxHook(ExtensionHook):
+    """A hook for adding new information to the the pop-up user infobox.
+
+    By default, the hook constructor takes a section and a template name.
+    Currently,valid sections are one of:
+
+    * ``'user-identity'``
+    * ``'extra-details'``
+
+    Each instance of the hook provides one piece of information, and as many
+    instantiations can be made as needed. If required,
+    :py:meth:`get_extra_context` and :py:meth:`get_etag_data` may be overridden
+    to provide custom information for display and the associated etag
+    information respectively.
+    """
+
+    SECTIONS = ['user-identity', 'extra-details']
+
+    def __init__(self, extension, section, template_name):
+        super(UserInfoBoxHook, self).__init__(extension)
+
+        if section not in UserInfoBoxHook.SECTIONS:
+            raise ValueError(
+                'Invalid UserInfoBox section: %s; must be one of %s'
+                % (section, ', '.join(UserInfoBoxHook.SECTIONS)))
+
+        self.section = section
+        self.template_name = template_name
+
+    def get_etag_data(self):
+        return ''
+
+    def get_extra_context(self, user, request):
+        return {}
+
+    def render(self, user, request):
+        context = {
+            'user': user,
+        }
+        context.update(self.get_extra_context(user, request))
+
+        return mark_safe(
+            render_to_string(self.template_name,
+                             RequestContext(request, context)))
+
+
+@six.add_metaclass(ExtensionHookPoint)
 class ReviewRequestApprovalHook(ExtensionHook):
     """A hook for determining if a review request is approved.
 
@@ -910,6 +960,7 @@ __all__ = [
     'ReviewUIHook',
     'SignalHook',
     'TemplateHook',
+    'UserInfoBoxHook',
     'URLHook',
     'UserPageSidebarItemsHook',
     'WebAPICapabilitiesHook',
diff --git a/reviewboard/reviews/views.py b/reviewboard/reviews/views.py
index a540ffd9d19fef888cd1ffe3d8a229be22be0f44..fb7da3d6248e3cb5fd8884bd212b608e5e908e22 100644
--- a/reviewboard/reviews/views.py
+++ b/reviewboard/reviews/views.py
@@ -1,8 +1,10 @@
 from __future__ import unicode_literals
 
+import datetime
 import logging
 import time
 
+import pytz
 from django.conf import settings
 from django.contrib.auth.decorators import login_required
 from django.contrib.auth.models import User
@@ -1601,24 +1603,104 @@ def user_infobox(request, username,
     This is meant to be embedded in other pages, rather than being
     a standalone page.
     """
+    from reviewboard.extensions.hooks import UserInfoBoxHook
+
     user = get_object_or_404(User, username=username)
     show_profile = user.is_profile_visible(request.user)
 
-    etag = encode_etag(':'.join([
+    user_tz = pytz.timezone(request.user.get_profile().timezone)
+    target_user_tz = pytz.timezone(user.get_profile().timezone)
+
+    etag_data = [
         user.first_name,
         user.last_name,
         user.email,
         six.text_type(user.last_login),
         six.text_type(settings.TEMPLATE_SERIAL),
-        six.text_type(show_profile)
-    ]))
+        six.text_type(show_profile),
+        six.text_type(user_tz),
+        six.text_type(target_user_tz),
+    ]
+
+    for hook in UserInfoBoxHook.hooks:
+        etag_data.append(hook.get_etag_data())
+
+    etag = encode_etag(':'.join(etag_data))
 
     if etag_if_none_match(request, etag):
         return HttpResponseNotModified()
 
+    utc_now = datetime.datetime.now(pytz.utc)
+
+    target_user_dt = utc_now.astimezone(target_user_tz).replace(tzinfo=utc)
+
+    target_user_tz = target_user_dt.strftime("%Z")
+    target_user_lt = target_user_dt.strftime("%H:%M")
+
+    user_dt = utc_now.astimezone(user_tz).replace(tzinfo=utc)
+
+    # Get the timedelta between the two datetimes, in seconds.
+    duration = target_user_dt - user_dt
+    hr_diff = six.text_type(duration).rsplit(':', 1)[0]
+
+    # Generates the message showing the hour difference in the UI.
+    if duration == datetime.timedelta(0):
+        td_msg = _('Same local time as you (%s)') % target_user_tz
+    elif duration.days >= 0:
+        td_msg = (
+            _('%(lt)s local time: %(hours)s ahead')
+            % {
+                'lt': target_user_lt,
+                'hours': hr_diff,
+            })
+    else:
+        td_msg = (
+            _('%(lt)s local time: %(hours)s behind')
+            % {
+                'lt': target_user_lt,
+                'hours': -1 * hr_diff,
+            })
+
+    review_requests_sent = (
+        ReviewRequest.objects
+        .public(user=request.user)
+        .filter(submitter=user)
+        .count()
+    )
+
+    assigned = ReviewRequest.objects.to_user(user).count()
+    posted = review_requests_sent
+
+    # Get the custom text from the extension hook, if any.
+    user_identity_parts = []
+    extra_details_parts = []
+
+    for hook in UserInfoBoxHook.hooks:
+        try:
+            if hook.section == 'user-identity':
+                user_identity_parts.append(hook.render(user, request))
+            elif hook.section == 'extra-details':
+                extra_details_parts.append(hook.render(user, request))
+
+        except Exception:
+            extension = hook.extension
+            logging.exception('Error when running UserInfoBoxHook.render in '
+                              'extension %s',
+                              extension.id)
+
+    custom_user_identity = ''.join(user_identity_parts)
+    custom_extra_details = ''.join(user_identity_parts)
+
     response = render_to_response(template_name, RequestContext(request, {
+        'custom_extra_details': custom_extra_details,
+        'custom_user_identity': custom_user_identity,
         'show_profile': show_profile,
         'requested_user': user,
+        'time_diff': td_msg,
+        'users_stats': {
+            'assigned': assigned,
+            'posted': posted,
+        },
     }))
     set_etag(response, etag)
 
@@ -1723,4 +1805,4 @@ def download_orig_file(*args, **kwargs):
 @check_local_site_access
 def download_modified_file(*args, **kwargs):
     """Downloads a modified file from a diff."""
-    return _download_diff_file(True, *args, **kwargs)
+    return _download_diff_file(True, *args, **kwargs)
\ No newline at end of file
diff --git a/reviewboard/static/rb/css/common.less b/reviewboard/static/rb/css/common.less
index 1d4d4cb3194a41d798127da568588b3b9b2cc3f6..85e84890ed1bcffa17ce11c860f5b9ebfd2eb2f3 100644
--- a/reviewboard/static/rb/css/common.less
+++ b/reviewboard/static/rb/css/common.less
@@ -485,7 +485,7 @@
     margin-top: 2em;
   }
 
-  .logged-in, .joined {
+  .logged-in, .joined, .timeinfo, .user-stats, .custom-info {
     font-size: 0.8em;
   }
 
diff --git a/reviewboard/templates/accounts/user_infobox.html b/reviewboard/templates/accounts/user_infobox.html
index 0f7090a5b5558a0f14f83f34018966db679e1983..710f7ddac9e740ab531c51b6a532321ac1f8aee1 100644
--- a/reviewboard/templates/accounts/user_infobox.html
+++ b/reviewboard/templates/accounts/user_infobox.html
@@ -1,4 +1,4 @@
-{% load gravatars %}
+{% load i18n rb_extensions gravatars %}
 
 <div class="infobox-content" class="vcard">
 {% if siteconfig_settings.integration_gravatars %}
@@ -13,8 +13,17 @@
 {% if request.user.is_authenticated %}
   <p class="email"><a href="mailto:{{requested_user.email}}">{{requested_user.email}}</a></p>
 {% endif %}
+{% if custom_user_identity %}
+  <p class="custom-info">{{custom_user_identity}}</p>
+{% endif %}
   <p class="logged-in">Last logged in {{requested_user.last_login|date:"F jS, Y"}}</p>
   <p class="joined">Joined {{requested_user.date_joined|date:"F jS, Y"}}</p>
+  <p class="timeinfo"><b><span class='fa fa-clock-o'></span> {{time_diff}}</b></p>
+  <p class="user-stats">{% trans "Review Requests Assigned: " %}{{users_stats.assigned}}</p>
+  <p class="user-stats">{% trans "Review Requests Posted: " %}{{users_stats.posted}}</p>
+{% if custom_extra_details %}
+  <p class="custom-info">{{custom_extra_details}}</p>
+{% endif %}
 {% endif %}
  </div>
-</div>
+</div>
\ No newline at end of file
