Index: privatemsg.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/privatemsg.module,v
retrieving revision 1.70.2.30.2.91.2.64
diff -u -r1.70.2.30.2.91.2.64 privatemsg.module
--- privatemsg.module	30 Jul 2009 20:02:05 -0000	1.70.2.30.2.91.2.64
+++ privatemsg.module	23 Aug 2009 15:04:08 -0000
@@ -1899,37 +1899,55 @@
     $operation = $operations[$keys[1]];
   }
 
+  // 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']);
+  }
+}
+
+/**
+ * Execute an operation on a number of threads.
+ *
+ * @param $operation
+ *   The operation that should be executed.
+ *   @see hook_privatemsg_thread_operations()
+ * @param $threads
+ *   An array of thread ids. The array is filtered before used, a checkboxes
+ *   array can be directly passed to it.
+ */
+function privatemsg_operation_execute($operation, $threads) {
   // Filter out unchecked threads, this gives us an array of "checked" threads.
-  $threads = array_filter($form_state['values']['threads']);
+  $threads = array_filter($threads);
 
-  // Only execute something if we have a valid callback and atleast one checked thread.
-  if (!empty($operation['callback']) && !empty($threads)) {
+  if (empty($threads)) {
+    // Do not execute anything if there are no checked threads.
+    return;
+  }
+  // Add in callback arguments if present.
+  if (isset($operation['callback arguments'])) {
+    $args = array_merge(array($threads), $operation['callback arguments']);
+  }
+  else {
+    $args = array($threads);
+  }
+  // Execute the chosen action and pass the defined arguments.
+  call_user_func_array($operation['callback'], $args);
+
+  // Check if that operation has defined an undo callback.
+  if (isset($operation['undo callback']) && $undo_function = $operation['undo callback']) {
     // Add in callback arguments if present.
-    if (isset($operation['callback arguments'])) {
-      $args = array_merge(array($threads), $operation['callback arguments']);
+    if (isset($operation['undo callback arguments'])) {
+      $undo_args = array_merge(array($threads), $operation['undo callback arguments']);
     }
     else {
-      $args = array($threads);
-    }
-    // Execute the chosen action and pass the defined arguments.
-    call_user_func_array($operation['callback'], $args);
-
-    // Check if that operation has defined a undo callback
-    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);
-      }
-      // Store the undo callback in the session and display a "Undo" link.
-      // @todo: Provide a more flexible solution for such an undo action, operation defined string for example.
-      $_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)));
+      $undo_args = array($threads);
     }
+    // Store the undo callback in the session and display a "Undo" link.
+    // @todo: Provide a more flexible solution for such an undo action, operation defined string for example.
+    $_SESSION['privatemsg']['undo callback'] = array('function' => $undo_function, 'args' => $undo_args);
+    $undo = url('messages/undo/action', array('query' => drupal_get_destination()));
+    
+    drupal_set_message(t('The previous action can be <a href="!undo">undone</a>.', array('!undo' => $undo)));
   }
 }
 
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
diff -u -r1.1.2.17 privatemsg_filter.module
--- privatemsg_filter/privatemsg_filter.module	11 Jul 2009 13:37:31 -0000	1.1.2.17
+++ privatemsg_filter/privatemsg_filter.module	23 Aug 2009 16:01:18 -0000
@@ -91,6 +91,95 @@
   return system_settings_form($form);
 }
 
+/**
+ * Function to create a tag
+ *
+ * @param $tags A single tag or an array of tags.
+ */
+function privatemsg_filter_create_tags($tags) {
+  if (!is_array($tags)) {
+    $tags = array($tags);
+  }
+  
+  $tag_ids = array();
+
+  foreach ($tags as $tag) {
+    $tag = trim($tag);
+    if (empty($tag)) {
+      // Do not save a blank tag.
+      continue;
+    }
+
+    // Check if the tag already exists and only create the tag if it does not.
+    $tag_id = db_result(db_query("SELECT tag_id FROM {pm_tags} WHERE tag = '%s'", $tag));
+    if (empty($tag_id) && privatemsg_user_access('create private message tags')) {
+      db_query("INSERT INTO {pm_tags} (tag) VALUES ('%s')", $tag);
+      $tag_id = db_last_insert_id('pm_tags', 'tag_id');
+    }
+    elseif (empty($tag_id)) {
+      // The user does not have permission to create new tags - disregard this tag and move onto the next.
+      drupal_set_message(t('Tag %tag was ignored because you do not have permission to create new tags.', array('%tag' => $tag)));
+      continue;
+    }
+    $tag_ids[] = $tag_id;
+  }
+  return $tag_ids;
+}
+
+/**
+ * Tag one or multiple threads with a tag.
+ *
+ * @param $threads A single thread id or an array of thread ids.
+ * @param $tag_id Id of the tag.
+ */
+function privatemsg_filter_add_tags($threads, $tag_id, $account = NULL) {
+  if (!is_array($threads)) {
+    $threads = array($threads);
+  }
+  if (empty($account)) {
+    global $user;
+    $account = drupal_clone($user);
+  }
+
+  foreach ($threads as $thread) {
+    // Make sure that we don't add a tag to a thread twice,
+    // only insert if there is no such tag yet.
+    if (db_result(db_query('SELECT COUNT(*) FROM {pm_tags_index} WHERE tag_id = %d AND (uid = %d AND thread_id = %d)', $tag_id, $user->uid, $thread)) == 0) {
+      db_query('INSERT INTO {pm_tags_index} (tag_id, uid, thread_id) VALUES (%d, %d, %d)', $tag_id, $user->uid, $thread);
+    }
+  }
+  drupal_set_message(t('Tagged %count threads.', array('%count' => count($threads))));
+}
+
+/**
+ * Remove tag from one or multiple threads.
+ *
+ * @param $threads A single thread id or an array of thread ids.
+ * @param $tag_id Id of the tag - set to NULL to remove all tags.
+ */
+function privatemsg_filter_remove_tags($threads, $tag_id = NULL, $account = NULL) {
+  if (!is_array($threads)) {
+    $threads = array($threads);
+  }
+  if (empty($account)) {
+    global $user;
+    $account = drupal_clone($user);
+  }
+
+  if (is_null($tag_id)) {
+    //Delete all tag mapping.
+    foreach ($threads as $thread) {
+      db_query('DELETE FROM {pm_tags_index} WHERE uid = %d AND thread_id = %d', $account->uid, $thread);
+    }
+  }
+  else {
+    //Delete tag mapping for the specified tag.
+    foreach ($threads as $thread) {
+      db_query('DELETE FROM {pm_tags_index} WHERE uid = %d AND thread_id = %d AND tag_id = %d', $account->uid, $thread, $tag_id);
+    }
+  }
+}
+
 function privatemsg_filter_get_filter($account) {
   $filter = array();
   if (isset($_GET['tags'])) {
@@ -308,11 +397,70 @@
  * Implementation of hook_form_alter() to add a filter widget to the message listing pages.
  */
 function privatemsg_filter_form_privatemsg_list_alter(&$form, $form_state) {
+  global $user;
+  
   if (privatemsg_user_access('filter private messages')) {
     $form += privatemsg_filter_dropdown($form_state, $form['#account']);
   }
+
+  $tags = privatemsg_filter_get_tags_data($user);
+  if (privatemsg_user_access('filter private messages') && !empty($tags)) {
+    $options = array();
+    $options[] = t('Apply Tag...');
+    foreach ($tags as $tag_id => $tag) {
+      $options[$tag_id] = $tag;
+    }
+    $form['actions']['tag-add'] = array(
+      '#type'          => 'select',
+      '#options'       => $options,
+      '#default_value' => 0,
+      // Execute the submit button if a operation has been selected.
+      '#attributes'    => array('onchange' => "$('#edit-tag-add-submit').click()"),
+    );
+    $form['actions']['tag-add-submit'] = array(
+        '#type'       => 'submit',
+        '#value'      => t('Apply Tag'),
+        '#submit'     => array('privatemsg_filter_add_tag_submit'),
+        '#attributes' => array('class' => 'privatemsg-action-button'),
+    );
+    $options[0] = t('Remove Tag...');
+    $form['actions']['tag-remove'] = array(
+      '#type'          => 'select',
+      '#options'       => $options,
+      '#default_value' => 0,
+      // Execute the submit button if a operation has been selected.
+      '#attributes'    => array('onchange' => "$('#edit-tag-remove-submit').click()"),
+    );
+    $form['actions']['tag-remove-submit'] = array(
+        '#type'       => 'submit',
+        '#value'      => t('Remove Tag'),
+        '#submit'     => array('privatemsg_filter_remove_tag_submit'),
+        '#attributes' => array('class' => 'privatemsg-action-button'),
+    );
+  }
 }
 
+/**
+ * Form callback for adding a tag to threads.
+ */
+function privatemsg_filter_add_tag_submit($form, &$form_state) {
+  $operation = array(
+    'callback' => 'privatemsg_filter_add_tags',
+    'callback arguments' => array('tag_id' => $form_state['values']['tag-add']),
+  );
+  privatemsg_operation_execute($operation, $form_state['values']['threads']);
+}
+
+/**
+ * Form callback for removing a tag to threads.
+ */
+function privatemsg_filter_remove_tag_submit($form, &$form_state) {
+  $operation = array(
+    'callback' => 'privatemsg_filter_remove_tags',
+    'callback arguments' => array('tag_id' => $form_state['values']['tag-remove']),
+  );
+  privatemsg_operation_execute($operation, $form_state['values']['threads']);
+}
 
 /**
  * Hook into the query builder to add the tagging info to the correct query
@@ -430,34 +578,16 @@
     $tags = explode(',', $form_state['values']['tags']);
 
     // Step 1 - Delete all tag mapping. I cannot think of a better way to remove tags that are no longer in the textfield, so ideas welcome.
-    db_query('DELETE FROM {pm_tags_index} WHERE uid = %d AND thread_id = %d', $form_state['values']['user_id'], $form_state['values']['thread_id']);
-
-    foreach ($tags as $tag) {
-      // Step 2 - We need to sanitise the tag.
-      // Since we allow tags to be passed via the url, there needs to be some sanity testing of each tag.
-      // Currently we replace blank spaces and a # with a "-", but this needs to be expanded to cover all the url special cases.
-      $tag = trim($tag);
-      if (empty($tag)) {
-        // Do not save a blank tag.
-        continue;
-      }
-
-      // Step 3 - Make sure that the tag exists and if it does not, we need to create it.
-      $tag_id = db_result(db_query("SELECT tag_id FROM {pm_tags} WHERE tag = '%s'", $tag));
-      if (empty($tag_id) && privatemsg_user_access('create private message tags')) {
-        db_query("INSERT INTO {pm_tags} (tag) VALUES ('%s')", $tag);
-        $tag_id = db_last_insert_id('pm_tags', 'tag_id');
-      }
-      elseif (empty($tag_id)) {
-        // The user does not have permission to create new tags - disregard this tag and move onto the next.
-        drupal_set_message(t('Tag %tag was ignored because you do not have permission to create new tags.', array('%tag' => $tag)));
-        continue;
-      }
+    privatemsg_filter_remove_tags($form_state['values']['thread_id']);
 
-      // Step 4 - map the tag to the thread and the user.
-      db_query('INSERT INTO {pm_tags_index} (tag_id, uid, thread_id) VALUES (%d, %d, %d)', $tag_id, $form_state['values']['user_id'], $form_state['values']['thread_id']);
+    // Step 2 - Get the id for each of the tags.
+    $tag_ids = privatemsg_filter_create_tags($tags);
+    
+    // Step 3 - Save all the tagging data.
+    foreach ($tag_ids as $tag_id) {
+      privatemsg_filter_add_tags($form_state['values']['thread_id'], $tag_ids);
     }
-  drupal_set_message(t('Tagging information has been saved.'));
+    drupal_set_message(t('Tagging information has been saved.'));
   }
 }
 

