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.86
diff -u -p -r1.70.2.30.2.91.2.64.2.86 privatemsg.module
--- privatemsg.module	9 Sep 2010 08:27:59 -0000	1.70.2.30.2.91.2.64.2.86
+++ privatemsg.module	4 Oct 2010 20:09:57 -0000
@@ -211,8 +211,8 @@ function privatemsg_menu() {
     'menu_name'        => 'user-menu',
   );
   // Auto-completes available user names & removes duplicates.
-  $items['messages/user-name-autocomplete'] = array(
-    'page callback'    => 'privatemsg_user_name_autocomplete',
+  $items['messages/autocomplete'] = array(
+    'page callback'    => 'privatemsg_autocomplete',
     'file'             => 'privatemsg.pages.inc',
     'access callback'  => 'privatemsg_user_access',
     'access arguments' => array('write privatemsg'),
@@ -659,6 +659,11 @@ function privatemsg_theme() {
       'path'                  => drupal_get_path('module', 'privatemsg'),
       'variables'             => array('count'),
     ),
+    'privatemsg_username'  => array(
+      'file'                  => 'privatemsg.theme.inc',
+      'path'                  => drupal_get_path('module', 'privatemsg'),
+      'variables'             => array('recipient' => NULL, 'options' => array()),
+    ),
   );
   // Include the theme file to load the theme suggestions.
   module_load_include('inc', 'privatemsg', 'privatemsg.theme');
@@ -845,51 +850,89 @@ function _privatemsg_parse_userstring($i
   // Start working through the input array.
   $invalid = array();
   $recipients = array();
+  $duplicates = array();
+  $denieds = array();
   foreach ($input as $string) {
     $string = trim($string);
-    if (!empty($string)) { // We don't care about white space names.
-      // First, check if another module is able to resolve the string into an
-      // user object.
+    // Ignore spaces.
+    if (!empty($string)) {
+
+      // First, collect all matches.
+      $matches = array();
+
+      // Remember if a possible match denies access.
+      $access_denied = FALSE;
 
       // Load recipient types.
       $types = privatemsg_recipient_get_types();
 
+      // Collect matches from hook implementations.
       foreach (module_implements('privatemsg_name_lookup') as $module) {
         $function = $module . '_privatemsg_name_lookup';
-        if (($recipient = $function($string)) && is_object($recipient)) {
-          // Default to recipient type user.
-          if (!isset($recipient->type)) {
-            $recipient->type = 'user';
-            $recipient->recipient = $recipient->uid;
-          }
-          // Only add recipient if user has the permission.
-          if (privatemsg_recipient_access($recipient->type, 'write', $recipient)) {
-            $recipients[privatemsg_recipient_key($recipient)] = $recipient;
-            continue 2;
-          }
-          else {
-            continue;
-          }
+        $return = $function($string);
+        if (isset($return) && is_array($return)) {
+          $matches += $return;
         }
       }
 
-      // Remove optonal user specifier.
-      $string = trim(str_replace(t('[user]'), '', $string));
-      // Fall back to the default username lookup.
-      if (!$error = module_invoke('user', 'validate_name', $string)) {
-        // String is a valid username, look it up.
-        if ($recipient = user_load_by_name($string)) {
-          $recipient->recipient = $recipient->uid;
-          $recipient->type = 'user';
-          $recipients[privatemsg_recipient_key($recipient)] = $recipient;
-          continue;
+      // Check permissions, remove any recipients the user doesn't have write
+      // access for.
+      foreach ($matches as $key => $recipient) {
+        if (!privatemsg_recipient_access($recipient->type, 'write', $recipient)) {
+          unset($matches[$key]);
+          $access_denied = TRUE;
         }
       }
-      $invalid[$string] = $string;
+
+      // Allow modules to alter the found matches.
+      drupal_alter('privatemsg_name_lookup_matches', $matches, $string);
+
+      // Check if there are any matches.
+      $number_of_matches = count($matches);
+      switch ($number_of_matches) {
+        case 1:
+          // Only a single match found, add to recipients.
+          $recipients += $matches;
+          break;
+        case 0:
+          // No match found, check if access was denied.
+          if ($access_denied) {
+            // There were possible matches, but access was denied.
+            $denieds[$string] = $string;
+          }
+          else {
+            // The string does not contain any valid recipients.
+            $invalid[$string] = $string;
+          }
+          break;
+
+        default:
+          // Multiple matches were found. The user has to specify which one he
+          // meant.
+          $duplicates[$string] = $matches;
+          break;
+      }
     }
   }
+  // Todo: Provide better API.
+  return array($recipients, $invalid, $duplicates, $denieds);
+}
 
-  return array($recipients, $invalid);
+/**
+ * Implements hook_privatemsg_name_lookup().
+ */
+function privatemsg_privatemsg_name_lookup($string) {
+  // Remove optonal user specifier.
+  $string = trim(str_replace(t('[user]'), '', $string));
+  // Fall back to the default username lookup.
+  if (!$error = module_invoke('user', 'validate_name', $string)) {
+    // String is a valid username, look it up.
+    if ($recipient = user_load_by_name($string)) {
+      $recipient->recipient = $recipient->uid;
+      $recipient->type = 'user';
+      return array(privatemsg_recipient_key($recipient) => $recipient);
+    }
+  }
 }
 
 /**
@@ -1067,7 +1110,7 @@ function privatemsg_sql_unread_count($ac
 
 function privatemsg_sql_autocomplete($search, $names) {
   $query = db_select('users', 'u')
-    ->fields('u', array('name'))
+    ->fields('u', array('uid'))
     ->condition('u.name', $search . '%', 'LIKE')
     ->condition('u.status', 0, '<>')
     ->where('NOT EXISTS (SELECT 1 FROM {pm_disable} pd WHERE pd.uid=u.uid)')
@@ -1632,7 +1675,7 @@ function _privatemsg_validate_message(&$
 
   if (empty($message->recipients) || !is_array($message->recipients)) {
     if ($form) {
-      form_set_error('to', t('You must include at least one valid recipient.'));
+      form_set_error('recipient', t('You must include at least one valid recipient.'));
     }
     else {
       $messages['error'][] = t('At least one valid recipient must be included with the message.');
@@ -1656,7 +1699,7 @@ function _privatemsg_validate_message(&$
   // Check again, give another error message if all recipients are blocked
   if (empty($message->recipients)) {
     if ($form) {
-      form_set_error('to', t('You are not allowed to send this message because all recipients are blocked.'));
+      form_set_error('recipient', t('You are not allowed to send this message because all recipients are blocked.'));
     }
     else {
       $messages['error'][] = t('The message cannot be sent because all recipients are blocked.');
@@ -2481,23 +2524,16 @@ function privatemsg_recipient_access($ty
  *
  * @ingroup types.
  */
-function privatemsg_recipient_format($participant, $options = array()) {
-  if ((!isset($participant->type) || $participant->type == 'user') && (!empty($participant->name) || isset($participant->uid) && $participant->uid === '0')) {
-    if (!isset($participant->uid)) {
-      $participant->uid = $participant->recipient;
-    }
-    if (isset($options['plain']) && $options['plain']) {
-      return $participant->name;
-    }
-    return theme('username', array('account' => $participant));
-  }
-  else {
-    $type = privatemsg_recipient_get_type($participant->type);
-    if (isset($type['format'])) {
-      return theme($type['format'], array('recipient' => $participant, 'options' => $options));
-    }
-    return NULL;
+function privatemsg_recipient_format($recipient, $options = array()) {
+  if (!isset($recipient->type)) {
+    $recipient->type = 'user';
+    $recipient->recipient = $recipient->uid;
+  }
+  $type = privatemsg_recipient_get_type($recipient->type);
+  if (isset($type['format'])) {
+    return theme($type['format'], array('recipient' => $recipient, 'options' => $options));
   }
+  return NULL;
 }
 
 /**
@@ -2509,7 +2545,7 @@ function privatemsg_privatemsg_recipient
       'name' => t('User'),
       'description' => t('Enter a user name to write a message to a user.'),
       'load' => '_privatemsg_user_load',
-      'format' => 'username',
+      'format' => 'privatemsg_username',
       'autocomplete' => 'privatemsg_user_autocomplete',
       // Make sure this comes always last.
       '#weight' => 50,
@@ -2521,10 +2557,26 @@ function privatemsg_privatemsg_recipient
  * Implements callback_recipient_autocomplete().
  */
 function privatemsg_user_autocomplete($fragment, $names, $limit) {
-  return _privatemsg_assemble_query('autocomplete', $fragment, $names)
-    ->range(0, 10)
+  // First, load all possible uid's.
+  $uids = _privatemsg_assemble_query('autocomplete', $fragment, $names)
+    ->range(0, $limit)
     ->execute()
     ->fetchCol();
+  $query = _privatemsg_assemble_query('autocomplete', $fragment, $names);
+  $query->preExecute();
+  $query->getArguments();
+  // Load the corresponding users, make sure to not load any duplicates.
+  $accounts = user_load_multiple(array_unique($uids));
+
+  // Return them in an array with the correct recipient key.
+  $suggestions = array();
+  foreach ($accounts as $account) {
+    $account->type = 'user';
+    $account->recipient = $account->uid;
+    $suggestions[privatemsg_recipient_key($account)] = $account;
+  }
+  return $suggestions;
+
 }
 
 /**
Index: privatemsg.pages.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/privatemsg.pages.inc,v
retrieving revision 1.1.2.26
diff -u -p -r1.1.2.26 privatemsg.pages.inc
--- privatemsg.pages.inc	8 Sep 2010 23:17:31 -0000	1.1.2.26
+++ privatemsg.pages.inc	4 Oct 2010 20:09:59 -0000
@@ -346,7 +346,7 @@ function privatemsg_new($form, &$form_st
       '#required'           => TRUE,
       '#weight'             => -10,
       '#size'               => 50,
-      '#autocomplete_path'  => 'messages/user-name-autocomplete',
+      '#autocomplete_path'  => 'messages/autocomplete',
       // Do not hardcode #maxlength, make it configurable by number of recipients, not their name length.
     );
   }
@@ -452,13 +452,45 @@ function privatemsg_new_validate($form, 
   }
   // Only parse the user string for a new thread.
   if (!isset($message->thread_id)) {
-    list($message->recipients, $invalid) = _privatemsg_parse_userstring($message->recipient);
+    list($message->recipients, $invalid, $duplicates, $denieds) = _privatemsg_parse_userstring($message->recipient);
   }
   else {
     // Load participants. Limit recipients to visible unless read_all is TRUE.
     $message->recipients = _privatemsg_load_thread_participants($message->thread_id, $message->read_all ? FALSE : $message->author);
   }
 
+  if (!empty($invalid)) {
+    // Display information about invalid recipients.
+    drupal_set_message(t('The following users will not receive this private message: @invalid.', array('@invalid' => implode(", ", $invalid))), 'error');
+  }
+  if (!empty($denieds)) {
+    // Display information about denied recipients.
+    drupal_set_message(t('You do not have access to write these recipients: @denieds.', array('@denieds' => implode(", ", $denieds))), 'error');
+  }
+
+  if (!empty($duplicates)) {
+    // Add JS and CSS to allow choosing the recipient.
+    drupal_add_js(drupal_get_path('module', 'privatemsg') . '/privatemsg-alternatives.js');
+
+    // Display information about recipients that couldn't be identified
+    // uniquely.
+    $js_duplicates = array();
+    foreach ($duplicates as $string => $duplicate) {
+      $alternatives = array();
+      foreach ($duplicate as $match) {
+        $formatted_match = privatemsg_recipient_format($match, array('plain' => TRUE, 'unique' => TRUE));
+        $js_duplicates[$formatted_match] = $string;
+        $alternatives[] = '<span class="privatemsg-recipient-alternative">' . $formatted_match . '</span>';
+      }
+      // Build a formatted list of possible recipients.
+      $alternatives = theme('item_list', array('items' => $alternatives, 'attributes' => array('class' => array('action-links'))));
+      form_set_error('recipient', '<span class="privatemsg-alternative-description">' . t('The site has multiple recipients named %string. Please choose your intended recipient: !list', array('%string' => $string, '!list' => $alternatives)) . '</span>');
+    }
+
+    // Also make that information available to the javascript replacement code.
+    drupal_add_js(array('privatemsg_duplicates' => $js_duplicates), 'setting');
+  }
+
   $validated = _privatemsg_validate_message($message, TRUE, !isset($message->thread_id));
   foreach ($validated['messages'] as $type => $texts) {
     foreach ($texts as $text) {
@@ -466,9 +498,6 @@ function privatemsg_new_validate($form, 
     }
   }
   $form_state['validate_built_message'] = $message;
-  if (!empty($invalid)) {
-    drupal_set_message(t('The following users will not receive this private message: @invalid', array('@invalid' => implode(", ", $invalid))), 'error');
-  }
 }
 
 function privatemsg_new_preview($form, &$form_state) {
@@ -560,7 +589,7 @@ function privatemsg_undo_action() {
  *
  * Prevents usernames from being used and/or suggested twice.
  */
-function privatemsg_user_name_autocomplete($string) {
+function privatemsg_autocomplete($string) {
   $names = array();
   // 1: Parse $string and build list of valid user names.
   $fragments = explode(',', $string);
@@ -589,11 +618,32 @@ function privatemsg_user_name_autocomple
       }
     }
   }
+  // Format the suggestions.
+  $themed_matches = array();
+  foreach ($matches as $key => $match) {
+    $themed_matches[$key] = privatemsg_recipient_format($match, array('plain' => TRUE));
+  }
+
+  // Check if there are any duplicates.
+  if (count(array_unique($themed_matches)) != count($themed_matches)) {
+    // Loop over matches, look for duplicates of each one.
+    foreach ($themed_matches as $key => $themed_match) {
+      $duplicate_keys = array_keys($themed_matches, $themed_match);
+      if (count($duplicate_keys) > 1) {
+        // There are duplicates, make them unique.
+        foreach ($duplicate_keys as $duplicate_key) {
+          // Reformat them with unique argument.
+          $themed_matches[$duplicate_key] = privatemsg_recipient_format($matches[$duplicate_key], array('plain' => TRUE, 'unique' => TRUE));
+        }
+      }
+    }
+  }
+
   // Prefix the matches and convert them to the correct form for the
   // autocomplete.
   $prefix = count($names) ? implode(", ", $names) .", " : '';
   $suggestions = array();
-  foreach ($matches as $match) {
+  foreach ($themed_matches as $match) {
     $suggestions[$prefix . $match . ', '] = $match;
   }
 
Index: privatemsg.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/privatemsg.test,v
retrieving revision 1.2.2.1.2.36
diff -u -p -r1.2.2.1.2.36 privatemsg.test
--- privatemsg.test	13 Sep 2010 18:37:22 -0000	1.2.2.1.2.36
+++ privatemsg.test	4 Oct 2010 20:10:04 -0000
@@ -756,22 +756,22 @@ class PrivatemsgTestCase extends DrupalW
     user_save($user2, array('name' => 'aaab'));
     user_save($user3, array('name' => 'bbbb'));
 
-    $json = $this->drupalGet('messages/user-name-autocomplete/aa');
+    $json = $this->drupalGet('messages/autocomplete/aa');
     $autocomplete = (array)json_decode($json);
     $this->assertEqual(count($autocomplete), 2, t('Autocomplete object contains two suggestions.'));
     $this->assertEqual($autocomplete['aaaa, '], 'aaaa');
     $this->assertEqual($autocomplete['aaab, '], 'aaab');
 
-    $json = $this->drupalGet('messages/user-name-autocomplete/bb');
+    $json = $this->drupalGet('messages/autocomplete/bb');
     $autocomplete = (array)json_decode($json);
     $this->assertEqual(count($autocomplete), 1, t('Autocomplete object contains one suggestion.'));
     $this->assertEqual($autocomplete['bbbb, '], 'bbbb');
 
-    $json = $this->drupalGet('messages/user-name-autocomplete/cc');
+    $json = $this->drupalGet('messages/autocomplete/cc');
     $autocomplete = (array)json_decode($json);
     $this->assertEqual(count($autocomplete), 0, t('Autocomplete object contains no suggestions.'));
 
-    $json = $this->drupalGet('messages/user-name-autocomplete/aaaa, a');
+    $json = $this->drupalGet('messages/autocomplete/aaaa, a');
     $autocomplete = (array)json_decode($json);
     $this->assertEqual(count($autocomplete), 1, t('Autocomplete object contains one suggestion.'));
     $this->assertEqual($autocomplete['aaaa, aaab, '], 'aaab');
Index: privatemsg.theme.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/privatemsg.theme.inc,v
retrieving revision 1.1.2.7.2.12
diff -u -p -r1.1.2.7.2.12 privatemsg.theme.inc
--- privatemsg.theme.inc	8 Sep 2010 23:17:31 -0000	1.1.2.7.2.12
+++ privatemsg.theme.inc	4 Oct 2010 20:10:04 -0000
@@ -252,5 +252,28 @@ function theme_privatemsg_new_block($cou
 }
 
 /**
+ * Used to theme and display user recipients.
+ *
+ * Wrapper for theme_username() with a few additional options.
+ */
+function theme_privatemsg_username($variables) {
+  $recipient = $variables['recipient'];
+  $options = $variables['options'];
+  if (!isset($recipient->uid)) {
+    $recipient->uid = $recipient->recipient;
+  }
+  if (!empty($options['plain'])) {
+    $name = $recipient->name;
+    if (!empty($options['unique'])) {
+      $name .= ' [user]';
+    }
+    return $name;
+  }
+  else {
+    return theme('username', array('account' => $recipient));
+  }
+}
+
+/**
  * @}
  */
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.2.28
diff -u -p -r1.1.2.17.2.28 privatemsg_filter.module
--- privatemsg_filter/privatemsg_filter.module	6 Sep 2010 16:04:39 -0000	1.1.2.17.2.28
+++ privatemsg_filter/privatemsg_filter.module	4 Oct 2010 20:10:07 -0000
@@ -114,8 +114,8 @@ function privatemsg_filter_menu() {
     'weight'           => -12,
     'menu_name'        => 'user-menu',
   );
-  $items['messages/filter/user-name-autocomplete'] = array(
-    'page callback'    => 'privatemsg_user_name_autocomplete',
+  $items['messages/filter/autocomplete'] = array(
+    'page callback'    => 'privatemsg_autocomplete',
     'file'             => 'privatemsg.pages.inc',
     'file path'        => drupal_get_path('module', 'privatemsg'),
     'access callback'  => 'privatemsg_user_access',
@@ -345,7 +345,7 @@ function privatemsg_filter_dropdown(&$fo
     '#description'        => t('Separate multiple names with commas.'),
     '#weight'             => -5,
     '#size'               => 50,
-    '#autocomplete_path'  => 'messages/filter/user-name-autocomplete',
+    '#autocomplete_path'  => 'messages/filter/autocomplete',
   );
 
   // Only show form if the user has some messages tagged.
Index: privatemsg_roles/privatemsg_roles.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/privatemsg_roles/privatemsg_roles.module,v
retrieving revision 1.1.2.4
diff -u -p -r1.1.2.4 privatemsg_roles.module
--- privatemsg_roles/privatemsg_roles.module	18 Jul 2010 21:01:07 -0000	1.1.2.4
+++ privatemsg_roles/privatemsg_roles.module	4 Oct 2010 20:10:07 -0000
@@ -70,7 +70,11 @@ function theme_privatemsg_roles_format($
   $role = $variables['recipient'];
   $options = $variables['options'];
   if (!empty($options['plain'])) {
-    return $role->name . ' ' . t('[role]');
+    $name = $role->name;
+    if (!empty($options['unique'])) {
+      $name .= ' [role]';
+    }
+    return $name;
   }
   return t('%role (role)', array('%role' => $role->name));
 }
@@ -111,8 +115,10 @@ function privatemsg_roles_autocomplete($
     ->range(0, $limit)
     ->execute();
   $roles = array();
-  foreach ($result->fetchCol() as $role) {
-    $roles[] = $role . ' ' . t('[role]');
+  foreach ($result as $role) {
+    $role->type = 'role';
+    $role->recipient = $role->rid;
+    $roles[privatemsg_recipient_key($role)] = $role;
   }
   return $roles;
 }
@@ -126,7 +132,7 @@ function privatemsg_roles_privatemsg_nam
   $role = db_query("SELECT *, rid AS recipient FROM {role} WHERE name = :name", array(':name' => trim($string)))->fetchObject();
   if ($role) {
     $role->type = 'role';
-    return $role;
+    return array(privatemsg_recipient_key($role) => $role);
   }
 }
 
@@ -142,7 +148,7 @@ function privatemsg_roles_privatemsg_nam
  */
 function privatemsg_roles_sql_autocomplete_roles($search, $names) {
   $query = db_select('role', 'role')
-    ->fields('role', array('name'))
+    ->fields('role')
     ->condition('role.name', $search . '%', 'LIKE')
     ->condition('role.rid', DRUPAL_ANONYMOUS_RID, '<>')
     ->orderBy('role.name', 'ASC');
Index: privatemsg_roles/privatemsg_roles.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/privatemsg_roles/privatemsg_roles.test,v
retrieving revision 1.1.2.5
diff -u -p -r1.1.2.5 privatemsg_roles.test
--- privatemsg_roles/privatemsg_roles.test	18 Aug 2010 06:40:39 -0000	1.1.2.5
+++ privatemsg_roles/privatemsg_roles.test	4 Oct 2010 20:10:08 -0000
@@ -159,9 +159,9 @@ class PrivatemsgRolesTestCase extends Dr
 
     // Verify autocomplete feature.
     $role_name = user_role_load($user1->roles[5])->name;
-    $json = $this->drupalGet('messages/user-name-autocomplete/' . drupal_substr($role_name, 0, 2));
+    $json = $this->drupalGet('messages/autocomplete/' . drupal_substr($role_name, 0, 2));
     $autocomplete = (array)json_decode($json);
-    $this->assertEqual($autocomplete[$role_name . ' [role], '], $role_name . ' [role]');
+    $this->assertEqual($autocomplete[$role_name . ', '], $role_name);
 
     // Access the form through a url that pre-fills the recipient field.
     $this->drupalGet('messages/new/role_5');
