You are here

privatemsg_filter.module in Privatemsg 6

Allows users to tag private messages and to filter based upon those tags.

File

privatemsg_filter/privatemsg_filter.module
View source
<?php

/**
 * @file
 * Allows users to tag private messages and to filter based upon those tags.
 */

/**
 * Implements hook_perm().
 */
function privatemsg_filter_perm() {
  return array(
    'filter private messages',
    'tag private messages',
    'create private message tags',
  );
}

/**
 * Implements hook_menu().
 */
function privatemsg_filter_menu() {
  $items['admin/settings/messages/filter'] = array(
    'title' => 'Filter',
    'description' => 'Configure filter settings.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'privatemsg_filter_admin',
    ),
    'access arguments' => array(
      'administer privatemsg settings',
    ),
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/settings/messages/tags'] = array(
    'title' => 'Tags',
    'description' => 'Configure tags.',
    'page callback' => 'privatemsg_tags_admin',
    'access arguments' => array(
      'administer privatemsg settings',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'privatemsg_filter.admin.inc',
  );
  $items['admin/settings/messages/tags/list'] = array(
    'title' => 'List',
    'description' => 'Configure tags.',
    'page callback' => 'privatemsg_tags_admin',
    'access arguments' => array(
      'administer privatemsg settings',
    ),
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'file' => 'privatemsg_filter.admin.inc',
    'weight' => -10,
  );
  $items['admin/settings/messages/tags/add'] = array(
    'title' => 'Add',
    'description' => 'Configure tags.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'privatemsg_tags_form',
    ),
    'access arguments' => array(
      'administer privatemsg settings',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'privatemsg_filter.admin.inc',
  );
  $items['admin/settings/messages/tags/edit/%'] = array(
    'title' => 'Add',
    'description' => 'Configure tags.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'privatemsg_tags_form',
      5,
    ),
    'access arguments' => array(
      'administer privatemsg settings',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'privatemsg_filter.admin.inc',
  );
  $items['admin/settings/messages/tags/delete/%'] = array(
    'title' => 'Add',
    'description' => 'Configure tags.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'privatemsg_filter_tags_delete',
      5,
    ),
    'access arguments' => array(
      'administer privatemsg settings',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'privatemsg_filter.admin.inc',
  );
  $items['messages/inbox'] = array(
    'title' => 'Inbox',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'privatemsg_list',
      'inbox',
    ),
    'access callback' => 'privatemsg_user_access',
    'type' => variable_get('privatemsg_filter_default_list', 0) ? MENU_LOCAL_TASK : MENU_DEFAULT_LOCAL_TASK,
    'weight' => -15,
  );
  $items['messages/sent'] = array(
    'title' => 'Sent messages',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'privatemsg_list',
      'sent',
    ),
    'access callback' => 'privatemsg_user_access',
    'type' => MENU_LOCAL_TASK,
    'weight' => -12,
  );
  $items['messages/filter/user-name-autocomplete'] = array(
    'page callback' => 'privatemsg_user_name_autocomplete',
    'access callback' => 'privatemsg_user_access',
    'access arguments' => array(
      'write privatemsg',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['messages/filter/tag-autocomplete'] = array(
    'page callback' => 'privatemsg_filter_tags_autocomplete',
    'access callback' => 'privatemsg_user_access',
    'access arguments' => array(
      'tag private messages',
    ),
    'type' => MENU_CALLBACK,
    'weight' => -10,
  );
  return $items;
}

/**
 * Implements hook_menu_alter().
 */
function privatemsg_filter_menu_alter(&$items) {

  // Rename messages to "All messages".
  $items['messages/list']['title'] = 'All messages';
  if (variable_get('privatemsg_filter_default_list', 0) == 0) {

    // Change default argument of /messages to inbox. and set the task to MENU_LOCAL_TASK.
    $items['messages']['page arguments'] = array(
      'privatemsg_list',
      'inbox',
    );
    $items['messages/list']['type'] = MENU_LOCAL_TASK;
  }
}
function privatemsg_filter_admin() {
  $form = array();
  $form['privatemsg_filter_searchbody'] = array(
    '#type' => 'checkbox',
    '#title' => t('Search message body'),
    '#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),
  );
  $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 system_settings_form($form);
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Add a filter widget to the message listing pages.
 */
function privatemsg_filter_form_private_message_settings_alter(&$form, $form_state) {
  $form['privatemsg_listing']['privatemsg_filter_default_list'] = array(
    '#type' => 'radios',
    '#default_value' => variable_get('privatemsg_filter_default_list', 0),
    '#options' => array(
      t('Inbox'),
      t('All messages'),
    ),
    '#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';
}

/**
 * Rebuilding the menu if necessary.
 */
function privatemsg_filter_settings_submit($form, &$form_state) {
  if ($form['privatemsg_listing']['privatemsg_filter_default_list']['#default_value'] != $form_state['values']['privatemsg_filter_default_list']) {
    menu_rebuild();
  }
}

/**
 * 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, $account->uid, $thread)) == 0) {
      db_query('INSERT INTO {pm_tags_index} (tag_id, uid, thread_id) VALUES (%d, %d, %d)', $tag_id, $account->uid, $thread);
    }
  }
}

/**
 * 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'])) {
    $_GET['tags'] = urldecode($_GET['tags']);
    $tag_data = privatemsg_filter_get_tags_data($account);
    foreach (explode(',', $_GET['tags']) as $tag) {
      if (isset($tag_data[$tag])) {
        $filter['tags'][$tag] = $tag;
      }
      elseif (in_array($tag, $tag_data)) {
        $filter['tags'][array_search($tag, $tag_data)] = array_search($tag, $tag_data);
      }
    }
  }
  if (isset($_GET['author'])) {
    list($filter['author']) = _privatemsg_parse_userstring($_GET['author']);
  }
  if (isset($_GET['search'])) {
    $filter['search'] = $_GET['search'];
  }
  if (!empty($filter)) {
    return $filter;
  }
  if (!empty($_SESSION['privatemsg_filter'])) {
    return $_SESSION['privatemsg_filter'];
  }
}
function privatemsg_filter_get_tags_data($account) {
  static $tag_data;
  if (is_array($tag_data)) {
    return $tag_data;
  }

  // Only show the tags that a user have used.
  $query = _privatemsg_assemble_query(array(
    'tags',
    'privatemsg_filter',
  ), $account);
  $results = db_query($query['query']);
  $tag_data = array();
  while ($result = db_fetch_object($results)) {
    $tag_data[$result->tag_id] = $result->tag;
  }
  return $tag_data;
}
function privatemsg_filter_dropdown(&$form_state, $account) {
  drupal_add_css(drupal_get_path('module', 'privatemsg_filter') . '/privatemsg_filter.css');
  $form['filter'] = array(
    '#type' => 'fieldset',
    '#title' => t('Filter messages'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['filter']['search'] = array(
    '#type' => 'textfield',
    '#title' => variable_get('privatemsg_filter_searchbody', FALSE) ? t('Search messages') : t('Search subjects'),
    '#weight' => -20,
  );
  $form['filter']['author'] = array(
    '#type' => 'textfield',
    '#title' => t('Participants'),
    '#description' => t('Separate multiple names with commas.'),
    '#weight' => -5,
    '#size' => 50,
    '#autocomplete_path' => 'messages/filter/user-name-autocomplete',
  );

  // Only show form if the user has some messages tagged.
  if (count($tag_data = privatemsg_filter_get_tags_data($account))) {
    $form['filter']['tags'] = array(
      '#type' => 'select',
      '#title' => t('Tags'),
      '#options' => $tag_data,
      '#multiple' => TRUE,
      '#size' => 5,
      '#weight' => 0,
    );
  }
  $form['filter']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Filter'),
    '#prefix' => '<div id="privatemsg-filter-buttons">',
    '#weight' => 10,
    '#submit' => array(
      'privatemsg_filter_dropdown_submit',
    ),
  );
  $form['filter']['save'] = array(
    '#type' => 'submit',
    '#value' => t('Save filter'),
    '#suffix' => '</div>',
    '#weight' => 11,
    '#submit' => array(
      'privatemsg_filter_dropdown_submit',
    ),
  );
  if ($filter = privatemsg_filter_get_filter($account)) {
    privatemsg_filter_dropdown_set_active($form, $filter);
  }
  return $form;
}
function privatemsg_filter_dropdown_set_active(&$form, $filter) {
  $form['filter']['#title'] = t('Filter messages (active)');
  $form['filter']['#collapsed'] = FALSE;
  if (isset($filter['author'])) {
    $string = '';
    foreach ($filter['author'] as $author) {
      $string .= $author->name . ', ';
    }
    $form['filter']['author']['#default_value'] = $string;
  }
  if (isset($filter['tags'])) {
    $form['filter']['tags']['#default_value'] = $filter['tags'];
  }
  if (isset($filter['search'])) {
    $form['filter']['search']['#default_value'] = $filter['search'];
  }
  $form['filter']['reset'] = array(
    '#type' => 'submit',
    '#value' => t('Reset'),
    '#suffix' => '</div>',
    '#weight' => 12,
    '#submit' => array(
      'privatemsg_filter_dropdown_submit',
    ),
  );
  unset($form['filter']['save']['#suffix']);
}
function privatemsg_filter_dropdown_submit($form, &$form_state) {
  if (!empty($form_state['values']['author'])) {
    list($form_state['values']['author']) = _privatemsg_parse_userstring($form_state['values']['author']);
  }
  switch ($form_state['values']['op']) {
    case t('Save filter'):
      $filter = array();
      if (!empty($form_state['values']['tags'])) {
        $filter['tags'] = $form_state['values']['tags'];
      }
      if (!empty($form_state['values']['author'])) {
        $filter['author'] = $form_state['values']['author'];
      }
      if (!empty($form_state['values']['search'])) {
        $filter['search'] = $form_state['values']['search'];
      }
      $_SESSION['privatemsg_filter'] = $filter;
      break;
    case t('Filter'):
      drupal_goto($_GET['q'], privatemsg_filter_create_get_query($form_state['values']));
      return;
      break;
    case t('Reset'):
      $_SESSION['privatemsg_filter'] = array();
      break;
  }
  $form_state['redirect'] = $_GET['q'];
}
function privatemsg_filter_create_get_query($filter) {
  $query = array();
  if (isset($filter['tags']) && !empty($filter['tags'])) {
    $ids = array();
    foreach ($filter['tags'] as $tag) {
      if ((int) $tag > 0) {
        $ids[] = $tag;
      }
      else {
        $query['tags'][] = $tag;
      }
    }
    $sql = 'SELECT pmt.tag FROM {pm_tags} pmt WHERE pmt.tag_id IN (' . implode(', ', $filter['tags']) . ')';
    $result = db_query($sql);
    while ($row = db_fetch_object($result)) {
      $query['tags'][] = $row->tag;
    }
    if (isset($query['tags'])) {
      $query['tags'] = implode(',', $query['tags']);
    }
  }
  if (isset($filter['author']) && !empty($filter['author'])) {
    foreach ($filter['author'] as $author) {
      if (is_object($author) && isset($author->uid) && isset($author->name)) {
        $query['author'][] = $author->name;
      }
      elseif ($author_obj = user_load($author)) {
        $query['author'][] = $author_obj->name;
      }
    }
    if (isset($query['author'])) {
      $query['author'] = implode(',', $query['author']);
    }
  }
  if (isset($filter['search']) && !empty($filter['search'])) {
    $query['search'] = $filter['search'];
  }
  return $query;
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Adds 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') && !empty($form['#data'])) {
    $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['#data']);

    // Fetch all tags of those threads.
    $query = _privatemsg_assemble_query(array(
      'tags',
      'privatemsg_filter',
    ), $user, $threads, 3);

    // Add them to #data
    $result = db_query($query['query']);
    while ($tag = db_fetch_array($result)) {
      $form['#data'][$tag['thread_id']]['tags'][$tag['tag_id']] = $tag['tag'];
    }
  }
  $tags = privatemsg_filter_get_tags_data($user);
  if (privatemsg_user_access('tag private messages') && !empty($tags) && !empty($form['#data'])) {
    $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,
    );
    $form['actions']['tag-add-submit'] = array(
      '#prefix' => '<div class="privatemsg-tag-add-submit">',
      '#suffix' => '</div>',
      '#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,
    );
    $form['actions']['tag-remove-submit'] = array(
      '#prefix' => '<div class="privatemsg-tag-remove-submit">',
      '#suffix' => '</div>',
      '#type' => 'submit',
      '#value' => t('Remove Tag'),
      '#submit' => array(
        'privatemsg_filter_remove_tag_submit',
      ),
      '#attributes' => array(
        'class' => 'privatemsg-action-button',
      ),
    );

    // JS for hiding the submit buttons.
    drupal_add_js(drupal_get_path('module', 'privatemsg_filter') . '/privatemsg-filter-list.js');
  }
}

/**
 * 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) {
  $operation = array(
    'callback' => 'privatemsg_filter_add_tags',
    'callback arguments' => array(
      'tag_id' => $form_state['values']['tag-add'],
    ),
    'undo callback' => 'privatemsg_filter_remove_tags',
    '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']);
}

/**
 * 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'],
    ),
    'undo callback' => 'privatemsg_filter_add_tags',
    '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']);
}

/**
 * Hook into the query builder to add the tagging info to the correct query
 */
function privatemsg_filter_privatemsg_sql_list_alter(&$fragments, $account, $argument) {

  // Check if its a filtered view.
  if ($argument == 'sent') {
    $fragments['where'][] = "pm.author = %d";
    $fragments['query_args']['where'][] = $account->uid;
  }
  if ($argument == 'inbox') {
    $fragments['having'][] = '((SELECT pmf.author FROM {pm_message} pmf WHERE pmf.mid = pmi.thread_id) = %d AND COUNT(pmi.thread_id) > 1) OR (SELECT COUNT(*) FROM {pm_message} pmf INNER JOIN {pm_index} pmif ON (pmf.mid = pmif.mid) WHERE pmif.thread_id = pmi.thread_id AND pmf.author <> %d) > 0';
    $fragments['query_args']['having'][] = $account->uid;
    $fragments['query_args']['having'][] = $account->uid;
  }

  // Filter the message listing by any set tags.
  if ($filter = privatemsg_filter_get_filter($account)) {
    $count = 0;
    if (isset($filter['tags']) && !empty($filter['tags'])) {
      foreach ($filter['tags'] as $tag) {
        $fragments['inner_join'][] = "INNER JOIN {pm_tags_index} pmti{$count} ON (pmti{$count}.thread_id = pmi.thread_id AND pmti{$count}.uid = pmi.uid)";
        $fragments['where'][] = "pmti{$count}.tag_id = %d";
        $fragments['query_args']['where'][] = $tag;
        $count++;
      }
    }
    if (isset($filter['author']) && !empty($filter['author'])) {
      foreach ($filter['author'] as $author) {
        $fragments['inner_join'][] = "INNER JOIN {pm_index} pmi{$count} ON (pmi{$count}.mid = pm.mid)";
        $fragments['where'][] = "pmi{$count}.uid = %d";
        $fragments['query_args']['where'][] = $author->uid;
        $count++;
      }
    }
    if (isset($filter['search']) && !empty($filter['search'])) {
      if (variable_get('privatemsg_filter_searchbody', FALSE)) {
        $fragments['where'][] = "pm.subject LIKE '%s' OR pm.body LIKE '%s'";
        $fragments['query_args']['where'][] = '%%' . $filter['search'] . '%%';
        $fragments['query_args']['where'][] = '%%' . $filter['search'] . '%%';
      }
      else {
        $fragments['where'][] = "pm.subject LIKE '%s'";
        $fragments['query_args']['where'][] = '%%' . $filter['search'] . '%%';
      }
    }
  }
}

/**
 * Hook into the view messages page to add a form for tagging purposes.
 */
function privatemsg_filter_privatemsg_view_messages_alter(&$content, $thread) {
  if (count($thread['messages']) > 0) {
    $content['tags']['#value'] = drupal_get_form('privatemsg_filter_form');
    $content['tags']['#weight'] = variable_get('privatemsg_filter_tagfield_weight', 10);
  }
}

/**
 * Form to show and allow modification of tagging information of a conversation.
 */
function privatemsg_filter_form(&$form_state) {
  global $user;
  $thread_id = arg(2);

  // Get a list of current tags for this thread
  $query = _privatemsg_assemble_query(array(
    'tags',
    'privatemsg_filter',
  ), $user, array(
    $thread_id,
  ));
  $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'),
    '#access' => privatemsg_user_access('tag private messages'),
    '#collapsible' => TRUE,
    '#collapsed' => empty($count) ? TRUE : FALSE,
  );
  $form['tags']['user_id'] = array(
    '#type' => 'value',
    '#value' => $user->uid,
  );
  $form['tags']['thread_id'] = array(
    '#type' => 'value',
    '#value' => $thread_id,
  );
  $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',
    '#value' => t('Tag this conversation'),
    '#submit' => array(
      'privatemsg_filter_form_submit',
    ),
  );
  return $form;
}
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.
    privatemsg_filter_remove_tags($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_id);
    }
    drupal_set_message(t('Tagging information has been saved.'));
  }
}

/**
 * Return autocomplete results for tags.
 *
 * Most of this code has been lifted/modified from
 * privatemsg_user_name_autocomplete().
 */
function privatemsg_filter_tags_autocomplete($string) {

  // 1: Parse $string and build a list of tags.
  $tags = array();
  $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);
}

/**
 * @addtogroup sql
 * @{
 */

/**
 * Limit the user autocomplete for the filter widget.
 *
 * @param $fragments
 *   Query fragments.
 * @param $search
 *   Username search string.
 * @param $names
 *   Array of names that are already part of the autocomplete field.
 */
function privatemsg_filter_privatemsg_sql_autocomplete_alter(&$fragments, $search, $names) {
  global $user;

  // arg(1) is an additional URL argument passed to the URL when only
  // users that are listed as recipient for threads of that user should be
  // displayed.
  // @todo: Check if these results can be grouped to avoid unecessary loops.
  if (arg(1) == 'filter') {

    // JOIN on index entries where the to be selected user is a recipient.
    $fragments['inner_join'][] = 'INNER JOIN {pm_index} pip ON pip.uid = u.uid';

    // JOIN on rows where the current user is the recipient and that have the
    // same mid as those above.
    $fragments['inner_join'][] = 'INNER JOIN {pm_index} piu ON piu.uid = %d AND pip.mid = piu.mid';
    $fragments['query_args']['join'][] = $user->uid;
  }
}

/**
 * Query definition to fetch tags.
 *
 * @param $fragments
 *   Query fragments array.
 * @param $user
 *   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(&$fragments, $user = NULL, $threads = NULL, $limit = NULL) {
  $fragments['primary_table'] = '{pm_tags} t';
  $fragments['select'][] = 't.tag';
  $fragments['select'][] = 't.tag_id';
  $fragments['select'][] = 't.public';
  if (!empty($threads)) {

    // If the tag list needs to be for specific threads.
    $fragments['select'][] = 'ti.thread_id';
    $fragments['inner_join'][] = 'INNER JOIN {pm_tags_index} ti on ti.tag_id = t.tag_id';
    $fragments['where'][] = 'ti.thread_id IN (' . db_placeholders($threads) . ')';
    $fragments['query_args']['where'] += $threads;
  }
  else {

    // Tag usage counter is only used when we select all tags.
    $fragments['select'][] = 'COUNT(ti.thread_id) as count';

    // LEFT JOIN so that unused tags are displayed too.
    $fragments['inner_join'][] = 'LEFT JOIN {pm_tags_index} ti ON t.tag_id = ti.tag_id';
    $fragments['group_by'][] = 't.tag_id';
    $fragments['group_by'][] = 't.tag';
    $fragments['group_by'][] = 't.public';
  }
  if (!empty($user)) {
    $fragments['where'][] = 'ti.uid = %d';
    $fragments['query_args']['where'][] = $user->uid;
  }

  // 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) {
    $fragments['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) < %d';
    $fragments['query_args']['where'][] = $limit;
  }
  elseif (!empty($threads) || !empty($user)) {

    // Only add a sort when we are not loading the tags for the admin page.
    // Sorting is handled through tablesort_sql() then.
    $fragments['order_by'][] = 't.tag ASC';
  }
}

/**
 * Query definition to get autocomplete suggestions for tags
 *
 * @param $fragments
 *   Query fragments array.
 * @param $search
 *   String fragment to use for tag suggestions.
 * @param $tags
 *   Array of tags not to be used as suggestions.
 */
function privatemsg_filter_sql_tags_autocomplete(&$fragments, $search, $tags) {
  global $user;
  $fragments['primary_table'] = '{pm_tags} pmt';
  $fragments['select'][] = 'pmt.tag';
  $fragments['where'][] = "pmt.tag LIKE '%s'";

  // Escape % to get through the placeholder replacement.
  $fragments['query_args']['where'][] = $search . '%%';
  if (!empty($tags)) {

    // Exclude tags.
    $fragments['where'][] = "pmt.tag NOT IN (" . db_placeholders($tags, 'text') . ")";
    $fragments['query_args']['where'] += $tags;
  }

  // LEFT JOIN to be able to load public, unused tags.
  $fragments['inner_join'][] = 'LEFT JOIN {pm_tags_index} pmti ON pmt.tag_id = pmti.tag_id AND pmti.uid = %d';
  $fragments['query_args']['join'][] = $user->uid;

  // Autocomplete should only display Tags used by that user or public tags.
  // This is done to avoid information disclosure as part of tag names.
  $fragments['where'][] = '(pmti.uid IS NOT NULL OR pmt.public = 1)';
  $fragments['order_by'][] = 'pmt.tag ASC';
}

/**
 * @}
 */

/**
 * Implement hook_user().
 */
function privatemsg_filter_user($op, &$edit, &$account, $category = NULL) {
  switch ($op) {
    case 'delete':

      // Delete tag information of that user.
      db_query("DELETE FROM {pm_tags_index} WHERE uid = %d", $account->uid, $account->uid);
      break;
  }
}

Functions

Namesort descending Description
phptemplate_privatemsg_list_field__tags Default theme pattern function to display tags.
phptemplate_privatemsg_list_header__tags Define the header for the tags column.
privatemsg_filter_add_tags Tag one or multiple threads with a tag.
privatemsg_filter_add_tag_submit Form callback for adding a tag to threads.
privatemsg_filter_admin
privatemsg_filter_create_get_query
privatemsg_filter_create_tags Function to create a tag
privatemsg_filter_dropdown
privatemsg_filter_dropdown_set_active
privatemsg_filter_dropdown_submit
privatemsg_filter_form Form to show and allow modification of tagging information of a conversation.
privatemsg_filter_form_privatemsg_list_alter Implements hook_form_FORM_ID_alter().
privatemsg_filter_form_private_message_settings_alter Implements hook_form_FORM_ID_alter().
privatemsg_filter_form_submit
privatemsg_filter_get_filter
privatemsg_filter_get_tags_data
privatemsg_filter_menu Implements hook_menu().
privatemsg_filter_menu_alter Implements hook_menu_alter().
privatemsg_filter_perm Implements hook_perm().
privatemsg_filter_privatemsg_sql_autocomplete_alter Limit the user autocomplete for the filter widget.
privatemsg_filter_privatemsg_sql_list_alter Hook into the query builder to add the tagging info to the correct query
privatemsg_filter_privatemsg_view_messages_alter Hook into the view messages page to add a form for tagging purposes.
privatemsg_filter_remove_tags Remove tag from one or multiple threads.
privatemsg_filter_remove_tag_submit Form callback for removing a tag to threads.
privatemsg_filter_settings_submit Rebuilding the menu if necessary.
privatemsg_filter_sql_tags Query definition to fetch tags.
privatemsg_filter_sql_tags_autocomplete Query definition to get autocomplete suggestions for tags
privatemsg_filter_tags_autocomplete Return autocomplete results for tags.
privatemsg_filter_user Implement hook_user().