Bug 776609 - Instant Messaging button and menuitem in Address Book should integrate with new IM component. r=florian, a=Standard8. SEAMONKEY_2_12b3_BUILD1 SEAMONKEY_2_12b3_RELEASE
authorMike Conley <mconley@mozilla.com>
Tue, 31 Jul 2012 16:57:56 -0400
changeset 12521 cecb35e420ed6aaee20f070fe383de456a990ad4
parent 12520 f6f3dafe2b14442e4fe1bb404f51b50ed80c3996
child 12522 b7bc8e40aa959483953c9996101ade572f1a6542
child 12524 bdabf0d14c3a339486950a30786b49cd2d179c1e
child 12526 bf0a4d89f903ea2571ce856b9a062ee480d6b14c
push id627
push usermconley@mozilla.com
push dateTue, 31 Jul 2012 20:58:08 +0000
treeherdercomm-beta@cecb35e420ed [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflorian, Standard8
bugs776609
Bug 776609 - Instant Messaging button and menuitem in Address Book should integrate with new IM component. r=florian, a=Standard8.
mail/components/addrbook/content/addressbook.js
mail/components/addrbook/content/addressbook.xul
mail/components/im/content/chat-messenger-overlay.js
mail/components/im/modules/chatHandler.jsm
--- a/mail/components/addrbook/content/addressbook.js
+++ b/mail/components/addrbook/content/addressbook.js
@@ -17,24 +17,33 @@ var gStatusText = null;
 var gQueryURIFormat = null;
 var gCardViewBox;
 var gCardViewBoxEmail1;
 var gPreviousDirTreeIndex = -1;
 
 var msgWindow = Components.classes["@mozilla.org/messenger/msgwindow;1"]
                           .createInstance(Components.interfaces.nsIMsgWindow);
 
+let chatHandler = {};
+Components.utils.import("resource:///modules/chatHandler.jsm", chatHandler);
+
 // Constants that correspond to choices
 // in Address Book->View -->Show Name as
 const kDisplayName = 0;
 const kLastNameFirst = 1;
 const kFirstNameFirst = 2;
 const kLDAPDirectory = 0; // defined in nsDirPrefs.h
 const kPABDirectory  = 2; // defined in nsDirPrefs.h
 
+// These chat properties are the ones that our IM component supports. If a
+// contact has a value for one of these properties, we can communicate with
+// that contact (assuming that the user has added that value to their list
+// of IM contacts).
+const kChatProperties = ["_GoogleTalk", "_JabberId"];
+
 // Note: We need to keep this listener as it does not just handle dir
 // pane deletes but also deletes of address books and lists from places like
 // the sidebar and LDAP preference pane.
 var gAddressBookAbListener = {
   onItemAdded: function(parentDir, item) {
     // will not be called
   },
   onItemRemoved: function(parentDir, item) {
@@ -131,16 +140,20 @@ function OnLoadAddressBook()
       document.documentElement.setAttribute("sizemode", "maximized");
 
     document.documentElement.setAttribute("width", defaultWidth);
     document.documentElement.setAttribute("height", defaultHeight);
     // Make sure we're safe at the left/top edge of screen
     document.documentElement.setAttribute("screenX", screen.availLeft);
     document.documentElement.setAttribute("screenY", screen.availTop);
   }
+
+  if (!chatHandler.ChatCore.initialized)
+    chatHandler.ChatCore.init();
+
   setTimeout(delayedOnLoadAddressBook, 0); // when debugging, set this to 5000, so you can see what happens after the window comes up.
 }
 
 function delayedOnLoadAddressBook()
 {
   verifyAccounts(null, false);   // this will do migration, if we need to.
 
   InitCommonJS();
@@ -167,16 +180,20 @@ function delayedOnLoadAddressBook()
   // to have both.
   MailServices.ab.addAddressBookListener(gAddressBookAbListener,
                                    nsIAbListener.directoryRemoved |
                                    nsIAbListener.directoryItemRemoved);
   MailServices.ab.addAddressBookListener(gDirectoryTreeView, nsIAbListener.all);
 
 
   gDirTree.controllers.appendController(DirPaneController);
+  gAbResultsTree.controllers.appendController(abResultsController);
+  // Force command update for the benefit of DirPaneController and
+  // abResultsController
+  CommandUpdate_AddressBook();
 
   // initialize the customizeDone method on the customizeable toolbar
   var toolbox = document.getElementById("ab-toolbox");
   toolbox.customizeDone = function(aEvent) { MailToolboxCustomizeDone(aEvent, "CustomizeABToolbar"); };
 
   var toolbarset = document.getElementById('customToolbars');
   toolbox.toolbarset = toolbarset;
 
@@ -250,16 +267,17 @@ function onOSXFileMenuInit()
           .setAttribute("checked", AbOSXAddressBookExists());
 }
 
 function CommandUpdate_AddressBook()
 {
   goUpdateCommand('cmd_delete');
   goUpdateCommand('button_delete');
   goUpdateCommand('cmd_newlist');
+  goUpdateCommand('cmd_chatWithCard');
 }
 
 function ResultsPaneSelectionChanged()
 {
   UpdateCardView();
 }
 
 function UpdateCardView()
@@ -613,46 +631,93 @@ function LaunchUrl(url)
   try {
     window.location = url;
   }
   catch (ex) {}
 }
 
 function AbIMSelected()
 {
-  var cards = GetSelectedAbCards();
-  var count = cards.length;
+  let cards = GetSelectedAbCards();
 
-  var screennames;
-  var screennameCount = 0;
+  if (cards.length != 1) {
+    Components.utils.reportError("AbIMSelected should only be called when 1"
+                                 + " card is selected. There are " + cards.length
+                                 + " cards selected.");
+    return;
+  }
+
+  let card = cards[0];
 
-  for (var i=0;i<count;i++) {
-    var screenname = cards[i].getProperty("_AimScreenName", "");
-    if (screenname) {
-      if (screennameCount == 0)
-        screennames = screenname;
-      else
-        screennames += "," + screenname;
+  // We want to open a conversation with the first online username that we can
+  // find. Failing that, we'll take the first offline (but still chat-able)
+  // username we can find.
+  //
+  // First, sort the IM usernames into two groups - online contacts go into
+  // the "online" group, and offline (but chat-able) contacts go into the
+  // "offline" group.
 
-      screennameCount++
+  let online = [];
+  let offline = [];
+
+  for each (let [, chatProperty] in Iterator(kChatProperties)) {
+    let chatID = card.getProperty(chatProperty, "");
+
+    if (chatID && (chatID in chatHandler.allContacts)) {
+      let chatContact = chatHandler.allContacts[chatID];
+      if (chatContact.online)
+        online.push(chatContact);
+      else if (chatContact.canSendMessage)
+        offline.push(chatContact);
     }
   }
 
-  var url = "aim:";
+  let selectedContact;
+
+  // If we have any contacts in the online group, we'll take the first one.
+  if (online.length)
+    selectedContact = online[0];
+  // If not, we'll take the first contact in the offline group.
+  else if (offline.length)
+    selectedContact = offline[0];
 
-  if (screennameCount == 0)
-    url += "goim";
-  else if (screennameCount == 1)
-    url += "goim?screenname=" + screennames;
-  else {
-    url += "SendChatInvite?listofscreennames=" + screennames;
-    url += "&message=" + gAddressBookBundle.getString("joinMeInThisChat");
+  // If we found a contact we can chat with, open / focus the chat tab with
+  // a conversation opened with that contact.
+  if (selectedContact) {
+    let prplConv = selectedContact.createConversation();
+    let uiConv = Services.conversations.getUIConversation(prplConv);
+    let win = Services.wm.getMostRecentWindow("mail:3pane");
+
+    if (win) {
+      win.focus();
+      win.showChatTab();
+      win.chatHandler.focusConversation(uiConv);
+    }
+    else {
+      window.openDialog("chrome://messenger/content/", "_blank",
+                        "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar",
+                        null, {tabType: "chat",
+                               tabParams: {convType: "focus", conv: uiConv}});
+    }
+
+    return;
   }
 
-  LaunchUrl(url);
+  // Ok, if we get here, we're going the old route of trying to use AIM.
+  let AIM = card.getProperty("_AimScreenName", "");
+  if (AIM) {
+    LaunchUrl("aim:goim?screenname=" + AIM);
+    return;
+  }
+
+  // And if we got here, that means we couldn't find *any* usernames we could
+  // chat with. That really shouldn't be possible, since the isEnabled for
+  // cmd_chatWithCard makes checks for this sort of thing, but we'll throw
+  // an exception for good measure.
+  throw new Error("Couldn't find any usernames to chat with for this card.");
 }
 
 function getMailToolbox()
 {
   return document.getElementById("ab-toolbox");
 }
 
 const kOSXDirectoryURI = "moz-abosxdirectory:///";
@@ -685,8 +750,63 @@ function AbShowHideOSXAddressBook()
   if (AbOSXAddressBookExists())
     MailServices.ab.deleteAddressBook(kOSXDirectoryURI);
   else {
     MailServices.ab.newAddressBook(
       gAddressBookBundle.getString(kOSXPrefBase + ".description"),
       kOSXDirectoryURI, 3, kOSXPrefBase);
   }
 }
+
+let abResultsController = {
+  commands: {
+    cmd_chatWithCard: {
+      isEnabled: function() {
+        let selected = GetSelectedAbCards();
+
+        if (selected.length != 1)
+          return false;
+
+        let selectedCard = selected[0];
+
+        let isIMContact = kChatProperties.some(function(aProperty) {
+          let contactName = selectedCard.getProperty(aProperty, "");
+
+          if (!contactName)
+            return false;
+
+          return (contactName in chatHandler.allContacts
+                  && chatHandler.allContacts[contactName].canSendMessage);
+        });
+
+        let hasAIM = selectedCard.getProperty("_AimScreenName", "");
+
+        return isIMContact || hasAIM;
+      },
+
+      doCommand: function() {
+        AbIMSelected();
+      },
+    }
+  },
+
+  supportsCommand: function(aCommand) {
+    return (aCommand in this.commands);
+  },
+
+  isCommandEnabled: function(aCommand) {
+    if (!this.supportsCommand(aCommand))
+      return false;
+
+    return this.commands[aCommand].isEnabled();
+  },
+
+  doCommand: function(aCommand) {
+    if (!this.supportsCommand(aCommand))
+      return;
+    let cmd = this.commands[aCommand];
+    if (!cmd.isEnabled())
+      return;
+    cmd.doCommand();
+  },
+
+  onEvent: function(aEvent) {}
+}
--- a/mail/components/addrbook/content/addressbook.xul
+++ b/mail/components/addrbook/content/addressbook.xul
@@ -97,16 +97,17 @@
 #ifdef XP_MACOSX
     <!-- Mac Window menu -->
     <command id="minimizeWindow" label="&minimizeWindow.label;" oncommand="window.minimize();"/>
     <command id="zoomWindow" label="&zoomWindow.label;" oncommand="zoomWindow();"/>
     <command id="Tasks:Mail" oncommand="toMessengerWindow();"/>
 #endif
     <command id="cmd_CustomizeABToolbar"
              oncommand="CustomizeMailToolbar('ab-toolbox', 'CustomizeABToolbar')"/>
+    <command id="cmd_chatWithCard" oncommand="goDoCommand('cmd_chatWithCard')" disabled="true" />
 </commandset>
 
 <broadcasterset id="abBroadcasters">
   <!-- Edit Menu -->
   <broadcaster id="button_delete" disabled="true"/>
 </broadcasterset>
 
 <keyset id="tasksKeys">
@@ -197,17 +198,17 @@
   <menuseparator/>
   <menuitem id="abResultsTreeContext-newmessage"
             label="&newmsgButton.label;"
             accesskey="&newmsgButton.accesskey;"
             oncommand="AbNewMessage()"/>
   <menuitem id="abResultsTreeContext-newim"
             label="&newIM.label;"
             accesskey="&newIM.accesskey;"
-            oncommand="AbIMSelected()"/>
+            command="cmd_chatWithCard"/>
   <menuseparator/>
   <menuitem id="abResultsTreeContext-delete"
             label="&deleteButton2.label;"
             accesskey="&deleteButton2.accesskey;"
             observes="button_delete"
             oncommand="goDoCommand('button_delete');"/>
 </menupopup>
 
@@ -553,17 +554,17 @@
                      label="&newContactButton.label;" oncommand="AbNewCard();"
                      tooltiptext="&newContactButton.tooltip;"/>
       <toolbarbutton class="toolbarbutton-1" id="button-newlist" label="&newlistButton.label;" tooltiptext="&newlistButton.tooltip;" command="cmd_newlist"/>
       <toolbarbutton class="toolbarbutton-1" id="button-editcard"
                      label="&editButton2.label;"
                      tooltiptext="&editButton2.tooltip;"
                      oncommand="goDoCommand('button_edit');"/>
       <toolbarbutton class="toolbarbutton-1" id="button-newmessage" label="&newmsgButton.label;" tooltiptext="&newmsgButton.tooltip;" oncommand="AbNewMessage();"/>
-      <toolbarbutton class="toolbarbutton-1" id="button-newim" label="&newIM.label;" tooltiptext="&newIM.tooltip;" oncommand="AbIMSelected();"/>
+      <toolbarbutton class="toolbarbutton-1" id="button-newim" label="&newIM.label;" tooltiptext="&newIM.tooltip;" command="cmd_chatWithCard"/>
       <toolbarbutton class="toolbarbutton-1" id="button-abdelete"
                      observes="button_delete" label="&deleteButton2.label;"
                      tooltiptext="&deleteButton2.tooltip;"
                      oncommand="goDoCommand('button_delete');"/>
 
       <toolbaritem id="search-container" title="&searchItem.title;"
                    align="center" flex="1"
                    class="chromeclass-toolbar-additional">
--- a/mail/components/im/content/chat-messenger-overlay.js
+++ b/mail/components/im/content/chat-messenger-overlay.js
@@ -906,22 +906,20 @@ var chatHandler = {
         notification.close();
       return;
     }
   },
   initAfterChatCore: function() {
     let onGroup = document.getElementById("onlinecontactsGroup");
     let offGroup = document.getElementById("offlinecontactsGroup");
 
-    imServices.tags.getTags().forEach(function (aTag) {
-      aTag.getContacts().forEach(function(aContact) {
-        let group = aContact.online ? onGroup : offGroup;
-        group.addContact(aContact);
-      });
-    });
+    for each (let contact in chatHandler.allContacts) {
+      let group = contact.online ? onGroup : offGroup;
+      group.addContact(contact);
+    }
 
     onGroup._updateGroupLabel();
     offGroup._updateGroupLabel();
 
     ["new-text", "new-ui-conversation", "ui-conversation-closed",
      "contact-signed-on", "contact-signed-off",
      "contact-added", "contact-removed", "contact-no-longer-dummy",
      "account-connected", "account-disconnected",
--- a/mail/components/im/modules/chatHandler.jsm
+++ b/mail/components/im/modules/chatHandler.jsm
@@ -1,32 +1,35 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const EXPORTED_SYMBOLS = ["onlineContacts", "ChatCore"];
+const EXPORTED_SYMBOLS = ["allContacts", "onlineContacts", "ChatCore"];
 
 Components.utils.import("resource:///modules/imServices.jsm");
 Components.utils.import("resource:///modules/iteratorUtils.jsm");
 
-var onlineContacts = {};
+let allContacts = {};
+let onlineContacts = {};
 
-var ChatCore = {
+let ChatCore = {
   initialized: false,
   _initializing: false,
   init: function() {
     if (this._initializing)
       return;
     this._initializing = true;
 
     Components.utils.import("resource:///modules/index_im.js");
 
     Services.obs.addObserver(this, "browser-request", false);
     Services.obs.addObserver(this, "contact-signed-on", false);
     Services.obs.addObserver(this, "contact-signed-off", false);
+    Services.obs.addObserver(this, "contact-added", false);
+    Services.obs.addObserver(this, "contact-removed", false);
 
     // The initialization of the im core may trigger a master password prompt,
     // so wrap it with the async prompter service.
     Components.classes["@mozilla.org/messenger/msgAsyncPrompter;1"]
               .getService(Components.interfaces.nsIMsgAsyncPrompter)
               .queueAsyncAuthPrompt("im", false, {
       onPromptStart: function() {
         Services.core.init();
@@ -54,16 +57,24 @@ var ChatCore = {
           inServer.wrappedJSObject.imAccount = account;
           let acc = mgr.createAccount();
           // Avoid new folder notifications.
           inServer.valid = false;
           acc.incomingServer = inServer;
           inServer.valid = true;
           mgr.notifyServerLoaded(inServer);
         }
+
+        Services.tags.getTags().forEach(function (aTag) {
+          aTag.getContacts().forEach(function(aContact) {
+            let name = aContact.preferredBuddy.normalizedName;
+            allContacts[name] = aContact;
+          });
+        });
+
         ChatCore.initialized = true;
         Services.obs.notifyObservers(null, "chat-core-initialized", null);
         ChatCore._initializing = false;
         return true;
       },
       onPromptAuthAvailable: function() { },
       onPromptCanceled: function() { }
     });
@@ -80,10 +91,20 @@ var ChatCore = {
       onlineContacts[aSubject.preferredBuddy.normalizedName] = aSubject;
       return;
     }
 
     if (aTopic == "contact-signed-off") {
       delete onlineContacts[aSubject.preferredBuddy.normalizedName];
       return;
     }
+
+    if (aTopic == "contact-added") {
+      allContacts[aSubject.preferredBuddy.normalizedName] = aSubject;
+      return;
+    }
+
+    if (aTopic == "contact-removed") {
+      delete allContacts[aSubject.preferredBuddy.normalizedName];
+      return;
+    }
   }
 };