Added tab-based interface to userpage UI. Yes, it is plugin expansible, and yes, it breaks existing plugins that add code to the userpage (but that can be fixed with a "colspan=4")
authorDan
Mon, 28 Jul 2008 13:13:09 -0600
changeset 672 08a7875258b4
parent 671 267c9f93b51f
child 673 99c617146a34
Added tab-based interface to userpage UI. Yes, it is plugin expansible, and yes, it breaks existing plugins that add code to the userpage (but that can be fixed with a "colspan=4")
cron.php
includes/clientside/css/enano-shared.css
includes/clientside/jsres.php
includes/clientside/static/ajax.js
includes/clientside/static/editor.js
includes/clientside/static/functions.js
includes/clientside/static/userpage.js
includes/pageprocess.php
language/english/core.json
language/english/user.json
themes/oxygen/css/bleu.css
themes/oxygen/images/buttonbg-lite.gif
--- a/cron.php	Mon Jul 28 13:10:22 2008 -0600
+++ b/cron.php	Mon Jul 28 13:13:09 2008 -0600
@@ -45,7 +45,7 @@
 $expiry_date = date('r', get_cron_next_run());
 
 $etag = sha1($expiry_date);
-  
+
 if ( isset($_SERVER['HTTP_IF_NONE_MATCH']) )
 {
   if ( "\"$etag\"" == $_SERVER['HTTP_IF_NONE_MATCH'] )
--- a/includes/clientside/css/enano-shared.css	Mon Jul 28 13:10:22 2008 -0600
+++ b/includes/clientside/css/enano-shared.css	Mon Jul 28 13:13:09 2008 -0600
@@ -845,3 +845,55 @@
   background-repeat: no-repeat;
 }
 
+/*
+ * Userpage styles
+ * Note: The colors and such given here are very minimal. It's really best to copy the definitions to your
+ * theme's CSS, and remove all structure-related rules (margin, position, padding, etc.)
+ */
+ 
+div.userpage_wrap {
+  position: relative;
+  top: 4em;
+  margin-bottom: 4em;
+  border: 1px solid #a0a0a0;
+}
+
+ul.userpage_links {
+  position: absolute;
+  top: -3em;
+  padding-left: 10px;
+  list-style-type: none !important;
+  list-style-image: none !important;
+}
+
+ul.userpage_links li {
+  float: left;
+  margin-right: 5px;
+  padding: 0 7px;
+  line-height: 1.9em;
+  list-style-type: none !important;
+  list-style-image: none !important;
+  border-style: solid;
+  border-color: #808080;
+  border-width: 1px 1px 0 1px;
+}
+
+ul.userpage_links li.userpage_tab_active {
+  margin-top: -0.2em;
+  line-height: 2.1em;
+  border-width: 1px 1px 1px 1px;
+  border-bottom-color: #ffffff;
+  font-weight: bold;
+}
+
+ul.userpage_links li:hover {
+  border-width: 1px 1px 1px 1px;
+  border-bottom-color: #ffffff;
+  cursor: pointer;
+}
+
+div.userpage_block {
+  clear: both;
+  padding: 10px;
+}
+
--- a/includes/clientside/jsres.php	Mon Jul 28 13:10:22 2008 -0600
+++ b/includes/clientside/jsres.php	Mon Jul 28 13:13:09 2008 -0600
@@ -102,6 +102,7 @@
   'pwstrength.js',
   'flyin.js',
   'rank-manager.js',
+  'userpage.js',
   'template-compiler.js',
   'toolbar.js',
 );
--- a/includes/clientside/static/ajax.js	Mon Jul 28 13:10:22 2008 -0600
+++ b/includes/clientside/static/ajax.js	Mon Jul 28 13:13:09 2008 -0600
@@ -21,6 +21,12 @@
       document.getElementById('ajaxEditContainer').innerHTML = ajax.responseText;
       selectButtonMajor('article');
       unselectAllButtonsMinor();
+      // if we're on a userpage, call the onload function to rebuild the tabs
+      if ( typeof(userpage_onload) == 'function' )
+      {
+        window.userpage_blocks = [];
+        userpage_onload();
+      }
     }
   });
 }
--- a/includes/clientside/static/editor.js	Mon Jul 28 13:10:22 2008 -0600
+++ b/includes/clientside/static/editor.js	Mon Jul 28 13:13:09 2008 -0600
@@ -627,30 +627,38 @@
         {
           if ( response.is_draft )
           {
-            document.getElementById('ajaxEditArea').used_draft = true;
-            document.getElementById('ajaxEditArea').needReset = true;
-            var img = $dynano('ajax_edit_savedraft_btn').object.getElementsByTagName('img')[0];
-            var lbl = $dynano('ajax_edit_savedraft_btn').object.getElementsByTagName('span')[0];
-            if ( response.is_draft == 'delete' )
+            try
             {
-              img.src = scriptPath + '/images/editor/savedraft.gif';
-              lbl.innerHTML = $lang.get('editor_btn_savedraft');
-              
-              var dn = $dynano('ajax_edit_draft_notice').object;
-              if ( dn )
+              document.getElementById('ajaxEditArea').used_draft = true;
+              document.getElementById('ajaxEditArea').needReset = true;
+              var img = $dynano('ajax_edit_savedraft_btn').object.getElementsByTagName('img')[0];
+              var lbl = $dynano('ajax_edit_savedraft_btn').object.getElementsByTagName('span')[0];
+              if ( response.is_draft == 'delete' )
               {
-                dn.parentNode.removeChild(dn);
+                img.src = scriptPath + '/images/editor/savedraft.gif';
+                lbl.innerHTML = $lang.get('editor_btn_savedraft');
+                
+                var dn = $dynano('ajax_edit_draft_notice').object;
+                if ( dn )
+                {
+                  dn.parentNode.removeChild(dn);
+                }
+              }
+              else
+              {
+                img.src = scriptPath + '/images/mini-info.png';
+                var d = new Date();
+                var m = String(d.getMinutes());
+                if ( m.length < 2 )
+                  m = '0' + m;
+                var time = d.getHours() + ':' + m;
+                lbl.innerHTML = $lang.get('editor_msg_draft_saved', { time: time });
               }
             }
-            else
+            catch(e)
             {
-              img.src = scriptPath + '/images/mini-info.png';
-              var d = new Date();
-              var m = String(d.getMinutes());
-              if ( m.length < 2 )
-                m = '0' + m;
-              var time = d.getHours() + ':' + m;
-              lbl.innerHTML = $lang.get('editor_msg_draft_saved', { time: time });
+              console.warn('Exception thrown during save, error dump follows');
+              console.debug(e);
             }
           }
           else
@@ -670,6 +678,12 @@
                   
                   ajaxEditorDestroyModalWindow();
                   document.getElementById('ajaxEditContainer').innerHTML = '<div class="usermessage">' + $lang.get('editor_msg_saved') + '</div>' + ajax.responseText;
+                  // if we're on a userpage, call the onload function to rebuild the tabs
+                  if ( typeof(userpage_onload) == 'function' )
+                  {
+                    window.userpage_blocks = [];
+                    userpage_onload();
+                  }
                   opacity('ajaxEditContainer', 0, 100, 1000);
                 }
               });
--- a/includes/clientside/static/functions.js	Mon Jul 28 13:10:22 2008 -0600
+++ b/includes/clientside/static/functions.js	Mon Jul 28 13:13:09 2008 -0600
@@ -464,6 +464,11 @@
   return position;
 }
 
+function setScrollOffset(offset)
+{
+  window.scroll(0, offset);
+}
+
 // Function to fade classes info-box, warning-box, error-box, etc.
 
 function fadeInfoBoxes()
@@ -568,7 +573,8 @@
   
   blackout.style.backgroundColor = '#FFFFFF';
   domObjChangeOpac(60, blackout);
-  blackout.style.backgroundImage = 'url(' + scriptPath + '/includes/clientside/tinymce/themes/advanced/skins/default/img/progress.gif)';
+  var background = ( $(el).Height() < 48 ) ? 'url(' + scriptPath + '/images/loading.gif)' : 'url(' + scriptPath + '/includes/clientside/tinymce/themes/advanced/skins/default/img/progress.gif)';
+  blackout.style.backgroundImage = background;
   blackout.style.backgroundPosition = 'center center';
   blackout.style.backgroundRepeat = 'no-repeat';
   blackout.style.zIndex = getHighestZ() + 2;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/includes/clientside/static/userpage.js	Mon Jul 28 13:13:09 2008 -0600
@@ -0,0 +1,119 @@
+// Tabs on userpage
+
+var userpage_blocks = [];
+
+var userpage_onload = function()
+{
+  var wrapper = document.getElementById('userpage_wrap');
+  var links = document.getElementById('userpage_links');
+  
+  wrapper.className = 'userpage_wrap';
+  links.className = 'userpage_links';
+  
+  var blocks = wrapper.getElementsByTagName('div');
+  var first_block = false;
+  for ( var i = 0; i < blocks.length; i++ )
+  {
+    var block = blocks[i];
+    if ( /^tab:/.test(block.id) )
+    {
+      $(block).addClass('userpage_block');
+      var block_id = block.id.substr(4);
+      userpage_blocks.push(block_id);
+      if ( !first_block )
+      {
+        // this is the first block on the page, memorize it
+        first_block = block_id;
+      }
+    }
+  }
+  // init links
+  var as = links.getElementsByTagName('a');
+  for ( var i = 0; i < as.length; i++ )
+  {
+    var a = as[i];
+    if ( a.href.indexOf('#') > -1 )
+    {
+      var hash = a.href.substr(a.href.indexOf('#'));
+      var blockid = hash.substr(5);
+      a.blockid = blockid;
+      a.onclick = function()
+      {
+        userpage_select_block(this.blockid);
+        return false;
+      }
+      a.id = 'userpage_blocklink_' + blockid;
+    }
+  }
+  if ( $_REQUEST['tab'] )
+  {
+    userpage_select_block($_REQUEST['tab'], true);
+  }
+  else
+  {
+    userpage_select_block(first_block, true);
+  }
+}
+
+addOnloadHook(userpage_onload);
+
+/**
+ * Select (show) the specified block on the userpage.
+ * @param string block name
+ * @param bool If true, omits transition effects.
+ */
+
+function userpage_select_block(block, nofade)
+{
+  // memorize existing scroll position, reset the hash, then scroll back to where we were
+  // a little hackish and might cause a flash, but it's better than hiding the tabs on each click
+  var currentScroll = getScrollOffset();
+  
+  var current_block = false;
+  nofade = true;
+  for ( var i = 0; i < userpage_blocks.length; i++ )
+  {
+    var div = document.getElementById('tab:' + userpage_blocks[i]);
+    if ( div )
+    {
+      if ( div.style.display != 'none' )
+      {
+        current_block = userpage_blocks[i];
+        if ( nofade || aclDisableTransitionFX )
+        {
+          div.style.display = 'none';
+        }
+      }
+    }
+    var a = document.getElementById('userpage_blocklink_' + userpage_blocks[i]);
+    if ( a )
+    {
+      if ( $(a.parentNode).hasClass('userpage_tab_active') )
+      {
+        $(a.parentNode).rmClass('userpage_tab_active');
+      }
+    }
+  }
+  if ( nofade || !current_block || aclDisableTransitionFX )
+  {
+    var div = document.getElementById('tab:' + block);
+    div.style.display = 'block';
+  }
+  /*
+  else
+  {
+    // do this in a slightly fancier fashion
+    load_component('SpryEffects');
+    (new Spry.Effect.Blind('tab:' + current_block, { from: '100%', to: '0%', finish: function()
+        {
+          (new Spry.Effect.Blind('tab:' + block, { from: '0%', to: '100%' })).start();
+        }
+      })).start();
+  }
+  */
+  var a = document.getElementById('userpage_blocklink_' + block);
+  $(a.parentNode).addClass('userpage_tab_active');
+  
+  window.location.hash = 'tab:' + block;
+  setScrollOffset(currentScroll);
+}
--- a/includes/pageprocess.php	Mon Jul 28 13:10:22 2008 -0600
+++ b/includes/pageprocess.php	Mon Jul 28 13:13:09 2008 -0600
@@ -1300,6 +1300,42 @@
     global $email;
     global $lang;
     
+    /**
+     * PLUGGING INTO USER PAGES
+     * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+     * Userpages are highly programmable and extendable using a number of
+     * hooks. These hooks are:
+     *
+     *   - userpage_sidebar_left
+     *   - userpage_sidebar_right
+     *   - userpage_tabs_links
+     *   - userpage_tabs_body
+     *
+     * You can add a variety of sections to user pages, including new tabs
+     * and new sections on the tables. To add a tab, attach to
+     * userpage_tabs_links and echo out:
+     *
+     *   <li><a href="#tab:YOURTABID">YOUR TAB TEXT</a></li>
+     *
+     * Then hook into userpage_tabs_body and echo out:
+     *
+     *   <div id="tab:YOURTABID">YOUR TAB CONTENT</div>
+     *
+     * The userpage javascript runtime will take care of everything else,
+     * meaning transitions, click events, etc. Currently it's not possible
+     * to add custom click events to tabs, but any DOM-related JS that needs
+     * to run in your tab can be run onload and the effects will be seen when
+     * your tab is clicked. YOURTABID should be lowercase alphanumeric and
+     * have a short prefix so as to assure that it remains specific to your
+     * plugin.
+     *
+     * To hook into the "profile" tab, use userpage_sidebar_{left,right}. Just
+     * echo out table cells as normal. The table on the left (the wide one) has
+     * four columns, and the one on the right has one column.
+     * 
+     * See plugins.php for a guide on creating and attaching to hooks.
+     */
+    
     $page_urlname = dirtify_page_id($this->page_id);
     if ( $this->page_id == $paths->page_id && $this->namespace == $paths->namespace )
     {
@@ -1322,7 +1358,7 @@
     
     if ( ( $page_name == str_replace('_', ' ', $this->page_id) || $page_name == $paths->nslist['User'] . str_replace('_', ' ', $this->page_id) ) || !$this->page_exists )
     {
-      $page_name = $lang->get('userpage_page_title', array('username' => htmlspecialchars($target_username)));
+      $page_name = $lang->get('userpage_page_title', array('username' => $target_username));
     }
     else
     {
@@ -1367,9 +1403,13 @@
     }
     else
     {
+      // get the rank data for the anonymous user (placeholder basically)
       $rank_data = $session->get_user_rank(1);
     }
     
+    // add the userpage script to the header
+    $template->add_header('<script type="text/javascript" src="' . cdnPath . '/includes/clientside/static/userpage.js"></script>');
+    
     $this->header();
     
     // if ( $send_headers )
@@ -1377,153 +1417,304 @@
     //  display_page_headers();
     // }
    
-    // Start left sidebar: basic user info, latest comments
-    
-    if ( $user_exists ):
-    
-    echo '<table border="0" cellspacing="4" cellpadding="0" style="width: 100%;">';
-    echo '<tr><td style="width: 150px;" valign="top">';
-    
-    echo '<div class="tblholder">
-            <table border="0" cellspacing="1" cellpadding="4">';
-    
     //
-    // Main part of sidebar
+    // BASIC INFORMATION
+    // Presentation of username/rank/avatar/basic info
     //
     
-    // Basic user info
-    
-    echo '<tr><th class="subhead">' . $lang->get('userpage_heading_basics', array('username' => htmlspecialchars($target_username))) . '</th></tr>';
-    
-    echo '<tr><td class="row1" style="text-align: center;">';
-    if ( $userdata['user_has_avatar'] == '1' )
-    {
-      echo '<img alt="' . $lang->get('usercp_avatar_image_alt', array('username' => $userdata['username'])) . '" src="' . make_avatar_url(intval($userdata['authoritative_uid']), $userdata['avatar_type'], $userdata['email']) . '" /><br />';
-    }
-    // username
-    echo '<big><span style="' . $rank_data['rank_style'] . '">' . htmlspecialchars($target_username) . '</span></big><br />';
-    // user title, if appropriate
-    if ( $rank_data['user_title'] )
-      echo htmlspecialchars($rank_data['user_title']) . '<br />';
-    // rank
-    echo htmlspecialchars($lang->get($rank_data['rank_title']));
-    echo '</td></tr>';
-    echo '<tr><td class="row3">' . $lang->get('userpage_lbl_joined') . ' ' . enano_date('F d, Y h:i a', $userdata['reg_time']) . '</td></tr>';
-    echo '<tr><td class="row1">' . $lang->get('userpage_lbl_num_comments') . ' ' . $userdata['n_comments'] . '</td></tr>';
-    
-    if ( !empty($userdata['real_name']) )
-    {
-      echo '<tr><td class="row3">' . $lang->get('userpage_lbl_real_name') . ' ' . $userdata['real_name'] . '</td></tr>';
-    }
-    
-    // Administer user button
-    
-    if ( $session->user_level >= USER_LEVEL_ADMIN )
+    if ( $user_exists )
     {
-      echo '<tr><td class="row1"><a href="' . makeUrlNS('Special', 'Administration', 'module=' . $paths->nslist['Admin'] . 'UserManager&src=get&user=' . urlencode($target_username), true) . '" onclick="ajaxAdminUser(\'' . addslashes($target_username) . '\'); return false;">' . $lang->get('userpage_btn_administer_user') . '</a></td></tr>';
-    }
-    
-    // Comments
-    
-    echo '<tr><th class="subhead">' . $lang->get('userpage_heading_comments', array('username' => htmlspecialchars($target_username))) . '</th></tr>';
-    $q = $db->sql_query('SELECT page_id, namespace, subject, time FROM '.table_prefix.'comments WHERE name=\'' . $db->escape($target_username) . '\' AND user_id=' . $userdata['authoritative_uid'] . ' AND approved=1 ORDER BY time DESC LIMIT 5;');
-    if ( !$q )
-      $db->_die();
-    
-    $comments = Array();
-    $no_comments = false;
-    
-    if ( $row = $db->fetchrow() )
-    {
-      do 
-      {
-        $row['time'] = enano_date('F d, Y', $row['time']);
-        $comments[] = $row;
-      }
-      while ( $row = $db->fetchrow() );
-    }
-    else
-    {
-      $no_comments = true;
-    }
-    
-    echo '<tr><td class="row3">';
-    echo '<div style="border: 1px solid #000000; padding: 0px; margin: 0; max-height: 200px; clip: rect(0px,auto,auto,0px); overflow: auto; background-color: transparent;" class="tblholder">';
-    
-    echo '<table border="0" cellspacing="1" cellpadding="4">';
-    $class = 'row1';
     
-    $tpl = '<tr>
-              <td class="{CLASS}">
-                <a href="{PAGE_LINK}" <!-- BEGINNOT page_exists -->class="wikilink-nonexistent"<!-- END page_exists -->>{PAGE}</a><br />
-                <small>{lang:userpage_comments_lbl_posted} {DATE}<br /></small>
-                <b><a href="{COMMENT_LINK}">{SUBJECT}</a></b>
+      ?>
+      <div id="userpage_wrap">
+        <ul id="userpage_links">
+          <li><a href="#tab:profile"><?php echo $lang->get('userpage_tab_profile'); ?></a></li>
+          <li><a href="#tab:content"><?php echo $lang->get('userpage_tab_content'); ?></a></li>
+          <?php
+          $code = $plugins->setHook('userpage_tabs_links');
+          foreach ( $code as $cmd )
+          {
+            eval($cmd);
+          }
+          ?>
+        </ul>
+        
+        <div id="tab:profile">
+      
+      <?php
+      
+      echo '<table border="0" cellspacing="0" cellpadding="0">
+              <tr>';
+                
+      echo '    <td valign="top">';
+      
+      echo '<div class="tblholder">
+              <table border="0" cellspacing="1" cellpadding="4">';
+              
+      // heading
+      echo '    <tr>
+                  <th colspan="' . ( $session->user_level >= USER_LEVEL_ADMIN ? '3' : '4' ) . '">
+                    ' . $lang->get('userpage_heading_basics', array('username' => htmlspecialchars($target_username))) . '
+                  </th>
+                  ' . (
+                    $session->user_level >= USER_LEVEL_ADMIN ?
+                    '<th class="subhead" style="width: 25%;"><a href="' . makeUrlNS('Special', 'Administration', 'module=' . $paths->nslist['Admin'] . 'UserManager&src=get&user=' . urlencode($target_username), true) . '" onclick="ajaxAdminUser(\'' . addslashes($target_username) . '\'); return false;">&raquo; ' . $lang->get('userpage_btn_administer_user') . '</a></th>'
+                      : ''
+                  ) . '
+                </tr>';
+                
+      // avi/rank/username
+      echo '    <tr>
+                  <td class="row3" colspan="4">
+                    ' . (
+                        $userdata['user_has_avatar'] == 1 ?
+                        '<div style="float: left; margin-right: 10px;">
+                          <img alt="' . $lang->get('usercp_avatar_image_alt', array('username' => $userdata['username'])) . '" src="' . make_avatar_url(intval($userdata['authoritative_uid']), $userdata['avatar_type'], $userdata['email']) . '" />
+                         </div>'
+                        : ''
+                      ) . '
+                      <span style="font-size: x-large; ' . $rank_data['rank_style'] . '">' . htmlspecialchars($userdata['username']) . '</span>
+                      ' . ( !empty($rank_data['user_title']) ? '<br />' . htmlspecialchars($rank_data['user_title']) : '' ) . '
+                      ' . ( !empty($rank_data['rank_title']) ? '<br />' . htmlspecialchars($lang->get($rank_data['rank_title'])) : '' ) . '
+                  </td>
+                </tr>';
+                
+      // join date & total comments
+      echo '<tr>';
+      echo '  <td class="row2" style="text-align: right; width: 25%;">
+                ' . $lang->get('userpage_lbl_joined') . '
               </td>
-            </tr>';
-    $parser = $template->makeParserText($tpl);
-    
-    if ( count($comments) > 0 )
-    {
-      foreach ( $comments as $comment )
+              <td class="row1" style="text-align: left; width: 25%;">
+                ' . enano_date('F d, Y h:i a', $userdata['reg_time']) . '
+              </td>';
+      echo '  <td class="row2" style="text-align: right; width: 25%;">
+                ' . $lang->get('userpage_lbl_num_comments') . '
+              </td>
+              <td class="row1" style="text-align: left; width: 25%;">
+                ' . $userdata['n_comments'] . '
+              </td>';
+      echo '</tr>';
+      
+      // real name
+      if ( !empty($userdata['real_name']) )
       {
-        $c_page_id = $paths->nslist[ $comment['namespace'] ] . sanitize_page_id($comment['page_id']);
-        if ( isset($paths->pages[ $c_page_id ]) )
+        echo '<tr>
+                <td class="row2" style="text-align: right;">
+                  ' . $lang->get('userpage_lbl_real_name') . '
+                </td>
+                <td class="row1" colspan="3" style="text-align: left;">
+                  ' . htmlspecialchars($userdata['real_name']) . '
+                </td>
+              </tr>';
+      }
+                
+      // latest comments
+      
+      echo '<tr><th class="subhead" colspan="4">' . $lang->get('userpage_heading_comments', array('username' => htmlspecialchars($target_username))) . '</th></tr>';
+      $q = $db->sql_query('SELECT page_id, namespace, subject, time FROM '.table_prefix.'comments WHERE name=\'' . $db->escape($target_username) . '\' AND user_id=' . $userdata['authoritative_uid'] . ' AND approved=1 ORDER BY time DESC LIMIT 7;');
+      if ( !$q )
+        $db->_die();
+      
+      $comments = Array();
+      $no_comments = false;
+      
+      if ( $row = $db->fetchrow() )
+      {
+        do 
         {
-          $parser->assign_bool(array(
-            'page_exists' => true
-            ));
-          $page_title = htmlspecialchars($paths->pages[ $c_page_id ]['name']);
-        }
-        else
-        {
-          $parser->assign_bool(array(
-            'page_exists' => false
-            ));
-          $page_title = htmlspecialchars(dirtify_page_id($c_page_id));
+          $row['time'] = enano_date('F d, Y', $row['time']);
+          $comments[] = $row;
         }
-        $parser->assign_vars(array(
-            'CLASS' => $class,
-            'PAGE_LINK' => makeUrlNS($comment['namespace'], sanitize_page_id($comment['page_id'])),
-            'PAGE' => $page_title,
-            'SUBJECT' => $comment['subject'],
-            'DATE' => $comment['time'],
-            'COMMENT_LINK' => makeUrlNS($comment['namespace'], sanitize_page_id($comment['page_id']), 'do=comments', true)
-          ));
-        $class = ( $class == 'row3' ) ? 'row1' : 'row3';
-        echo $parser->run();
+        while ( $row = $db->fetchrow() );
+      }
+      else
+      {
+        $no_comments = true;
+      }
+      
+      echo '<tr><td class="row3" colspan="4">';
+      echo '<div style="border: 1px solid #000000; padding: 0px; width: 100%; clip: rect(0px,auto,auto,0px); overflow: auto; background-color: transparent;" class="tblholder">';
+      
+      echo '<table border="0" cellspacing="1" cellpadding="4" style="width: 200%;"><tr>';
+      $class = 'row1';
+      
+      $tpl = '  <td class="{CLASS}">
+                  <a href="{PAGE_LINK}" <!-- BEGINNOT page_exists -->class="wikilink-nonexistent"<!-- END page_exists -->>{PAGE}</a><br />
+                  <small>{lang:userpage_comments_lbl_posted} {DATE}<br /></small>
+                  <b><a href="{COMMENT_LINK}">{SUBJECT}</a></b>
+                </td>';
+      $parser = $template->makeParserText($tpl);
+      
+      if ( count($comments) > 0 )
+      {
+        foreach ( $comments as $comment )
+        {
+          $c_page_id = $paths->nslist[ $comment['namespace'] ] . sanitize_page_id($comment['page_id']);
+          if ( isset($paths->pages[ $c_page_id ]) )
+          {
+            $parser->assign_bool(array(
+              'page_exists' => true
+              ));
+            $page_title = htmlspecialchars($paths->pages[ $c_page_id ]['name']);
+          }
+          else
+          {
+            $parser->assign_bool(array(
+              'page_exists' => false
+              ));
+            $page_title = htmlspecialchars(dirtify_page_id($c_page_id));
+          }
+          $parser->assign_vars(array(
+              'CLASS' => $class,
+              'PAGE_LINK' => makeUrlNS($comment['namespace'], sanitize_page_id($comment['page_id'])),
+              'PAGE' => $page_title,
+              'SUBJECT' => $comment['subject'],
+              'DATE' => $comment['time'],
+              'COMMENT_LINK' => makeUrlNS($comment['namespace'], sanitize_page_id($comment['page_id']), 'do=comments', true)
+            ));
+          $class = ( $class == 'row3' ) ? 'row1' : 'row3';
+          echo $parser->run();
+        }
+      }
+      else
+      {
+        echo '<td class="' . $class . '">' . $lang->get('userpage_msg_no_comments') . '</td>';
+      }
+      echo '</tr></table>';
+      
+      echo '</div>';
+      echo '</td></tr>';
+      
+      $code = $plugins->setHook('userpage_sidebar_left');
+      foreach ( $code as $cmd )
+      {
+        eval($cmd);
       }
-    }
-    else
-    {
-      echo '<tr><td class="' . $class . '">' . $lang->get('userpage_msg_no_comments') . '</td></tr>';
-    }
-    echo '</table>';
+              
+      echo '  </table>
+            </div>';
+            
+      echo '</td>';
+      
+      //
+      // CONTACT INFORMATION
+      //
+      
+      echo '    <td valign="top" style="width: 150px; padding-left: 10px;">';
+      
+      echo '<div class="tblholder">
+              <table border="0" cellspacing="1" cellpadding="4">';
+      
+      //
+      // Main part of sidebar
+      //
+      
+      // Contact information
+      
+      echo '<tr><th class="subhead">' . $lang->get('userpage_heading_contact') . '</th></tr>';
+      
+      $class = 'row3';
+      
+      if ( $userdata['email_public'] == 1 )
+      {
+        $class = ( $class == 'row1' ) ? 'row3' : 'row1';
+        $email_link = $email->encryptEmail($userdata['email']);
+        echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_email') . ' ' . $email_link . '</td></tr>';
+      }
+      
+      $class = ( $class == 'row1' ) ? 'row3' : 'row1';
+      if ( $session->user_logged_in )
+      {
+        echo '<tr><td class="'.$class.'">' . $lang->get('userpage_btn_send_pm', array('username' => htmlspecialchars($target_username), 'pm_link' => makeUrlNS('Special', 'PrivateMessages/Compose/to/' . $this->page_id, false, true))) . '</td></tr>';
+      }
+      else
+      {
+        echo '<tr><td class="'.$class.'">' . $lang->get('userpage_btn_send_pm_guest', array('username' => htmlspecialchars($target_username), 'login_flags' => 'href="' . makeUrlNS('Special', 'Login/' . $paths->nslist[$this->namespace] . $this->page_id) . '" onclick="ajaxStartLogin(); return false;"')) . '</td></tr>';
+      }
+      
+      if ( !empty($userdata['user_aim']) )
+      {
+        $class = ( $class == 'row1' ) ? 'row3' : 'row1';
+        echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_aim') . ' ' . $userdata['user_aim'] . '</td></tr>';
+      }
+      
+      if ( !empty($userdata['user_yahoo']) )
+      {
+        $class = ( $class == 'row1' ) ? 'row3' : 'row1';
+        echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_yim') . ' ' . $userdata['user_yahoo'] . '</td></tr>';
+      }
+      
+      if ( !empty($userdata['user_msn']) )
+      {
+        $class = ( $class == 'row1' ) ? 'row3' : 'row1';
+        $email_link = $email->encryptEmail($userdata['user_msn']);
+        echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_wlm') . ' ' . $email_link . '</td></tr>';
+      }
+      
+      if ( !empty($userdata['user_xmpp']) )
+      {
+        $class = ( $class == 'row1' ) ? 'row3' : 'row1';
+        $email_link = $email->encryptEmail($userdata['user_xmpp']);
+        echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_xmpp') . ' ' . $email_link . '</td></tr>';
+      }
+      
+      // Real life
+      
+      echo '<tr><th class="subhead">' . $lang->get('userpage_heading_real_life', array('username' => htmlspecialchars($target_username))) . '</th></tr>';
+      
+      if ( !empty($userdata['user_location']) )
+      {
+        $class = ( $class == 'row1' ) ? 'row3' : 'row1';
+        echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_location') . ' ' . $userdata['user_location'] . '</td></tr>';
+      }
+      
+      if ( !empty($userdata['user_job']) )
+      {
+        $class = ( $class == 'row1' ) ? 'row3' : 'row1';
+        echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_job') . ' ' . $userdata['user_job'] . '</td></tr>';
+      }
+      
+      if ( !empty($userdata['user_hobbies']) )
+      {
+        $class = ( $class == 'row1' ) ? 'row3' : 'row1';
+        echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_hobbies') . ' ' . $userdata['user_hobbies'] . '</td></tr>';
+      }
+      
+      if ( empty($userdata['user_location']) && empty($userdata['user_job']) && empty($userdata['user_hobbies']) )
+      {
+        $class = ( $class == 'row1' ) ? 'row3' : 'row1';
+        echo '<tr><td class="'.$class.'">' . $lang->get('userpage_msg_no_contact_info', array('username' => htmlspecialchars($target_username))) . '</td></tr>';
+      }
+      
+      $code = $plugins->setHook('userpage_sidebar_right');
+      foreach ( $code as $cmd )
+      {
+        eval($cmd);
+      }
+      
+      echo '  </table>
+            </div>';
+      echo '</td>';
+      
+      //
+      // End of profile
+      //
+      
+      echo '</tr></table>';
+      
+      echo '</div>'; // tab:profile
     
-    echo '</div>';
-    echo '</td></tr>';
-    
-    $code = $plugins->setHook('userpage_sidebar_left');
-    foreach ( $code as $cmd )
-    {
-      eval($cmd);
     }
     
-    echo '  </table>
-          </div>';
-    
-    echo '</td><td valign="top" style="padding: 0 10px;">';
-    
-    else:
-    
-    // Nothing for now
-    
-    endif;
-    
     // User's own content
     
     $send_headers = $this->send_headers;
     $this->send_headers = false;
     
+    echo '<span class="menuclear"></span>';
+    
+    echo '<div id="tab:content">';
+    
     if ( $this->page_exists )
     {
       $this->render();
@@ -1533,116 +1724,26 @@
       $this->err_page_not_existent(true);
     }
     
-    // Right sidebar
-    
-    if ( $user_exists ):
-    
-    echo '</td><td style="width: 150px;" valign="top">';
-    
-    echo '<div class="tblholder">
-            <table border="0" cellspacing="1" cellpadding="4">';
-    
-    //
-    // Main part of sidebar
-    //
-    
-    // Contact information
-    
-    echo '<tr><th class="subhead">' . $lang->get('userpage_heading_contact') . '</th></tr>';
-    
-    $class = 'row3';
-    
-    if ( $userdata['email_public'] == 1 )
-    {
-      $class = ( $class == 'row1' ) ? 'row3' : 'row1';
-      $email_link = $email->encryptEmail($userdata['email']);
-      echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_email') . ' ' . $email_link . '</td></tr>';
-    }
-    
-    $class = ( $class == 'row1' ) ? 'row3' : 'row1';
-    if ( $session->user_logged_in )
-    {
-      echo '<tr><td class="'.$class.'">' . $lang->get('userpage_btn_send_pm', array('username' => htmlspecialchars($target_username), 'pm_link' => makeUrlNS('Special', 'PrivateMessages/Compose/to/' . $this->page_id, false, true))) . '</td></tr>';
-    }
-    else
-    {
-      echo '<tr><td class="'.$class.'">' . $lang->get('userpage_btn_send_pm_guest', array('username' => htmlspecialchars($target_username), 'login_flags' => 'href="' . makeUrlNS('Special', 'Login/' . $paths->nslist[$this->namespace] . $this->page_id) . '" onclick="ajaxStartLogin(); return false;"')) . '</td></tr>';
-    }
-    
-    if ( !empty($userdata['user_aim']) )
-    {
-      $class = ( $class == 'row1' ) ? 'row3' : 'row1';
-      echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_aim') . ' ' . $userdata['user_aim'] . '</td></tr>';
-    }
+    echo '</div>'; // tab:content
     
-    if ( !empty($userdata['user_yahoo']) )
-    {
-      $class = ( $class == 'row1' ) ? 'row3' : 'row1';
-      echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_yim') . ' ' . $userdata['user_yahoo'] . '</td></tr>';
-    }
-    
-    if ( !empty($userdata['user_msn']) )
-    {
-      $class = ( $class == 'row1' ) ? 'row3' : 'row1';
-      $email_link = $email->encryptEmail($userdata['user_msn']);
-      echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_wlm') . ' ' . $email_link . '</td></tr>';
-    }
-    
-    if ( !empty($userdata['user_xmpp']) )
-    {
-      $class = ( $class == 'row1' ) ? 'row3' : 'row1';
-      $email_link = $email->encryptEmail($userdata['user_xmpp']);
-      echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_xmpp') . ' ' . $email_link . '</td></tr>';
-    }
-    
-    // Real life
-    
-    echo '<tr><th class="subhead">' . $lang->get('userpage_heading_real_life', array('username' => htmlspecialchars($target_username))) . '</th></tr>';
-    
-    if ( !empty($userdata['user_location']) )
-    {
-      $class = ( $class == 'row1' ) ? 'row3' : 'row1';
-      echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_location') . ' ' . $userdata['user_location'] . '</td></tr>';
-    }
-    
-    if ( !empty($userdata['user_job']) )
-    {
-      $class = ( $class == 'row1' ) ? 'row3' : 'row1';
-      echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_job') . ' ' . $userdata['user_job'] . '</td></tr>';
-    }
-    
-    if ( !empty($userdata['user_hobbies']) )
-    {
-      $class = ( $class == 'row1' ) ? 'row3' : 'row1';
-      echo '<tr><td class="'.$class.'">' . $lang->get('userpage_lbl_hobbies') . ' ' . $userdata['user_hobbies'] . '</td></tr>';
-    }
-    
-    if ( empty($userdata['user_location']) && empty($userdata['user_job']) && empty($userdata['user_hobbies']) )
-    {
-      $class = ( $class == 'row1' ) ? 'row3' : 'row1';
-      echo '<tr><td class="'.$class.'">' . $lang->get('userpage_msg_no_contact_info', array('username' => htmlspecialchars($target_username))) . '</td></tr>';
-    }
-    
-    $code = $plugins->setHook('userpage_sidebar_right');
+    $code = $plugins->setHook('userpage_tabs_body');
     foreach ( $code as $cmd )
     {
       eval($cmd);
     }
     
-    echo '  </table>
-          </div>';
-          
-    echo '</tr></table>';
-    
-    else:
-    
-    if ( !is_valid_ip($target_username) )
+    if ( $user_exists )
+    {
+      echo '</div>'; // userpage_wrap
+    }
+    else
     {
-      echo '<p>' . $lang->get('userpage_msg_user_not_exist', array('username' => htmlspecialchars($target_username))) . '</p>';
+      if ( !is_valid_ip($target_username) )
+      {
+        echo '<p>' . $lang->get('userpage_msg_user_not_exist', array('username' => htmlspecialchars($target_username))) . '</p>';
+      }
     }
     
-    endif;
-    
     // if ( $send_headers )
     // {
     //  display_page_footers();
@@ -1823,7 +1924,7 @@
     global $db, $session, $paths, $template, $plugins; // Common objects
     global $lang;
     
-    header('HTTP/1.1 404 Not Found');
+    @header('HTTP/1.1 404 Not Found');
     
     $this->header();
     $this->do_breadcrumbs();
@@ -1838,7 +1939,7 @@
     {
       if ( $userpage )
       {
-        echo '<h3>' . $lang->get('page_msg_404_title') . '</h3>
+        echo '<h3>' . $lang->get('page_msg_404_title_userpage') . '</h3>
                <p>' . $lang->get('page_msg_404_body_userpage');
       }
       else
--- a/language/english/core.json	Mon Jul 28 13:10:22 2008 -0600
+++ b/language/english/core.json	Mon Jul 28 13:13:09 2008 -0600
@@ -156,8 +156,9 @@
       lbl_password: 'Password:',
       
       msg_404_title: 'There is no page with this title yet.',
-      msg_404_body_userpage: 'This user has not created his or her user page yet.',
       msg_404_body: 'You have requested a page that doesn\'t exist yet.',
+      msg_404_title_userpage: 'No content',
+      msg_404_body_userpage: 'This user has not yet created any custom user page content.',
       msg_404_create: 'You can <a %create_flags%>create this page</a>, or return to the <a href="%mainpage_link%">homepage</a>.',
       msg_404_gohome: 'Return to the <a href="%mainpage_link%">homepage</a>.',
       msg_404_was_deleted: '<b>This page was deleted on %delete_time%.</b> The stated reason was:</p><blockquote>%delete_reason%</blockquote><p>You can probably <a %rollback_flags%>roll back</a> the deletion.',
--- a/language/english/user.json	Mon Jul 28 13:10:22 2008 -0600
+++ b/language/english/user.json	Mon Jul 28 13:13:09 2008 -0600
@@ -128,6 +128,7 @@
       
       msg_elev_timed_out: '<b>Your administrative session has timed out.</b> <a href="%login_link%" onclick="ajaxLogonToElev(); return false;">Log in again</a>',
       
+      reg_err_locked_out: 'Registration is disabled because you are currently locked out from logging in. Please wait %time% minutes before attempting to register again.',
       reg_err_captcha: 'The confirmation code you entered was incorrect.',
       reg_err_disabled_title: 'Registration disabled',
       reg_err_disabled_body: 'The administrator has disabled the registration of new accounts on this site.',
@@ -620,9 +621,9 @@
       ml_msg_matches: 'Search returned %matches% matches',
     },
     userpage: {
-      page_title: '%username%\'s user page',
+      page_title: '%username|htmlsafe%\'s user page',
       heading_basics: 'All about %username%',
-      lbl_joined: 'Joined:',
+      lbl_joined: 'Member since:',
       lbl_num_comments: 'Total comments:',
       lbl_real_name: 'Real name:',
       btn_administer_user: 'Administer user',
@@ -643,6 +644,8 @@
       lbl_hobbies: 'Enjoys:',
       msg_no_contact_info: '%username% hasn\'t posted any real-life contact information.',
       msg_user_not_exist: 'Additional information: user "%username%" does not exist.',
+      tab_profile: 'Profile',
+      tab_content: 'User page',
     }
   }
 };
--- a/themes/oxygen/css/bleu.css	Mon Jul 28 13:10:22 2008 -0600
+++ b/themes/oxygen/css/bleu.css	Mon Jul 28 13:13:09 2008 -0600
@@ -776,3 +776,29 @@
   padding-right: 5px;
 }
 
+/*
+ * Userpage styles
+ */
+
+ul.userpage_links li {
+  background-image: url('../images/buttonbg.gif');
+  background-repeat: repeat-x;
+}
+
+ul.userpage_links li a {
+  color: #202020;
+}
+
+ul.userpage_links li.userpage_tab_active {
+  background-image: url('../images/buttonbg-lite.gif');
+}
+
+ul.userpage_links li:hover {
+  background-image: url('../images/buttonbg-lite.gif');
+  border-color: #404040 #404040 #ffffff #404040;
+  border-bottom-width: 0;
+}
+
+ul.userpage_links li.userpage_tab_active:hover {
+  border-bottom-width: 1px;
+}
Binary file themes/oxygen/images/buttonbg-lite.gif has changed