Index: privatemsg.module
===================================================================
RCS file: /cvs/drupal/contributions/modules/privatemsg/privatemsg.module,v
retrieving revision 1.70.2.30.2.91.2.24
diff -u -p -r1.70.2.30.2.91.2.24 privatemsg.module
--- privatemsg.module	20 Feb 2009 21:38:28 -0000	1.70.2.30.2.91.2.24
+++ privatemsg.module	21 Feb 2009 17:00:28 -0000
@@ -77,28 +77,23 @@ function privatemsg_menu() {
   $items['messages'] = array(
     'title'            => 'Messages',
     'title callback'  => 'privatemsg_title_callback',
-    'page callback'    => 'privatemsg_list',
+    'page callback'    => 'drupal_get_form',
+    'page arguments'   => array('privatemsg_list', 'list'),
     'access callback'  => 'privatemsg_user_access',
     'type'             => MENU_NORMAL_ITEM,
   );
   $items['messages/inbox'] = array(
     'title'            => 'All messages',
-    'page callback'    => 'privatemsg_list',
+    'page callback'    => 'drupal_get_form',
+    'page arguments'   => array('privatemsg_list', 'list'),
     'access callback'  => 'privatemsg_user_access',
     'type'             => MENU_DEFAULT_LOCAL_TASK,
     'weight'           => -10,
   );
-  $items['messages/mark-read'] = array(
-    'title'            => 'Mark all as read',
-    'page callback'    => 'privatemsg_set_new_status',
-    'page arguments'   => array(NULL, NULL, 0, TRUE),
-    'access callback'  => 'privatemsg_unread_count',
-    'type'             => MENU_NORMAL_ITEM,
-    'weight'           => -9,
-  );
   $items['messages/sent'] = array(
     'title'            => 'Sent messages',
-    'page callback'    => 'privatemsg_list',
+    'page callback'    => 'drupal_get_form',
+    'page arguments'   => array('privatemsg_list', 'list_sent'),
     'access callback'  => 'privatemsg_user_access',
     'type'             => MENU_LOCAL_TASK,
     'weight'           => -5,
@@ -153,6 +148,13 @@ function privatemsg_menu() {
     'type'             => MENU_DEFAULT_LOCAL_TASK,
     'weight'           => -10,
   );
+  $items['messages/undo/action'] = array(
+    'title'            => 'Private messages',
+    'description'      => 'Undo last thread action',
+    'page callback'    => 'privatemsg_undo_action',
+    'access arguments' => array('read privatemsg'),
+    'type'             => MENU_CALLBACK,
+  );
   return $items;
 }
 
@@ -282,8 +284,14 @@ function privatemsg_theme() {
       'arguments'        => array('recipients' => NULL),
       'template'         => 'privatemsg-between',
     ),
-    'privatemsg_message_row'  => array(
-      'arguments'             => array('row'),
+    'privatemsg_list'    => array(
+      'arguments'        => array('form'),
+    ),
+    'privatemsg_list_headers'  => array(
+      'arguments'              => array('has_posts', 'query_name'),
+    ),
+    'privatemsg_list_thread'   => array(
+      'arguments'              => array('thread'),
     ),
     'privatemsg_new_block'  => array(
       'arguments'             => array('count'),
@@ -323,14 +331,18 @@ function privatemsg_preprocess_privatems
 /**
  * List messages.
  *
- * @param $uid - user id for whom to load messages.
+ * @param array  $form_state Form state array
+ * @param string $query_name Which query id should be used to load the messages
+ * @param int    $uid        User id messages of another user should be displayed
+ * 
+ * @return array Form array
  */
-function privatemsg_list($uid = NULL) {
+function privatemsg_list(&$form_state, $query_name = 'list', $uid = NULL) {
   global $user;
 
   // Setting default behavior...
   $account = $user;
-  if (is_int($uid) && $uid != $user->uid) {
+  if ((int)$uid > 0 && $uid != $user->uid) {
     // Trying to view someone else's messages...
     if (!privatemsg_user_access('read all private messages')) {
       drupal_set_message(t("You do not have sufficient rights to view someone else's messages"), 'warning');
@@ -342,59 +354,58 @@ function privatemsg_list($uid = NULL) {
   }
   // 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.
 
-  $content = array();
-  $content['list']['content'] = privatemsg_list_messages($account);
-  $content['list']['#weight'] = 0;
-
-  drupal_alter('privatemsg_list_messages', $content, $account);
-
-  uasort($content, 'element_sort');
+  $query = _privatemsg_assemble_query($query_name, $account);
+  $result = pager_query($query['query'], variable_get('privatemsg_per_page', 25), 0, $query['count']);
 
-  $expand = '';
-  foreach ($content as $element) {
-    $expand .= $element['content'];
+  $threads = array();
+  $form['#data'] = array();
+  while ($row = db_fetch_array($result)) {
+    $form['#data'][$row['thread_id']] = $row;
+    $form['#rows'][$row['thread_id']] = theme('privatemsg_list_thread', $row);
+    $threads[$row['thread_id']] = '';
   }
-  return $expand;
-}
 
-/**
- * Display the list of messages for a given user, called by privatemsg_privatemsg_alter
- */
-function privatemsg_list_messages($account) {
-  switch (arg(1)) {
-    case 'sent':
-      $query = _privatemsg_assemble_query('list_sent', $account);
-      break;
-    case 'inbox':
-    default:
-      $query = _privatemsg_assemble_query('list', $account);
-  }
-  $result = pager_query($query['query'], variable_get('privatemsg_per_page', 25), 0, $query['count']);
+  $form['#headers'] = theme('privatemsg_list_headers', !empty($form['#rows']), $query_name);
 
-  $rows = array();
-  while ($thread = db_fetch_array($result)) {
-    $rows[] = theme('privatemsg_message_row', $thread);
-  }
+  if (!empty($form['#rows'])) {
+    $form['actions'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Actions'),
+      '#prefix' => '<div class="container-inline">',
+      '#suffix' => '</div>',
+      '#collapsible' => TRUE,
+      '#collapsed' => FALSE,
+      '#weight' => 15,
+    );
 
-  $content = '';
-  if (!empty($rows)) {
-    $head = array();
-    foreach (array_keys($rows[0]) as $index) {
-      $head[$index] =  array('data' => $index);
-      if ($index == t('Last updated') || $index == t('Subject')) {
-        $head[$index] += array('field' => $index, 'sort' => 'desc');
-      }
-    }
+    $form['actions']['delete'] = array(
+      '#type'  => 'submit',
+      '#value' => t('Delete'),
+      '#submit' => array('privatemsg_list_submit_delete'),
+    );
 
-    $content = theme('table', $head, $rows);
-    $content .= theme('pager');
-    drupal_add_css(drupal_get_path('module', 'privatemsg') .'/styles/privatemsg-list.css');
-  }
-  else {
-    drupal_set_message(t('No messages to display.'));
+    $options = array(0 => t('More actions..'));
+    foreach (module_invoke_all('privatemsg_thread_operations') as $operation => $array) {
+      $options[$operation] = $array['label'];
+    }
+    $form['actions']['operation'] = array(
+      '#type' => 'select',
+      '#options' => $options,
+      '#default_value' => 0,
+      '#attributes' => array('onchange' => "$('#edit-submit').click()"),
+    );
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Execute'),
+      '#submit' => array('privatemsg_list_submit'),
+    );
   }
 
-  return $content;
+  $form['threads'] = array('#type' => 'checkboxes', '#options' => $threads);
+  $form['pager'] = array('#value' => theme('pager'), '#weight' => 20);
+  $form['#theme'] = 'privatemsg_list';
+  $form['#account'] = $account;
+  return $form;
 }
 
 function theme_privatemsg_new_block($count) {
@@ -406,36 +417,6 @@ function theme_privatemsg_new_block($cou
 }
 
 /**
- * Theme a row of messages
- *
- */
-function theme_privatemsg_message_row($row) {
-  $themed_row = array();
-
-  $unread = '';
-  if (isset($row['is_new']) && $row['is_new'] ) {
-    $unread = ' privatemsg-unread';
-  }
-  $themed_row[t('Subject')] = '<span class="privatemsg-list-subject'. $unread .'">'. l($row['subject'], 'messages/view/'. $row['thread_id']) .'</span>';
-  if (isset($row['author']) && $row['author']) {
-    $authors = _privatemsg_generate_user_array($row['author']);
-    $themed_row[t('Authors')] = '<span class="privatemsg-list-from'.  $unread .'">'. _privatemsg_format_participants($authors, 3, TRUE) .'</span>';
-  }
-  if (array_key_exists('recipient', $row)) {
-    $themed_row[t('Recipients')] = '';
-    if (!empty($row['recipient'])) {
-      $recipients = _privatemsg_generate_user_array($row['recipient']);
-      $themed_row[t('Recipients')] = '<span class="privatemsg-list-to'.  $unread .'">'. _privatemsg_format_participants($recipients, 3, TRUE) .'</span>';
-    }
-  }
-//  drupal_set_message('<pre>'. print_r($row, 1) . '</pre>');
-  if ($row['last_updated']) {
-    $themed_row[t('Last updated')] = '<span class="privatemsg-list-date'.  $unread .'">'. format_date($row['last_updated'], 'small') .'</span>';
-  }
-  return $themed_row;
-}
-
-/**
  * API function
  *
  * Sets a single message as read for a user.
@@ -516,7 +497,7 @@ function privatemsg_view($thread_id) {
   $content['participants']['#weight'] = -5;
 
   // Load the messages.
-  $query = _privatemsg_assemble_query('messages', $thread_id, $user);
+  $query = _privatemsg_assemble_query('messages', array($thread_id), $user);
   $conversation = db_query($query['query']);
   $message_count = 0;
   while ($result = db_fetch_array($conversation)) {
@@ -773,17 +754,9 @@ function privatemsg_sql_list_sent(&$frag
   $fragments['group_by'][]  = 'pmi.thread_id';
   $fragments['group_by'][]  = 'pm.subject';
 
-  $order = 'last_updated';
-  $sort = 'desc';
-  if (isset($_GET['order'])) {
-    switch ($_GET['order']) {
-      case 'subject':
-        $order = 'subject';
-        break;
-    }
-    $sort = isset($_GET['sort']) && ($_GET['sort'] == 'asc' || $_GET['sort'] == 'desc') ? $_GET['sort'] : 'desc';
-  }
-  $fragments['order_by'][]  = $order .' '. $sort;
+  // we can use tablesort_sql now, but we don't need the ORDER BY part of the query
+  $order_by = substr(tablesort_sql(theme('privatemsg_list_headers', false, 'sent_list')), 9);
+  $fragments['order_by'][]  = $order_by;
 }
 
 function privatemsg_sql_list(&$fragments, $account) {
@@ -811,19 +784,10 @@ function privatemsg_sql_list(&$fragments
   $fragments['query_args'][]  = $account->uid;
   $fragments['where'][]       = 'pmi.deleted = 0';
   $fragments['group_by'][]    = 'pmi.thread_id';
-  $order = 'is_new';
-  $sort = 'desc';
-  if (isset($_GET['order'])) {
-    switch ($_GET['order']) {
-      case t('Subject'): // Allows translated headers to be sorted
-        $order = 'subject';
-        break;
-      default:
-    }
-    $sort = isset($_GET['sort']) && ($_GET['sort'] == 'asc' || $_GET['sort'] == 'desc') ? $_GET['sort'] : 'desc';
-  }
-  $fragments['order_by'][]  = $order .' '. $sort .', last_updated DESC';
 
+  // we can use tablesort_sql now, but we don't need the ORDER BY part of the query
+  $order_by = ' '. substr(tablesort_sql(theme('privatemsg_list_headers', false, 'sent'), 'is_new DESC,'), 9);
+  $fragments['order_by'][]  = $order_by;
 }
 
 function privatemsg_sql_load(&$fragments, $pmid, $account) {
@@ -844,12 +808,12 @@ function privatemsg_sql_load(&$fragments
   $fragments['query_args'][]  = $account->uid;
 }
 
-function privatemsg_sql_messages(&$fragments, $thread_id, $account) {
+function privatemsg_sql_messages(&$fragments, $threads, $account) {
   $fragments['primary_table'] = '{pm_index} pmi';
 
   $fragments['select'][]      = 'DISTINCT(pmi.mid) as mid';
-  $fragments['where'][]       = 'pmi.thread_id = %d';
-  $fragments['query_args'][]  = $thread_id;
+  $fragments['where'][]       = 'pmi.thread_id IN ('. db_placeholders($threads) .')';
+  $fragments['query_args']   += $threads;
   $fragments['where'][]       = 'pmi.uid = %d';
   $fragments['query_args'][]  = $account->uid;
   $fragments['where'][]       = 'pmi.deleted = 0';
@@ -1450,3 +1414,282 @@ function _privatemsg_assemble_query($que
   }
   return FALSE;
 }
+
+/**
+ * Marks one or multiple threads as (un)read
+ *
+ * @param array|int $threads Array with id's, a single int value will be converted to int
+ * @param int       $is_new  Indiciates if the threads should be marked as read or unread. 1 = unread, 0 => read
+ * @param object    $account User object for which the threads should be deleted, defaults to the current user
+ */
+function privatemsg_thread_read($threads, $is_new = 0, $account = NULL) {
+  if (!is_array($threads)) {
+    $threads = array($threads);
+  }
+
+  if (empty($account)) {
+    global $user;
+    $account = drupal_clone($user);
+  }
+
+  $params = array($is_new, $account->uid) + $threads;
+  db_query('UPDATE {pm_index} SET is_new = %d WHERE uid = %d AND thread_id IN ('. db_placeholders($threads) .')', $params);
+
+  if ($is_new) {
+    drupal_set_message(t('Marked %count threads as unread.', array('%count' => count($threads))));
+  }
+  else {
+    drupal_set_message(t('Marked %count threads as read.', array('%count' => count($threads))));
+  }
+}
+
+/**
+ * Theme to display the privatemsg list
+ */
+function theme_privatemsg_list($form) {
+  $has_posts = !empty($form['#rows']);
+
+  drupal_add_css(drupal_get_path('module', 'privatemsg') .'/styles/privatemsg-list.css');
+
+  $headers = $form['#headers'];
+  usort($headers, 'element_sort');
+
+  $themed_rows = array();
+  foreach ($form['#rows'] as $thread_id => $row) {
+    $data = array();
+    $data[] = drupal_render($form['threads'][$thread_id]);
+
+    foreach ($headers as $header) {
+      if (!empty($header['key'])) {
+        if (isset($row['data'][$header['key']])) {
+          $data[] = $row['data'][$header['key']];
+        }
+        else {
+          $data[] = '';
+        }
+      }
+    }
+    unset($row['data']);
+    $themed_rows[] = array('data' => $data) + $row;
+  }
+
+  //remove any data in header that we don't need anymore
+  foreach ($headers as $id => $header) {
+    unset($headers[$id]['key']);
+    unset($headers[$id]['#weight']);
+  }
+
+  if (empty($themed_rows)) {
+    $themed_rows[] = array(array('data' => t('No messages available.'), 'colspan' => count($header)));
+  }
+
+  $form['list'] = array('#value' => theme('table', $headers, $themed_rows, array('class' => 'privatemsg-list')), '#weight' => 5);
+  return drupal_render($form);
+}
+
+function theme_privatemsg_list_headers($has_posts, $query_name = 'list') {
+  $select_header = $has_posts ? theme('table_select_header_cell') : '';
+  $select_header['#weight'] = -50;
+
+  $header = array($select_header);
+  $header['subject'] = array(
+    'data'    => t('Subject'),
+    'field'   => 'subject',
+    'key'     => 'subject',
+    'class'   => 'privatemsg-header-subject',
+    '#weight' => -40,
+  );
+  if ($query_name == 'list') {
+    $header['authors'] = array(
+      'data'    => t('Authors'),
+      'key'     => 'authors',
+      'class'   => 'privatemsg-header-authors',
+      '#weight' => -30,
+    );
+  }
+  if ($query_name == 'list_sent') {
+    $header['recipients'] = array(
+      'data'    => t('Recipients'),
+      'key'     => 'recipients',
+      'class'   => 'privatemsg-header-recipients',
+      '#weight' => -30,
+    );
+  }
+  $header['last_updated'] = array(
+    'data'    => t('Last Updated'),
+    'field'   => 'last_updated',
+    'key'     => 'last_updated',
+    'sort'    => 'desc',
+    'class'   => 'privatemsg-header-lastupdated',
+    '#weight' => -20,
+  );
+  return $header;
+}
+
+function theme_privatemsg_list_thread($thread) {
+  $row = array('data' => array());
+
+  if (!empty($thread['is_new'])) {
+    $row['class'] = 'privatemsg-unread';
+  }
+  $row['data']['subject']['data'] = l($thread['subject'], 'messages/view/'. $thread['thread_id']);
+  $row['data']['subject']['class'] = 'privatemsg-list-subject';
+  if (!empty($thread['author'])) {
+    $authors = _privatemsg_generate_user_array($thread['author']);
+    $row['data']['authors']['data'] = _privatemsg_format_participants($authors, 3, TRUE);
+    $row['data']['authors']['class'] = 'privatemsg-list-author';
+  }
+  if (!empty($thread['recipient'])) {
+    $recipients = _privatemsg_generate_user_array($thread['recipient']);
+    $row['data']['recipients']['data'] = _privatemsg_format_participants($recipients, 3, TRUE);
+    $row['data']['recipients']['class'] = 'privatemsg-list-to';
+  }
+  if (!empty($thread['last_updated'])) {
+    $row['data']['last_updated']['data'] = format_date($thread['last_updated'], 'small');
+    $row['data']['last_updated']['class'] = 'privatemsg-list-date';
+  }
+  return $row;
+}
+
+/**
+ * Menu callback for messages/undo/action.
+ *
+ * This function will test if a undo callback is stored in SESSION and execute it.
+ */
+function privatemsg_undo_action() {
+  if (isset($_SESSION['privatemsg']['undo callback']) && is_array($_SESSION['privatemsg']['undo callback'])) {
+    $undo = $_SESSION['privatemsg']['undo callback'];
+    if (isset($undo['function']) && isset($undo['args'])) {
+      call_user_func_array($undo['function'], $undo['args']);
+    }
+    drupal_goto();
+  }
+}
+
+/**
+ * Process privatemsg_list form submissions.
+ *
+ * Execute the chosen action on the selected messages. This function is
+ * a copy of node_admin_nodes_submit
+ */
+function privatemsg_list_submit($form, &$form_state) {
+  $operations = module_invoke_all('privatemsg_thread_operations');
+  $operation = $operations[$form_state['values']['operation']];
+  // Filter out unchecked nodes
+  $threads = array_filter($form_state['values']['threads']);
+  if ($operation['callback']) {
+    _privatemsg_execute_action($threads, $operation);
+  }
+  else {
+    // We need to rebuild the form to go to a second step.  For example, to
+    // show the confirmation form for the deletion of privatemsg.
+    $form_state['rebuild'] = TRUE;
+  }
+}
+
+/**
+ * Executes the action, helper function for privatemsg_list_submit
+ *
+ * @param array $threads   Array with thread id's to work on
+ * @param array $operation Operation array, @see privatemsg_privatemsg_thread_operations
+ */
+function _privatemsg_execute_action($threads, $operation) {
+  // Add in callback arguments if present.
+  if (isset($operation['callback arguments'])) {
+    $args = array_merge(array($threads), $operation['callback arguments']);
+  }
+  else {
+    $args = array($threads);
+  }
+  call_user_func_array($operation['callback'], $args);
+
+  if (isset($operation['undo callback']) && $undo_function = $operation['undo callback']) {
+    // Add in callback arguments if present.
+    if (isset($operation['undo callback arguments'])) {
+      $undo_args = array_merge(array($threads), $operation['undo callback arguments']);
+    }
+    else {
+      $undo_args = array($threads);
+    }
+    $_SESSION['privatemsg']['undo callback'] = array('function' => $undo_function,
+                                                     'args'     => $undo_args);
+    $undo = l(t('undone'), 'messages/undo/action', array('query' => drupal_get_destination()));
+
+    drupal_set_message(t('The previous action can be !undo.', array('!undo' => $undo)));
+  }
+}
+
+/**
+ * Submit callback for the delete button
+ *
+ * @param array $form
+ * @param array $form_state
+ */
+function privatemsg_list_submit_delete($form, &$form_state) {
+  global $user;
+
+  $operation = array(
+      'callback' => 'privatemsg_thread_delete',
+      'callback arguments' => array('delete' => 1),
+      'undo callback' => 'privatemsg_thread_delete',
+      'undo callback arguments' => array('delete' => 0),
+  );
+  _privatemsg_execute_action(array_filter($form_state['values']['threads']), $operation);
+}
+
+/**
+ * Delete or restore one or multiple threads
+ *
+ * @param array|int $threads Array with id's, a single int value will be converted to int
+ * @param int       $delete  Indiciates if the threads should be deleted or restored. 1 = delete, 0 => restore
+ * @param object    $account User object for which the threads should be deleted, defaults to the current user
+ */
+function privatemsg_thread_delete($threads, $delete = 1, $account = NULL) {
+  if (!is_array($threads)) {
+    $threads = array($threads);
+  }
+
+  if (empty($account)) {
+    global $user;
+    $account = drupal_clone($user);
+  }
+
+  $params = array($delete, $account->uid) + $threads;
+
+  $query = _privatemsg_assemble_query('messages', $threads, $account);
+  $result = db_query($query['query']);
+
+  while ($row = db_fetch_array($result)) {
+    privatemsg_message_delete($row['mid'], $account);
+  }
+
+  if ($delete) {
+    drupal_set_message(t('Deleted %count threads.', array('%count' => count($threads))));
+  }
+  else {
+    drupal_set_message(t('Restored %count threads.', array('%count' => count($threads))));
+  }
+}
+
+/**
+ * Implementation of hook_privatemsg_thread_operations().
+ */
+function privatemsg_privatemsg_thread_operations() {
+  $operations = array(
+    'mark as read' => array(
+      'label' => t('Mark as read'),
+      'callback' => '_privatemsg_thread_setnew',
+      'callback arguments' => array('new' => 0),
+      'undo callback' => '_privatemsg_thread_setnew',
+      'undo callback arguments' => array('new' => 1),
+    ),
+    'mark as unread' => array(
+      'label' => t('Mark as unread'),
+      'callback' => '_privatemsg_thread_setnew',
+      'callback arguments' => array('new' => 1),
+      'undo callback' => '_privatemsg_thread_setnew',
+      'undo callback arguments' => array('new' => 0),
+    ),
+  );
+  return $operations;
+}
\ No newline at end of file
Index: styles/privatemsg-list.css
===================================================================
RCS file: /cvs/drupal/contributions/modules/privatemsg/styles/privatemsg-list.css,v
retrieving revision 1.1
diff -u -p -r1.1 privatemsg-list.css
--- styles/privatemsg-list.css	3 Sep 2008 06:24:50 -0000	1.1
+++ styles/privatemsg-list.css	21 Feb 2009 17:00:28 -0000
@@ -1,3 +1,3 @@
-.privatemsg-list-subject.privatemsg-unread {
+tr.privatemsg-unread td.privatemsg-list-subject {
   font-weight: bold;
 }
\ No newline at end of file
