Index: privatemsg.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/privatemsg.module,v
retrieving revision 1.70.2.30.2.91.2.64.2.31
diff -u -p -r1.70.2.30.2.91.2.64.2.31 privatemsg.module
--- privatemsg.module	19 Nov 2009 22:28:26 -0000	1.70.2.30.2.91.2.64.2.31
+++ privatemsg.module	23 Nov 2009 23:09:37 -0000
@@ -470,11 +470,6 @@ function privatemsg_theme() {
       'variables'        => array('recipients' => NULL),
       'template'         => 'privatemsg-between',
     ),
-    'privatemsg_list'    => array(
-      'file'                  => 'privatemsg.theme.inc',
-      'path'                  => drupal_get_path('module', 'privatemsg'),
-      'render element'        => 'form',
-    ),
     // Define pattern for header/field templates. The theme system will register all
     // theme functions that start with the defined pattern.
     'privatemsg_list_header'  => array(
@@ -568,33 +563,36 @@ function privatemsg_list($form, &$form_s
   }
   // By this point we have figured out for which user we are listing messages and now it is safe to use $account->uid in the listing query.
 
-  $query = _privatemsg_assemble_query('list', $account, $argument);
+  drupal_add_css(drupal_get_path('module', 'privatemsg') .'/styles/privatemsg-list.css');
+
+  // Load the table columns.
+  $columns = array_merge(array('subject', 'last_updated'), array_filter(variable_get('privatemsg_display_fields', array('participants'))));
 
-  $threads = array();
-  $form['#data'] = array();
+  // Load the themed list headers based on the available data.
+  $headers = _privatemsg_list_headers($columns);
+  uasort($headers, 'element_sort');
+
+  $form['list'] = array(
+    '#type' => 'tableselect',
+    '#header' => $headers,
+    '#options' => array(),
+    '#attributes' => array('class' => array('privatemsg-list')),
+    '#empty' => t('No messages available.'),
+    '#weight' => 5,
+    '#pre_render' => array('_privatemsg_list_thread'),
+  );
+
+  $query = _privatemsg_assemble_query('list', $account, $argument);
   foreach ($query->execute() as $row) {
     // Store the raw row data.
-    $form['#data'][$row->thread_id] = (array)$row;
-    // Store the themed row data.
-    $form['#rows'][$row->thread_id] = _privatemsg_list_thread((array)$row);
-    // store thread id for the checkboxes array
-    $threads[$row->thread_id] = '';
-  }
-  if (empty($form['#data'])) {
-    // If no threads are displayed, use these default columns.
-    $keys = array('subject', 'author', 'last_updated');
+    $form['list']['#options'][$row->thread_id] = (array)$row;
   }
-  else {
-    // Load the keys of the first row in data, we don't know the key
-    $keys = array_keys($form['#data'][key($form['#data'])]);
+
+  if (!empty($form['list']['#options'])) {
     $form['actions'] = _privatemsg_action_form();
-    $form['actions']['#weight'] = 20;
   }
-  // Load the themed list headers based on the available data
-  $form['#headers'] = _privatemsg_list_headers(!empty($form['#data']), $keys);
 
   // Define checkboxes, pager and theme
-  $form['threads'] = array('#type' => 'checkboxes', '#options' => $threads);
   $form['pager'] = array('#theme' => 'pager', '#weight' => 20);
   //$form['#theme'] = 'privatemsg_list';
 
@@ -1052,7 +1050,7 @@ function privatemsg_sql_list($account, $
     ->condition('pmi.deleted', 0)
     ->groupBy('pmi.thread_id')
     ->orderBy('MAX(pmi.is_new)', 'DESC')
-    ->orderByHeader(_privatemsg_list_headers( FALSE, array('subject', 'last_updated') + $fields))
+    ->orderByHeader(_privatemsg_list_headers(array_merge(array('subject', 'last_updated'), $fields)))
     ->limit(variable_get('privatemsg_per_page', 25));
 }
 
@@ -1971,22 +1969,20 @@ function privatemsg_thread_change_status
  * @return
  *   Array with header defintions for tablesort_sql and theme('table').
  */
-function _privatemsg_list_headers($has_posts, $keys) {
-  $select_header = $has_posts ? theme('table_select_header_cell') : '';
-  $select_header['#weight'] = -50;
+function _privatemsg_list_headers($keys) {
 
   // theme() doesn't include the theme file for patterns, we need to do it manually.
   include_once drupal_get_path('module', 'privatemsg') .'/privatemsg.theme.inc';
 
-  $header = array($select_header);
+  $header = array();
   foreach ($keys as $key) {
     // First, try to load a specific theme for that header, if not present, use the default.
-    if ($return = theme(array('privatemsg_list_header__'. $key, 'privatemsg_list_header'))) {
+    if ($return = theme(array('privatemsg_list_header__' . $key , 'privatemsg_list_header'))) {
       // The default theme returns nothing, only store the value if we have something.
       $header[$key] = $return;
     }
   }
-  if (count($header) == 1) {
+  if (count($header) == 0) {
     // No header definition returned, fallback to the default.
     $header += _privatemsg_list_headers_fallback($keys);
   }
@@ -2012,7 +2008,7 @@ function _privatemsg_list_headers_fallba
 }
 
 /**
- * Formats a row in the message list.
+ * Formats all rows (#options) in the privatemsg tableselect thread list.
  *
  * Uses @link theming theme patterns @endlink to theme single fields.
  *
@@ -2021,24 +2017,26 @@ function _privatemsg_list_headers_fallba
  * @return
  *   Row definition for use with theme('table')
  */
-function _privatemsg_list_thread($thread) {
-  $row = array('data' => array());
-
-  if (!empty($thread['is_new'])) {
-    // Set the css class in the tr tag.
-    $row['class'][] = 'privatemsg-unread';
-  }
-  foreach ($thread as $key => $data) {
-    // First, try to load a specific theme for that field, if not present, use the default.
-    if ($return = theme(array('privatemsg_list_field__'. $key, 'privatemsg_list_field'), $thread)) {
-      // The default theme returns nothing, only store the value if we have something.
-      $row['data'][$key] = $return;
+function _privatemsg_list_thread($tableselect) {
+  foreach ($tableselect['#options'] as $id => $thread) {
+    $row = array();
+    if (!empty($thread['is_new'])) {
+      // Set the css class in the tr tag.
+      $row['#attributes']['class'][] = 'privatemsg-unread';
+    }
+    foreach ($thread as $key => $data) {
+      // First, try to load a specific theme for that field, if not present, use the default.
+      if ($return = theme(array('privatemsg_list_field__'. $key, 'privatemsg_list_field'), $thread)) {
+        // The default theme returns nothing, only store the value if we have something.
+        $row['data'][$key] = $return;
+      }
     }
+    if (empty($row['data'])) {
+      $row = _privatemsg_list_thread_fallback($thread);
+    }
+    $tableselect['#options'][$id] = $row;
   }
-  if (empty($row['data'])) {
-    $row['data'] = _privatemsg_list_thread_fallback($thread);
-  }
-  return $row;
+  return $tableselect;
 }
 
 /**
@@ -2105,7 +2103,7 @@ function privatemsg_list_submit($form, &
 
   // Only execute something if we have a valid callback and at least one checked thread.
   if (!empty($operation['callback'])) {
-    privatemsg_operation_execute($operation, $form_state['values']['threads']);
+    privatemsg_operation_execute($operation, $form_state['values']['list']);
   }
 }
 
Index: privatemsg.theme.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/Attic/privatemsg.theme.inc,v
retrieving revision 1.1.2.7.2.4
diff -u -p -r1.1.2.7.2.4 privatemsg.theme.inc
--- privatemsg.theme.inc	12 Nov 2009 14:35:09 -0000	1.1.2.7.2.4
+++ privatemsg.theme.inc	23 Nov 2009 23:09:37 -0000
@@ -155,7 +155,6 @@ function phptemplate_privatemsg_list_fie
  *   this column.
  */
 function theme_privatemsg_list_header() {
-
 }
 
 /**
@@ -167,7 +166,6 @@ function phptemplate_privatemsg_list_hea
   return array(
     'data'    => t('Subject'),
     'field'   => 'subject',
-    'key'     => 'subject',
     'class'   => array('privatemsg-header-subject'),
     '#weight' => -40,
   );
@@ -181,7 +179,6 @@ function phptemplate_privatemsg_list_hea
 function phptemplate_privatemsg_list_header__count() {
   return array(
     'data'    => t('Messages'),
-    'key'     => 'count',
     'class'   => array('privatemsg-header-count'),
     '#weight' => -25,
   );
@@ -195,7 +192,6 @@ function phptemplate_privatemsg_list_hea
 function phptemplate_privatemsg_list_header__participants() {
   return array(
     'data'    => t('Participants'),
-    'key'     => 'participants',
     'class'   => array('privatemsg-header-participants'),
     '#weight' => -30,
   );
@@ -210,7 +206,6 @@ function phptemplate_privatemsg_list_hea
   return array(
     'data'    => t('Last Updated'),
     'field'   => 'last_updated',
-    'key'     => 'last_updated',
     'sort'    => 'desc',
     'class'   => array('privatemsg-header-lastupdated'),
     '#weight' => -20,
@@ -226,78 +221,12 @@ function phptemplate_privatemsg_list_hea
   return array(
     'data'    => t('Started'),
     'field'   => 'thread_started',
-    'key'     => 'thread_started',
     'class'   => array('privatemsg-header-threadstarted'),
     '#weight' => -15,
   );
 }
 
 /**
- * Theme to display the privatemsg list.
- *
- * This theme builds a table with paging based on the data which has been built
- * by the header and field theme patterns.
- */
-function theme_privatemsg_list($form) {
-  // @todo: Something is wrong, remove this hack.
-  $form = $form['form'];
-  $has_posts = !empty($form['#rows']);
-
-  drupal_add_css(drupal_get_path('module', 'privatemsg') .'/styles/privatemsg-list.css');
-
-  // sort the headers array based on the #weight property.
-  $headers = $form['#headers'];
-  usort($headers, 'element_sort');
-
-  $themed_rows = array();
-  // Check if there is atleast a single thread.
-  if ($has_posts) {
-    foreach ($form['#rows'] as $thread_id => $row) {
-      $data = array();
-      // Render the checkbox.
-      $data[] = array('data' => drupal_render($form['threads'][$thread_id]), 'class' => array('privatemsg-list-select'));
-
-      // Store the #rows data in the same order as the header is, the key property of the header refers to the field that belongs to it.
-      foreach ($headers as $header) {
-        if (!empty($header['key'])) {
-          if (isset($row['data'][$header['key']])) {
-            $data[] = $row['data'][$header['key']];
-          }
-          else {
-            // Store a empty value so that the order is still correct.
-            $data[] = '';
-          }
-        }
-      }
-      // Replace the data
-      $row['data'] = $data;
-      $themed_rows[] = $row;
-    }
-  }
-  else {
-    // Display a message if now messages are available.
-    $themed_rows[] = array(array('data' => t('No messages available.'), 'colspan' => count($headers)));
-  }
-
-  // Remove any data in header that we don't need anymore.
-  foreach ($headers as $id => $header) {
-    unset($headers[$id]['key']);
-    unset($headers[$id]['#weight']);
-  }
-
-  // Theme the table, pass all generated information to the table theme function.
-  $form['list'] = array(
-    '#markup' => theme('table', array(
-      'header'     => $headers,
-      'rows'       => $themed_rows,
-      'attributes' => array('class' => array('privatemsg-list')),
-    )),
-    '#weight' => 5,
-  );
-  return drupal_render_children($form);
-}
-
-/**
  * Theme a block which displays the number of new messages a user has.
  */
 function theme_privatemsg_new_block($count) {
Index: privatemsg_filter/privatemsg_filter.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/privatemsg_filter/privatemsg_filter.module,v
retrieving revision 1.1.2.17.2.10
diff -u -p -r1.1.2.17.2.10 privatemsg_filter.module
--- privatemsg_filter/privatemsg_filter.module	9 Nov 2009 21:13:33 -0000	1.1.2.17.2.10
+++ privatemsg_filter/privatemsg_filter.module	23 Nov 2009 23:09:40 -0000
@@ -164,6 +164,8 @@ function privatemsg_filter_form_private_
     '#title' => t('Choose the default list option'),
     '#description' => t('Choose which of the two lists are shown by default when following the messages link.'),
   );
+  // Add tags to the list of possible columns.
+  $form['privatemsg_listing']['privatemsg_display_fields']['#options']['tags'] = t('Tags');
 
   $form['#submit'][] = 'privatemsg_filter_settings_submit';
 }
@@ -487,8 +489,28 @@ function privatemsg_filter_form_privatem
     $form += privatemsg_filter_dropdown($form_state, $form['#account']);
   }
 
+  $fields = array_filter(variable_get('privatemsg_display_fields', array('participants')));
+  if (in_array('tags', $fields)) {
+    // Load thread id's of the current list.
+    $threads = array_keys($form['list']['#options']);
+
+    // Fetch all tags of those threads.
+    $query = _privatemsg_assemble_query(array('tags', 'privatemsg_filter'), $user, $threads, 3);
+
+    // Add them to #data
+    foreach ($query->execute() as $tag) {
+      $form['list']['#options'][$tag->thread_id]['tags'][$tag->tag_id] = $tag->tag;
+    }
+    // Avoid notices for threads without tags.
+    foreach ($form['list']['#options'] as &$thread) {
+      if (empty($thread['tags'])) {
+        $thread['tags'] = array();
+      }
+    }
+  }
+
   $tags = privatemsg_filter_get_tags_data($user);
-  if (privatemsg_user_access('filter private messages') && !empty($tags) && !empty($form['#data'])) {
+  if (privatemsg_user_access('filter private messages') && !empty($tags) && !empty( $form['list']['#options'])) {
     $options = array();
     $options[] = t('Apply Tag...');
     foreach ($tags as $tag_id => $tag) {
@@ -525,6 +547,43 @@ function privatemsg_filter_form_privatem
 }
 
 /**
+ * Define the header for the tags column.
+ *
+ * @see theme_privatemsg_list_header()
+ */
+function phptemplate_privatemsg_list_header__tags() {
+   return array(
+    'data' => t('Tags'),
+    'key' => 'tags',
+    'class' => 'privatemsg-header-tags',
+    '#weight' => -42,
+  );
+}
+
+
+/**
+ * Default theme pattern function to display tags.
+ * 
+ * @see theme_privatemsg_list_field()
+ */
+function phptemplate_privatemsg_list_field__tags($thread) {
+  if (!empty($thread['tags'])) {
+    $tags = array();
+
+    foreach ($thread['tags'] as $tag_id => $tag) {
+      $tags[] = l(strlen($tag) > 15 ? substr($tag, 0, 13) . '...' : $tag, 'messages', array(
+        'attributes' => array('title' => $tag),
+        'query' => array('tags' => $tag)
+      ));
+    }
+    return array(
+      'data'  => implode(', ', $tags),
+      'class' => 'privatemsg-list-tags',
+    );
+  }
+}
+
+/**
  * Form callback for adding a tag to threads.
  */
 function privatemsg_filter_add_tag_submit($form, &$form_state) {
@@ -535,7 +594,7 @@ function privatemsg_filter_add_tag_submi
     'undo callback arguments' => array('tag_id' => $form_state['values']['tag-add']),
   );
   drupal_set_message(t('The selected conversations have been tagged.'));
-  privatemsg_operation_execute($operation, $form_state['values']['threads']);
+  privatemsg_operation_execute($operation, $form_state['values']['list']);
 }
 
 /**
@@ -549,7 +608,7 @@ function privatemsg_filter_remove_tag_su
     'undo callback arguments' => array('tag_id' => $form_state['values']['tag-remove']),
   );
   drupal_set_message(t('The tag has been removed from the selected conversations.'));
-  privatemsg_operation_execute($operation, $form_state['values']['threads']);
+  privatemsg_operation_execute($operation, $form_state['values']['list']);
 }
 
 /**
@@ -622,7 +681,7 @@ function privatemsg_filter_form(&$form_s
   $thread_id = arg(2);
 
   // Get a list of current tags for this thread
-  $query = _privatemsg_assemble_query(array('tags', 'privatemsg_filter'), $user, $thread_id);
+  $query = _privatemsg_assemble_query(array('tags', 'privatemsg_filter'), $user, array($thread_id));
   $count = $query->countQuery()->execute()->fetchField();
   $tags = implode(', ', $query->execute()->fetchCol(1));
 
@@ -723,17 +782,20 @@ function privatemsg_filter_query_private
  * Query definition to get the tags in use by the specified user.
  *
  * @param $user
- *  User object for whom we want the tags.
- * @param $thread_id
- *  The thread_id - Only needed if we want the tags for a specific thread instead of all used tags on any thread.
+ *   User object for whom we want the tags.
+ * @param $threads
+ *   Array of thread ids, defaults to all threads of a user.
+ * @param $limit
+ *   Limit the number of tags *per thread*.
  */
-function privatemsg_filter_sql_tags($user = NULL, $thread_id = NULL) {
+function privatemsg_filter_sql_tags($user = NULL, $threads = NULL, $limit = NULL) {
   $query = db_select('pm_tags', 't')
     ->fields('t', array('tag_id', 'tag', 'public'))
     ->orderBy('t.tag', 'ASC');
-  if (!empty($thread_id)) {
+  if (!empty($threads)) {
+    $query->addField('ti', 'thread_id');
     $query->join('pm_tags_index', 'ti', 'ti.tag_id = t.tag_id');
-    $query->condition('ti.thread_id', $thread_id);
+    $query->condition('ti.thread_id', $threads);
   }
   else {
     $query->addExpression('COUNT(ti.thread_id)', 'count');
@@ -746,7 +808,22 @@ function privatemsg_filter_sql_tags($use
   if (!empty($user)) {
     $query->condition('ti.uid', $user->uid);
   }
-  if (!empty($thread_id) || !empty($user)) {
+
+  // Only select n tags per thread (ordered per tag_id), see
+  // http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/.
+  //
+  // It does select how many tags for that thread/uid combination exist that
+  // have a lower tag_id and does only select those that have less than $limit.
+  //
+  // This should only have a very minor performance impact as most users won't
+  // tag a thread with 1000 different tags.
+  if ($limit) {
+    $query->where('(SELECT count(*) FROM {pm_tags_index} AS pmtic
+                    WHERE pmtic.thread_id = ti.thread_id
+                    AND pmtic.uid = ti.uid
+                    AND pmtic.tag_id < ti.tag_id) < :limit', array(':limit' => $limit));
+  }
+  elseif (!empty($thread_id) || !empty($user)) {
     $query->orderBy('t.tag', 'ASC');
   }
   return $query;
Index: privatemsg_filter/privatemsg_filter.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/privatemsg_filter/Attic/privatemsg_filter.test,v
retrieving revision 1.1.4.5
diff -u -p -r1.1.4.5 privatemsg_filter.test
--- privatemsg_filter/privatemsg_filter.test	29 Oct 2009 15:57:14 -0000	1.1.4.5
+++ privatemsg_filter/privatemsg_filter.test	23 Nov 2009 23:09:41 -0000
@@ -74,7 +74,7 @@ class PrivatemsgFilterTestCase extends D
     // Test for bug http://drupal.org/node/617648
     // Delete all messages for author.
     $delete = array(
-        'threads[1]' => 1,
+        'list[1]' => 1,
     );
     $this->drupalPost(NULL, $delete, t('Delete'));
     $this->assertNoText($edit['subject'], t('Thread has been deleted for author.'));
Index: styles/privatemsg-list.css
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/styles/privatemsg-list.css,v
retrieving revision 1.1.2.2
diff -u -p -r1.1.2.2 privatemsg-list.css
--- styles/privatemsg-list.css	17 Jul 2009 00:32:05 -0000	1.1.2.2
+++ styles/privatemsg-list.css	23 Nov 2009 23:09:41 -0000
@@ -9,3 +9,11 @@
 .privatemsg-list-count {
   text-align: center;
 }
+
+.privatemsg-list-subject {
+  min-width: 35%;
+}
+
+.privatemsg-list-date {
+  min-width: 20%;
+}
