Only in privatemsg: privatemsg.filter_autocomplete.patch
Only in privatemsg: privatemsg.module.orig
diff -urp privatemsg/privatemsg_filter/privatemsg_filter.install privatemsg_textfield/privatemsg_filter/privatemsg_filter.install
--- privatemsg/privatemsg_filter/privatemsg_filter.install	2009-04-16 21:10:35.000000000 +0100
+++ privatemsg_textfield/privatemsg_filter/privatemsg_filter.install	2009-04-23 20:38:26.632361000 +0100
@@ -66,5 +66,6 @@ function privatemsg_filter_install() {
 
 function privatemsg_filter_uninstall() {
   variable_del('privatemsg_filter_searchbody');
+  variable_del('privatemsg_filter_tagfield_weight');
   drupal_uninstall_schema('privatemsg_filter');
 }
diff -urp privatemsg/privatemsg_filter/privatemsg_filter.module privatemsg_textfield/privatemsg_filter/privatemsg_filter.module
--- privatemsg/privatemsg_filter/privatemsg_filter.module	2009-04-23 18:39:12.911361000 +0100
+++ privatemsg_textfield/privatemsg_filter/privatemsg_filter.module	2009-04-24 03:12:18.731361000 +0100
@@ -21,13 +21,6 @@ function privatemsg_filter_perm() {
  * Implementation of hook_menu().
  */
 function privatemsg_filter_menu() {
-  $items['messages/tags'] = array(
-    'title'            => 'Tags',
-    'page callback'    => 'privatemsg_filter_page',
-    'access callback'  => 'privatemsg_user_access',
-    'access arguments' => array('use privatemsg_filter'),
-    'type'             => MENU_LOCAL_TASK,
-  );
   $items['admin/settings/messages/filter'] = array(
     'title'            => 'Filter',
     'description'      => 'Configure filter settings.',
@@ -67,6 +60,13 @@ function privatemsg_filter_menu() {
     'type'             => MENU_CALLBACK,
     'weight'           => -10,
   );
+  $items['messages/filter/tag-autocomplete'] = array(
+    'page callback'    => 'privatemsg_filter_tags_autocomplete',
+    'access callback'  => 'privatemsg_user_access',
+    'access arguments' => array('use privatemsg_filter'),
+    'type'             => MENU_CALLBACK,
+    'weight'           => -10,
+  );
   return $items;
 }
 
@@ -79,136 +79,22 @@ function privatemsg_filter_admin() {
     '#description'   => t('WARNING: turning on this feature will slow down search performance by a large factor. Gets worse as your messages database increases.'),
     '#default_value' => variable_get('privatemsg_filter_searchbody', FALSE),
   );
-
-  return system_settings_form($form);
-}
-
-function privatemsg_filter_page() {
-  $content = '';
-  drupal_set_title(t('Tags'));
-
-  $sql = 'SELECT * FROM {pm_tags}';
-  $query = db_query($sql);
-  $tag_array = array();
-
-  while ($result = db_fetch_object($query)) {
-    $tag_array[] = l($result->tag, 'messages', array('query' => 'tags='. $result->tag));
-  }
-  if (count($tag_array)) {
-    $content .= '<h2>'. t('Current tags:') .'</h2>';
-    $content .= implode(', ', $tag_array) .'.';
-  }
-
-  if (privatemsg_user_access('create privatemsg_filter')) {
-    $content .= drupal_get_form('privatemsg_filter_add_tags');
-  }
-  if (privatemsg_user_access('delete privatemsg_filter')) {
-    $content .= drupal_get_form('privatemsg_filter_delete_tags');
-  }
-
-  return $content;
-}
-
-/**
- * Add new tags
- */
-function privatemsg_filter_add_tags($form_state) {
-  $form['addtags'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Add tags'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-  );
-
-  $form['addtags']['newtags'] = array(
-    '#type' => 'textfield',
-    '#title' => t('What tags would you like to add?'),
-    '#description' => t('Please insert a comma separated list of new tags in the form of "tag1, tag2, tag3...". All spaces will be replaced by hyphens.'),
-    '#default_value' => '',
-    '#required' => 1,
-  );
-
-  $form['addtags']['submit'] = array(
-   '#type'     => 'submit',
-   '#value'    => t('Add tags'),
-  );
-
-
-  return $form;
-}
-
-function privatemsg_filter_add_tags_submit($form, &$form_state) {
-
-  if (isset($form_state['values']['submit'])) {
-    $tags = explode(',', $form_state['values']['newtags']);
-
-    foreach ($tags as $tag) {
-      $tag = trim($tag);
-      $tag = str_replace(' ', '-', $tag);
-      $count = db_result(db_query("SELECT COUNT(*) FROM {pm_tags} WHERE tag = '%s'", $tag));
-      if ($count == 0) {
-        db_query("INSERT INTO {pm_tags} (tag) VALUES ('%s')", $tag);
-        $inserted[] = $tag;
-      }
-    }
-  }
-  if (count($inserted)) {
-    drupal_set_message(t('!count tags have been saved: !tags.', array('!count' => count($inserted), '!tags' => implode(', ', $inserted))));
-  }
-}
-
-/**
- * Delete existing tags
- */
-function privatemsg_filter_delete_tags($form_state) {
-
-  $form['deletetags'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Delete tags'),
-    '#collapsible' => TRUE,
-    '#collapsed' => TRUE,
-  );
-
-  $sql = 'SELECT * FROM {pm_tags}';
-  $query = db_query($sql);
-  $tag_array = array();
-
-  while ($result = db_fetch_object($query)) {
-    $checked = 0;
-    $form_data = array('#type' => 'checkbox', '#title' => $result->tag, '#default_value' => $checked, '#return_value' => 1);
-    $form['deletetags']['tag_'. $result->tag_id] = $form_data;
-  }
-
-  $form['deletetags']['submit'] = array(
-    '#type'     => 'submit',
-    '#value'    => t('Delete selected tags'),
+  
+    $form['privatemsg_filter_tagfield_weight'] = array(
+    '#type'          => 'textfield',
+    '#title'         => t('Position of the tagging textfield'),
+    '#description'   => t('Use higher values to push the form lower down the page, lower or negative values to raise it higher.'),
+    '#size'          => 4,
+    '#default_value' => variable_get('privatemsg_filter_tagfield_weight', 10),
   );
 
-  return $form;
-
-}
-
-function privatemsg_filter_delete_tags_submit($form, &$form_state) {
-  if (isset($form_state['values']['submit'])) {
-    $sql = 'SELECT * FROM {pm_tags}';
-    $query = db_query($sql);
-
-    while ($result = db_fetch_object($query)) {
-      if ($form_state['values']['tag_'. $result->tag_id] == 1) {
-        db_query('DELETE FROM {pm_tags_index} WHERE tag_id = %d', $result->tag_id);
-        db_query('DELETE FROM {pm_tags} WHERE tag_id = %d', $result->tag_id);
-        $deleted[] = $result->tag;
-      }
-    }
-  }
-  if (count($deleted)) {
-    drupal_set_message(t('!count tags have been deleted: !tags.', array('!count' => count($deleted), '!tags' => implode(', ', $deleted))));
-  }
+  return system_settings_form($form);
 }
 
 function privatemsg_filter_get_filter($account) {
   $filter = array();
   if (isset($_GET['tags'])) {
+    $_GET['tags'] = urldecode($_GET['tags']);
     $tag_data = privatemsg_filter_get_tags_data($account);
     foreach (explode(' ', $_GET['tags']) as $tag) {
       if (isset($tag_data[$tag])) {
@@ -479,9 +365,9 @@ function privatemsg_filter_privatemsg_sq
 }
 
 function privatemsg_filter_privatemsg_view_messages_alter(&$content, $thread) {
-  if (count($thread['messages']) > 0 && db_result(db_query('SELECT COUNT(*) FROM {pm_tags}')) > 0) {
+  if (count($thread['messages']) > 0) {
     $content['tags']['#value'] = drupal_get_form('privatemsg_filter_form');
-    $content['tags']['#weight'] = 10;
+    $content['tags']['#weight'] = variable_get('privatemsg_filter_tagfield_weight', 10);
   }
 }
 
@@ -489,11 +375,20 @@ function privatemsg_filter_form(&$form_s
   global $user;
   $thread_id = arg(2);
 
+  // Get a list of current tags for this thread
+  $query = _privatemsg_assemble_query(array('used_tags', 'privatemsg_filter'), $thread_id, $user);
+  $results = db_query($query['query']);
+  $count = db_result(db_query($query['count']));
+  $tags = '';
+  while ($tag = db_fetch_array($results)) {
+    $tags .= $tag['tag']. ', ';
+  }
+
   $form['tags'] = array(
     '#type' => 'fieldset',
     '#title' => t('Tags'),
     '#collapsible' => TRUE,
-    '#collapsed' => TRUE,
+    '#collapsed' => empty($count) ? TRUE : FALSE,
   );
   $form['tags']['user_id'] = array(
     '#type' => 'value',
@@ -503,19 +398,15 @@ function privatemsg_filter_form(&$form_s
     '#type' => 'value',
     '#value' => $thread_id,
   );
-
-  $sql = 'SELECT * FROM {pm_tags}';
-  $query = db_query($sql);
-  $tag_array = array();
-
-  while ($result = db_fetch_object($query)) {
-    $checked = 0;
-    if (db_result(db_query('SELECT COUNT(*) FROM {pm_tags_index} WHERE tag_id = %d AND (uid = %d AND thread_id = %d)', $result->tag_id, $user->uid, $thread_id))) {
-      $checked = 1;
-    }
-    $form_data = array('#type' => 'checkbox', '#title' => $result->tag, '#default_value' => $checked, '#return_value' => 1);
-    $form['tags']['tag_'. $result->tag_id] = $form_data;
-  }
+  
+  $form['tags']['tags'] = array(
+    '#type'               => 'textfield',
+    '#title'              => t('Tags for this conversation'),
+    '#description'        => t('Separate multiple tags with commas.'),
+    '#size'               => 50,
+    '#default_value'      => $tags,
+    '#autocomplete_path'  => 'messages/filter/tag-autocomplete',
+  );
 
   $form['tags']['submit'] = array(
     '#type'     => 'submit',
@@ -526,20 +417,48 @@ function privatemsg_filter_form(&$form_s
   return $form;
 }
 
-function privatemsg_filter_form_submit($form, &$form_state) {
-  if (isset($form_state['values']['submit'])) {
-    $sql = 'SELECT * FROM {pm_tags}';
-    $query = db_query($sql);
+function privatemsg_filter_sql_used_tags(&$fragments, $thread_id, $user) {
+  $fragments['primary_table'] = '{pm_tags} t';
+  $fragments['select'][] = 't.tag';
+  $fragments['inner_join'][]  = 'INNER JOIN {pm_tags_index} ti on ti.tag_id = t.tag_id';
+  $fragments['where'][] = 'ti.thread_id = %d';
+  $fragments['where'][] = 'ti.uid = %d';
+  $fragments['query_args']['where'][] = $thread_id;
+  $fragments['query_args']['where'][] = $user->uid;
 
-    while ($result = db_fetch_object($query)) {
-      if ($form_state['values']['tag_'. $result->tag_id] == 0) {
-        db_query('DELETE FROM {pm_tags_index} WHERE tag_id = %d AND (uid = %d AND thread_id = %d)', $result->tag_id, $form_state['values']['user_id'], $form_state['values']['thread_id']);
-      }
-      elseif (db_result(db_query('SELECT COUNT(*) FROM {pm_tags_index} WHERE tag_id = %d AND (uid = %d AND thread_id = %d)', $result->tag_id, $form_state['values']['user_id'], $form_state['values']['thread_id'])) == 0) {
-        db_query('INSERT INTO {pm_tags_index} (tag_id, uid, thread_id) VALUES (%d, %d, %d)', $result->tag_id, $form_state['values']['user_id'], $form_state['values']['thread_id']);
+  $fragments['order_by'][] = 't.tag ASC';
+}
+
+function privatemsg_filter_form_submit($form, &$form_state) {  
+  if (isset($form_state['values']['submit'])) {
+    $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);
+      $tag = $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)) {
+        db_query("INSERT INTO {pm_tags} (tag) VALUES ('%s')", $tag);
+        $tag_id = db_last_insert_id('pm_tags', 'tag_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']);
     }
-    drupal_set_message(t('Tagging information has been saved.'));
+  
+  drupal_set_message(t('Tagging information has been saved.'));
   }
 }
 
@@ -550,4 +469,46 @@ function privatemsg_filter_privatemsg_sq
     $fragments['inner_join'][] = 'INNER JOIN {pm_index} piu ON piu.uid = %d AND pip.mid = piu.mid';
     $fragments['query_args']['join'][] = $user->uid;
   }
+}
+
+/**
+ * Return autocomplete results for tags.
+ *
+ * Most of this code has been lifted/modified from privatemsg_user_name_autocomplete().
+ */
+function privatemsg_filter_tags_autocomplete($string) {
+  $tags = array();
+  // 1: Parse $string and build a list of tags.
+  $fragments = explode(',', $string);
+  foreach ($fragments as $index => $tag) {
+    $tag = trim($tag);
+    $tags[$tag] = $tag;
+  }
+  
+  // 2: Find the next tag suggestion.
+  $fragment = array_pop($tags);
+  $matches = array();
+  if (!empty($fragment)) {
+    $query = _privatemsg_assemble_query(array('tags_autocomplete', 'privatemsg_filter'), $fragment, $tags);
+    $result = db_query_range($query['query'], $fragment, 0, 10);
+    $prefix = count($tags) ? implode(", ", $tags) .", " : '';
+    // 3: Build proper suggestions and print.
+    while ($tag = db_fetch_object($result)) {
+      $matches[$prefix . $tag->tag .", "] = $tag->tag;
+    }
+  }
+  // convert to object to prevent drupal bug, see http://drupal.org/node/175361
+  drupal_json((object)$matches);
+}
+
+function privatemsg_filter_sql_tags_autocomplete(&$fragments, $search, $tags) {
+  $fragments['primary_table'] = '{pm_tags} pmt';
+  $fragments['select'][] = 'pmt.tag';
+  $fragments['where'][] = "pmt.tag LIKE '%s'";
+  $fragments['query_args']['where'][] = $search .'%%';
+  if (!empty($tags)) {
+    $fragments['where'][] = "pmt.tag NOT IN (". db_placeholders($tags, 'text') .")";
+    $fragments['query_args']['where'] += $tags;
+  }
+  $fragments['order_by'][] = 'pmt.tag ASC';
 }
\ No newline at end of file
