? overlay_subtabs_invisible.png
Index: privatemsg.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/privatemsg.install,v
retrieving revision 1.5.2.4.2.11.2.11.2.10
diff -u -p -r1.5.2.4.2.11.2.11.2.10 privatemsg.install
--- privatemsg.install	26 Jul 2010 18:23:47 -0000	1.5.2.4.2.11.2.11.2.10
+++ privatemsg.install	15 Aug 2010 14:01:29 -0000
@@ -101,6 +101,14 @@ function privatemsg_schema() {
         'not null'      => TRUE,
         'unsigned'      => TRUE,
       ),
+      'has_tokens'     => array(
+        'description'   => 'Indicates if the message has tokens',
+        'type'          => 'int',
+        'size'          => 'small',
+        'not null'      => TRUE,
+        'unsigned'      => TRUE,
+        'default'       => 0,
+      ),
     ),
     'primary key'     => array('mid'),
     'indexes'         => array(
@@ -141,3 +149,17 @@ function privatemsg_uninstall() {
   variable_del('privatemsg_no_messages_notification');
   variable_del('privatemsg_display_on_comments');
 }
+
+/**
+ * Add has_tokens field to indicate if a message is using tokens.
+ */
+function privatemsg_update_7000() {
+  db_add_field('pm_message', 'has_tokens', array(
+    'description'   => 'Indicates if the message has tokens',
+    'type'          => 'int',
+    'size'          => 'small',
+    'not null'      => TRUE,
+    'unsigned'      => TRUE,
+    'default'       => 0,
+  ));
+}
\ No newline at end of file
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.81
diff -u -p -r1.70.2.30.2.91.2.64.2.81 privatemsg.module
--- privatemsg.module	5 Aug 2010 09:27:17 -0000	1.70.2.30.2.91.2.64.2.81
+++ privatemsg.module	15 Aug 2010 14:01:30 -0000
@@ -52,6 +52,10 @@ function privatemsg_permission() {
       'title' => t('Reply to private messages'),
       'description' => t('Allows to reply to private messages but not send new ones. Note that the write new private messages permission includes replies.')
     ),
+    'use tokens in privatemsg' => array(
+      'title' => t('Use tokens in private messages'),
+      'description' => t("Allows user to use available tokens when sending private messages.")
+    ),
   );
 }
 
@@ -509,6 +513,9 @@ function privatemsg_thread_load($thread_
         $thread['user'] = $account;
         $message = current($thread['messages']);
         $thread['subject'] = $message->subject;
+        if ($message->has_tokens) {
+          $thread['subject'] = privatemsg_token_replace($thread['subject'], array('privatemsg_message' => $message));
+        }
       }
       $threads[$account->uid][$thread_id] = $thread;
     }
@@ -680,6 +687,12 @@ function privatemsg_preprocess_privatems
     ),
   );
 
+  if ($message->has_tokens) {
+    // Replace tokens including option to add a notice if the user is not a
+    // recipient.
+    $message->content['body']['#markup'] = privatemsg_token_replace($message->content['body']['#markup'], array('privatemsg_message' => $message), array('privatemsg-token-notice' => TRUE));
+  }
+
   // Build fields content.
   field_attach_prepare_view('privatemsg_message', array($vars['mid'] => $message), 'message');
   $message->content += field_attach_view('privatemsg_message', $message, 'message');
@@ -911,6 +924,7 @@ function privatemsg_sql_list($account, $
   $query->addField('pmi', 'thread_id');
   $query->addExpression('MIN(pm.subject)', 'subject');
   $query->addExpression('MAX(pm.timestamp)', 'last_updated');
+  $query->addExpression('MAX(pm.has_tokens)', 'has_tokens');
   $query->addExpression('SUM(pmi.is_new)', 'is_new');
 
   // Load enabled columns
@@ -955,7 +969,7 @@ function privatemsg_sql_list($account, $
  */
 function privatemsg_sql_load($pmids, $account = NULL) {
   $query = db_select('pm_message', 'pm')
-    ->fields('pm', array('mid', 'author', 'subject', 'body', 'timestamp', 'format'))
+    ->fields('pm', array('mid', 'author', 'subject', 'body', 'timestamp', 'format', 'has_tokens'))
     ->fields('pmi', array('is_new', 'thread_id'))
     ->condition('pmi.mid', $pmids)
     ->orderBy('pm.timestamp', 'ASC')
@@ -1625,6 +1639,9 @@ function _privatemsg_validate_message(&$
     }
   }
 
+  // Verify if message has tokens and user is allowed to use them.
+  $message->has_tokens = privatemsg_user_access('use tokens in privatemsg', $message->author) && count(token_scan($message->subject . $message->body));
+
   $messages += module_invoke_all('privatemsg_message_validate', $message, $form);
   // Check if there are errors in $messages or if $form is TRUE, there are form errors.
   $success = empty($messages['error']) || ($form && count((array)form_get_errors()) > 0);
@@ -1675,6 +1692,7 @@ function _privatemsg_send($message) {
     $args['body'] = $message->body;
     $args['format'] = $message->format;
     $args['timestamp'] = $message->timestamp;
+    $args['has_tokens'] = (int)$message->has_tokens;
     $mid = db_insert('pm_message')
       ->fields($args)
       ->execute();
@@ -2521,4 +2539,190 @@ function privatemsg_field_extra_fields()
     )
   );
   return $extra;
+}
+
+/**
+ * Implements hook_token_info().
+ */
+function privatemsg_token_info() {
+  $type = array(
+    'name' => t('Private message'),
+    'description' => t('Tokens related to private messages.'),
+    'needs-data' => 'privatemsg_message',
+  );
+
+  // Core tokens for nodes.
+  $message['mid'] = array(
+    'name' => t("Message ID"),
+    'description' => t("The unique ID of the message."),
+  );
+  $message['subject'] = array(
+    'name' => t("Subject"),
+    'description' => t("The subject of the message."),
+  );
+
+  // Chained tokens for nodes.
+  $message['sent'] = array(
+    'name' => t("Date created"),
+    'description' => t("The date the message was sent."),
+    'type' => 'date',
+  );
+  $message['author'] = array(
+    'name' => t("Author"),
+    'description' => t("The author of the message."),
+    'type' => 'user',
+  );
+  $message['recipient'] = array(
+    'name' => t("Recipient"),
+    'description' => t("The recipient of the message. Tokens are only replaced when the user viewing the message is a recipient."),
+    'type' => 'user',
+  );
+
+  return array(
+    'types' => array('privatemsg_message' => $type),
+    'tokens' => array('privatemsg_message' => $message),
+  );
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function privatemsg_tokens($type, $tokens, array $data = array(), array $options = array()) {
+
+  global $user;
+  if (isset($options['language'])) {
+    $url_options['language'] = $options['language'];
+    $language_code = $options['language']->language;
+  }
+  else {
+    $language_code = NULL;
+  }
+
+  $sanitize = !empty($options['sanitize']);
+  $replacements = array();
+  if ($type == 'privatemsg_message' && !empty($data['privatemsg_message'])) {
+    $message = $data['privatemsg_message'];
+
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        // Simple key values on the node.
+        case 'mid':
+          $replacements[$original] = $message->mid;
+          break;
+
+        case 'subject':
+          // Avoid recursion.
+          if (empty($options['privatemsg_recursion'])) {
+            $subject = privatemsg_token_replace($message->subject, $data, $options + array('privatemsg_recursion' => 1));
+          }
+          else {
+            $subject = $message->subject;
+          }
+          $replacements[$original] = $sanitize ? check_plain($subject) : $subject;
+          break;
+
+        // Default values for the chained tokens handled below.
+        case 'author':
+          $replacements[$original] = $sanitize ? filter_xss($message->author->name) : $message->author->name;
+          break;
+
+        case 'recipient':
+            $replacements[$original] = $sanitize ? filter_xss($user->name) : $user->name;
+          break;
+
+        case 'sent':
+          $replacements[$original] = format_date($message->timestamp, 'medium', '', NULL, $language_code);
+          break;
+      }
+    }
+
+    if ($author_tokens = token_find_with_prefix($tokens, 'author')) {
+      $replacements += token_generate('user', $author_tokens, array('user' => $message->author), $options);
+    }
+
+    if ($recipient_tokens = token_find_with_prefix($tokens, 'recipient')) {
+      $replacements += token_generate('user', $recipient_tokens, array('user' => $user), $options);
+    }
+
+    if ($sent_tokens = token_find_with_prefix($tokens, 'sent')) {
+      $replacements += token_generate('date', $sent_tokens, array('date' => $message->timestamp), $options);
+    }
+  }
+
+  return $replacements;
+}
+
+/**
+ * Wrapper function for token_replace() that does not replace the tokens if the
+ * user viewing the message is not a recipient.
+ */
+function privatemsg_token_replace($text, $data, array $options = array()) {
+  global $user;
+
+  if (isset($options['language'])) {
+    $url_options['language'] = $options['language'];
+    $language_code = $options['language']->language;
+  }
+  else {
+    $language_code = NULL;
+  }
+
+  $message = $data['privatemsg_message'];
+
+  // We do not replace tokens if the user viewing the message is the author or
+  // not a real recipient to avoid confusion.
+  $sql = "SELECT 1 FROM {pm_index} WHERE recipient = :uid AND type IN ('hidden', 'user') AND mid = :mid";
+  $args = array(':uid' => $user->uid, ':mid' => $message->mid);
+  if ($message->author->uid == $user->uid || !db_query($sql, $args)->fetchField()) {
+    $replacements = array();
+    $has_tokens = FALSE;
+    foreach (token_scan($text) as $type => $tokens) {
+      // token_replace() returns tokens separated by type.
+      foreach ($tokens as $name => $original) {
+        $replacements[$original] = _privatemsg_token_replacement($type, $name, $original, $language_code);
+      }
+      $has_tokens = TRUE;
+    }
+    $replaced = str_replace(array_keys($replacements), $replacements, $text);
+
+    // If there are any tokens, add a notice that the tokens will be replaced
+    // for the recipient.
+    if (!empty($options['privatemsg-token-notice'])) {
+      $replaced .= '<p class="privatemsg-token-notice">' . t('Note: Valid tokens will be replaced when a recipient is reading this message.') . '</p>';
+    }
+
+    return $replaced;
+  }
+
+  // If the user is a recipient, use default token_replace() function.
+  return token_replace($text, $data, $options);
+}
+
+/**
+ * Checks if a token is valid.
+ */
+function _privatemsg_token_replacement($type, $name, $original, $language_code) {
+  $info = token_info();
+
+  // Check this is a direct token.
+  if (strpos($name, ':') === FALSE) {
+    // Make sure this is a valid token.
+    if (isset($info['tokens'][$type][$name])) {
+      return t('< Token @token >', array('@token' => $original), array('langcode' => $language_code));
+    }
+    else {
+      return t('< INVALID TOKEN @token >', array('@token' => $original), array('langcode' => $language_code));
+    }
+  }
+  else {
+    // This is part of a complex token, call recursive with updated name and
+    // type.
+    $new_type = substr($name, 0, strpos($name, ':'));
+    if (!isset($info['tokens'][$type][$new_type]['type'])) {
+      return t('< INVALID TOKEN @token >', array('@token' => $original), array('langcode' => $language_code));
+    }
+    $new_type = $info['tokens'][$type][$new_type]['type'];
+    $new_name = substr($name, strpos($name, ':') + 1);
+    return _privatemsg_token_replacement($new_type, $new_name, $original, $language_code);
+  }
 }
\ No newline at end of file
Index: privatemsg.pages.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/privatemsg.pages.inc,v
retrieving revision 1.1.2.23
diff -u -p -r1.1.2.23 privatemsg.pages.inc
--- privatemsg.pages.inc	12 Aug 2010 22:34:46 -0000	1.1.2.23
+++ privatemsg.pages.inc	15 Aug 2010 14:01:31 -0000
@@ -300,9 +300,20 @@ function privatemsg_new($form, &$form_st
     '#access'   => (privatemsg_user_access('write privatemsg') || privatemsg_user_access('reply only privatemsg')) && (!empty($recipients_string) || !$thread_id),
   );
   if (isset($form_state['privatemsg_preview'])) {
+
+    $preview_subject = '';
+    // Only display subject on preview for new messages.
+    if (empty($form_state['validate_built_message']->thread_id)) {
+      $preview_subject = check_plain($form_state['validate_built_message']->subject);
+      // If message has tokens, replace them.
+      if ($form_state['validate_built_message']->has_tokens) {
+        $preview_subject = privatemsg_token_replace($preview_subject, array('privatemsg_message' => $form_state['validate_built_message']));
+      }
+    }
+
     $form['message_header'] = array(
       '#type' => 'fieldset',
-      '#title' => empty($form_state['validate_built_message']->thread_id) ? check_plain($form_state['validate_built_message']->subject) : t('Preview'),
+      '#title' => !empty($preview_subject) ? $preview_subject : t('Preview'),
       '#attributes' => array('class' => array('preview')),
       '#weight' => -20,
     );
@@ -357,6 +368,13 @@ function privatemsg_new($form, &$form_st
     '#resizable'          => TRUE,
     '#format'             => isset($format) ? $format : NULL,
   );
+  if (privatemsg_user_access('use tokens in privatemsg') && module_exists('token')) {
+    $form['token'] = array(
+      '#theme' => 'token_tree',
+      '#weight' => 99,
+      '#token_types' => array('privatemsg_message'),
+    );
+  }
   $form['actions'] = array('#type' => 'actions');
   $form['actions']['preview'] = array(
     '#type'               => 'submit',
Index: privatemsg.theme.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/privatemsg.theme.inc,v
retrieving revision 1.1.2.7.2.10
diff -u -p -r1.1.2.7.2.10 privatemsg.theme.inc
--- privatemsg.theme.inc	20 Jul 2010 18:40:21 -0000	1.1.2.7.2.10
+++ privatemsg.theme.inc	15 Aug 2010 14:01:31 -0000
@@ -99,7 +99,12 @@ function theme_privatemsg_list_field__su
     $is_new = theme('mark', array('type' => MARK_NEW));
     $options['fragment'] = 'new';
   }
-  $field['data'] = l($thread['subject'], 'messages/view/' . $thread['thread_id'], $options) . $is_new;
+  $subject = $thread['subject'];
+  if ($thread['has_tokens']) {
+    $message = privatemsg_message_load($thread['thread_id']);
+    $subject = privatemsg_token_replace($subject, array('privatemsg_message' => $message));
+  }
+  $field['data'] = l($subject, 'messages/view/' . $thread['thread_id'], $options) . $is_new;
   $field['class'][] = 'privatemsg-list-subject';
   return $field;
 }
Index: styles/privatemsg-view.css
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/styles/privatemsg-view.css,v
retrieving revision 1.1.2.2.2.4
diff -u -p -r1.1.2.2.2.4 privatemsg-view.css
--- styles/privatemsg-view.css	22 Mar 2010 07:29:18 -0000	1.1.2.2.2.4
+++ styles/privatemsg-view.css	15 Aug 2010 14:01:31 -0000
@@ -61,4 +61,8 @@
   margin: 20px 0 20px 200px;
   padding-left: 10px;
   width: 300px;
+}
+
+p.privatemsg-token-notice {
+  font-size: small;
 }
\ No newline at end of file
