Bug 955000 - Add someone as a buddy directly from an open conversation. r=florian, a=Standard8
authoraleth <aleth@instantbird.org>
Mon, 16 Jun 2014 13:26:10 +0200
changeset 16339 0f438f2411e7bd076846a542513adc054bd9d2f4
parent 16338 b3c31617885aa10bd1687b50830b7b141a13e141
child 16340 b7824bdf5b5d6c4e958b768b49e4c3b7af5ea4f8
push id10211
push useraleth@instantbird.org
push dateMon, 16 Jun 2014 11:35:03 +0000
treeherdercomm-central@0f438f2411e7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflorian, Standard8
bugs955000
Bug 955000 - Add someone as a buddy directly from an open conversation. r=florian, a=Standard8
im/content/blist.js
im/content/blist.xul
im/content/conversation.xml
im/content/instantbird.xul
im/content/nsContextMenu.js
im/content/tabbrowser.xml
im/locales/en-US/chrome/instantbird/conversation.properties
im/locales/en-US/chrome/instantbird/instantbird.dtd
im/locales/en-US/chrome/instantbird/instantbird.properties
im/modules/ibTagMenu.jsm
--- a/im/content/blist.js
+++ b/im/content/blist.js
@@ -43,16 +43,22 @@ function buddyListContextMenu(aXulMenu) 
 
   [ "context-edit-buddy-separator",
     "context-alias",
     "context-delete",
     "context-tags"
   ].forEach(function (aId) {
     document.getElementById(aId).hidden = hide;
   });
+  if (!hide) {
+    Components.utils.import("resource:///modules/ibTagMenu.jsm");
+    this.tagMenu = new TagMenu(this, window, "context-tags",
+                               this.toggleTag, this.addTag,
+                               this.onBuddy ? this.target.contact : this.target);
+  }
 
   document.getElementById("context-hide-tag").hidden = !this.onGroup;
 
   document.getElementById("context-visible-tags").hidden =
     !hide || this.onConv || !hasVisibleBuddies;
 
   let uiConv;
   if (!hide) {
@@ -84,20 +90,16 @@ function buddyListContextMenu(aXulMenu) 
 
   let detach = document.getElementById("context-detach");
   detach.hidden = !this.onBuddy;
   if (this.onBuddy)
     detach.disabled = this.target.buddy.contact.getBuddies().length == 1;
 
   document.getElementById("context-openconversation").disabled =
     !hide && !this.target.canOpenConversation();
-
-  Components.utils.import("resource:///modules/ibTagMenu.jsm");
-  this.tagMenu = new TagMenu(this, window,
-                             this.onBuddy ? this.target.contact : this.target);
 }
 
 // Prototype for buddyListContextMenu "class."
 buddyListContextMenu.prototype = {
   openConversation: function blcm_openConversation() {
     if (this.onContact || this.onBuddy || this.onConv)
       this.target.openConversation();
   },
--- a/im/content/blist.xul
+++ b/im/content/blist.xul
@@ -88,27 +88,17 @@
                 accesskey="&detachCmd.accesskey;"
                 oncommand="gBuddyListContextMenu.detach();"/>
       <menuitem id="context-delete"
                 label="&deleteCmd.label;"
                 accesskey="&deleteCmd.accesskey;"
                 oncommand="gBuddyListContextMenu.delete();"/>
       <menu id="context-tags"
             label="&tagsCmd.label;"
-            accesskey="&tagsCmd.accesskey;">
-        <menupopup id="context-tags-popup"
-                   oncommand="gBuddyListContextMenu.tagMenu.tag(event, gBuddyListContextMenu.toggleTag.bind(gBuddyListContextMenu));"
-                   onpopupshowing="gBuddyListContextMenu.tagMenu.tagsPopupShowing();">
-          <menuseparator id="context-create-tag-separator"/>
-          <menuitem id="context-create-tag"
-                    label="&addNewTagCmd.label;"
-                    accesskey="&addNewTagCmd.accesskey;"
-                    oncommand="gBuddyListContextMenu.tagMenu.addNewTag(gBuddyListContextMenu.addTag.bind(gBuddyListContextMenu));"/>
-        </menupopup>
-      </menu>
+            accesskey="&tagsCmd.accesskey;"/>
       <menuitem id="context-hide-tag"
                 label="&hideTagCmd.label;"
                 accesskey="&hideTagCmd.accesskey;"
                 oncommand="gBuddyListContextMenu.hideTag();"/>
       <menu id="context-visible-tags"
             label="&visibleTagsCmd.label;"
             accesskey="&visibleTagsCmd.accesskey;">
         <menupopup id="context-visible-tags-popup"
--- a/im/content/conversation.xml
+++ b/im/content/conversation.xml
@@ -1551,18 +1551,39 @@
             let conv = this;
             function createMenuItem(aId, aCommandHandler) {
               let item = document.createElementNS(XUL_NS, "menuitem");
               item.setAttribute("label", bundle.GetStringFromName(aId + ".label"));
               item.setAttribute("accesskey", bundle.GetStringFromName(aId + ".accesskey"));
               item.addEventListener("command", aCommandHandler);
               return item;
             }
+
+            if (!this._conv.isChat && !this._conv.buddy) {
+              let menu = document.createElementNS(XUL_NS, "menu");
+              if (!this._conv.account.connected)
+                menu.setAttribute("disabled", "true");
+              let id = "contextAddContact";
+              menu.setAttribute("id", id)
+              menu.setAttribute("label", bundle.GetStringFromName(id + ".label"));
+              menu.setAttribute("accesskey",
+                                bundle.GetStringFromName(id + ".accesskey"));
+              let conv = this._conv;
+              let addContact = aTag => conv.account.addBuddy(aTag, conv.name);
+              menu.actionOnShowing = function() {
+                Components.utils.import("resource:///modules/ibTagMenu.jsm");
+                menu.tagMenu =
+                  new TagMenu(menu, window, id, addContact, addContact);
+              };
+              items.push(menu);
+            }
+
             let showLogsItem = createMenuItem("contextShowLogs", function() conv.showLogs());
-            showLogsItem.disabled = !this.hasLogs();
+            if (!this.hasLogs())
+              showLogsItem.setAttribute("disabled", "true");
             items.push(showLogsItem);
 
             let hideConvItem = createMenuItem("contextHideConv", function() {
               conv.hide();
               document.getBindingParent(conv).removeTab(conv.tab);
             });
             items.push(hideConvItem);
 
--- a/im/content/instantbird.xul
+++ b/im/content/instantbird.xul
@@ -125,27 +125,17 @@
 
     <menupopup id="contentAreaContextMenu"
                onpopupshowing="if (event.target != this) return true; gContextMenu = new nsContextMenu(this, window.getBrowser()); return gContextMenu.shouldDisplay;"
                onpopuphiding="if (event.target == this &amp;&amp; gContextMenu) { gContextMenu.cleanup(); gContextMenu = null; }">
       <menuitem id="context-nick-openconv"
                 oncommand="gContextMenu.nickOpenConv();"/>
       <menuitem id="context-nick-showlogs"
                 oncommand="gContextMenu.nickShowLogs();"/>
-      <menu id="context-nick-addcontact">
-        <menupopup id="context-tags-popup"
-                   oncommand="gContextMenu.tagMenu.tag(event, gContextMenu.nickAddContact.bind(gContextMenu));"
-                   onpopupshowing="gContextMenu.tagMenu.tagsPopupShowing();">
-          <menuseparator id="context-create-tag-separator"/>
-          <menuitem id="context-create-tag"
-                    label="&addNewTagCmd.label;"
-                    accesskey="&addNewTagCmd.accesskey;"
-                    oncommand="gContextMenu.tagMenu.addNewTag(gContextMenu.nickAddContact.bind(gContextMenu));"/>
-        </menupopup>
-      </menu>
+      <menu id="context-nick-addcontact"/>
       <menuseparator id="context-sep-nick"/>
 
       <menuitem id="context-openlink"
                 label="&openLinkCmd.label;"
                 accesskey="&openLinkCmd.accesskey;"
                 oncommand="gContextMenu.openLink();"/>
       <menuitem id="context-copyemail"
                 label="&copyEmailCmd.label;"
--- a/im/content/nsContextMenu.js
+++ b/im/content/nsContextMenu.js
@@ -173,19 +173,21 @@ nsContextMenu.prototype = {
       // This is a problem e.g. for XMPP MUCs. We require at least that the
       // normalizedChatBuddyName of the nick is normalized like a normalizedName
       // for contacts.
       let normalizedNick = this.conv.target.getNormalizedChatBuddyName(nick);
       if (normalizedNick == account.normalize(normalizedNick) &&
           !Services.contacts.getAccountBuddyByNameAndAccount(normalizedNick, account))
         isAddContact = true;
     }
-    if (isAddContact)
-      this.tagMenu = new TagMenu(this, window);
     addAction("AddContact", isAddContact);
+    if (isAddContact) {
+      this.tagMenu = new TagMenu(this, window, "context-nick-addcontact",
+                                 this.nickAddContact, this.nickAddContact);
+    }
 
     return actions;
   },
   nickOpenConv: function() {
     let name = this.conv.target.getNormalizedChatBuddyName(this.nick);
     let newConv = this.conv.account.createConversation(name);
     Conversations.focusConversation(newConv);
   },
--- a/im/content/tabbrowser.xml
+++ b/im/content/tabbrowser.xml
@@ -179,18 +179,23 @@
             var disabled = this.mTabs.length == 1;
             var multipleTabMenuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple");
             for (let item of multipleTabMenuItems)
               item.disabled = disabled;
             let tabSpecificEndSeparator = document.getElementById("context_tabSpecificEndSeparator");
             if ("getPanelSpecificMenuItems" in this.mContextTab.linkedTabPanel) {
               // Add in tab-specific menu items from the tab panel
               let panelMenuItems = this.mContextTab.linkedTabPanel.getPanelSpecificMenuItems();
-              for (let item of panelMenuItems)
+              for (let item of panelMenuItems) {
                 aPopupMenu.insertBefore(item, tabSpecificEndSeparator);
+                if (item.actionOnShowing) {
+                  item.actionOnShowing();
+                  delete item.actionOnShowing;
+                }
+              }
             }
             return true;
           ]]>
         </body>
       </method>
 
       <method name="tabContextMenuHiding">
         <parameter name="aPopupMenu"/>
--- a/im/locales/en-US/chrome/instantbird/conversation.properties
+++ b/im/locales/en-US/chrome/instantbird/conversation.properties
@@ -1,10 +1,12 @@
 # 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/.
 
+contextAddContact.label=Add Contact…
+contextAddContact.accesskey=A
 contextShowLogs.label=Show Logs
 contextShowLogs.accesskey=L
 contextCloseConv.label=Close Conversation
 contextCloseConv.accesskey=v
 contextHideConv.label=Put Conversation on Hold
 contextHideConv.accesskey=h
--- a/im/locales/en-US/chrome/instantbird/instantbird.dtd
+++ b/im/locales/en-US/chrome/instantbird/instantbird.dtd
@@ -105,18 +105,16 @@
 <!ENTITY aliasCmd.label                "Rename">
 <!ENTITY aliasCmd.accesskey            "R">
 <!ENTITY detachCmd.label               "Detach from contact">
 <!ENTITY detachCmd.accesskey           "D">
 <!ENTITY deleteCmd.label               "Remove">
 <!ENTITY deleteCmd.accesskey           "v">
 <!ENTITY tagsCmd.label                 "Tags…">
 <!ENTITY tagsCmd.accesskey             "T">
-<!ENTITY addNewTagCmd.label            "Add New Tag…">
-<!ENTITY addNewTagCmd.accesskey        "N">
 <!ENTITY showLogsCmd.label             "Show Logs">
 <!ENTITY showLogsCmd.accesskey         "L">
 <!ENTITY hideTagCmd.label              "Hide Tag">
 <!ENTITY hideTagCmd.accesskey          "H">
 <!ENTITY visibleTagsCmd.label          "Visible Tags…">
 <!ENTITY visibleTagsCmd.accesskey      "V">
 <!ENTITY showOfflineContactsCmd.label  "Show Offline Contacts">
 <!ENTITY showOfflineContactsCmd.accesskey "O">
--- a/im/locales/en-US/chrome/instantbird/instantbird.properties
+++ b/im/locales/en-US/chrome/instantbird/instantbird.properties
@@ -3,16 +3,18 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # LOCALIZATION NOTE (contextMenuSearchText): %1$S is the search engine,
 # %2$S is the selection string.
 contextMenuSearchText=Search %1$S for "%2$S"
 contextMenuSearchText.accesskey=S
 contextMenuSearchWith=Search "%S" with…
 
+addNewTagCmd.label=Add New Tag…
+addNewTagCmd.accesskey=N
 newTagPromptTitle=New Tag
 newTagPromptMessage=Please enter the name of the new tag:
 
 #LOCALIZATION NOTE
 # this is used in the addBuddies dialog if the list of existing groups is empty
 defaultGroup=Contacts
 
 #LOCALIZATION NOTE This string appears in a notification bar at the
--- a/im/modules/ibTagMenu.jsm
+++ b/im/modules/ibTagMenu.jsm
@@ -7,36 +7,67 @@ const EXPORTED_SYMBOLS = ["TagMenu"];
 const Cu = Components.utils;
 Cu.import("resource:///modules/imServices.jsm");
 Cu.import("resource:///modules/imXPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "_", function()
   l10nHelper("chrome://instantbird/locale/instantbird.properties")
 );
 
+// aOnTag and aOnAddTag will be called with aParent as the this value.
 // If a contact binding is given in aTarget, the menu checkmarks the existing
 // tags on this contact.
-function TagMenu(aParent, aWindow, aTarget = null) {
+function TagMenu(aParent, aWindow, aMenuId, aOnTag, aOnAddTag, aTarget = null) {
   this.parent = aParent;
-  this.window = aWindow;
-  if (aWindow)
-    this.document = aWindow.document;
+  this.document = aWindow.document;
   this.target = aTarget;
+  this.onAddTag = aOnAddTag;
+  this.onTag = aOnTag;
+
+  // Set up the tag menu at the menu element specified by aMenuId.
+  let document = this.document;
+  let menu = document.getElementById(aMenuId);
+  let popup = menu.firstChild;
+  if (popup)
+    popup.remove();
+  popup = document.createElement("menupopup");
+  this.popup = popup;
+  popup.addEventListener("command", this);
+  popup.addEventListener("popupshowing", this);
+  popup.addEventListener("popuphiding", this);
+  popup.appendChild(document.createElement("menuseparator"));
+  let addTagItem = document.createElement("menuitem");
+  addTagItem.setAttribute("label" , _("addNewTagCmd.label"));
+  addTagItem.setAttribute("accesskey", _("addNewTagCmd.accesskey"));
+  addTagItem.addEventListener("command", this);
+  addTagItem.isAddTagItem = true;
+  popup.appendChild(addTagItem);
+  menu.appendChild(popup);
 }
 TagMenu.prototype = {
-  document: null,
-  window: null,
-  target: null,
-  tagsPopupShowing: function() {
-    if (!this.parent.onContact && !this.parent.onBuddy && !this.parent.onNick)
-      return;
-
-    let popup = this.document.getElementById("context-tags-popup");
+  handleEvent: function(aEvent) {
+    // Don't let events bubble as the tag menu may be a submenu of a context
+    // menu with its own popupshowing handler, and as the command event
+    // on the addTagItem would otherwise bubble to the popup and be handled
+    // again.
+    aEvent.stopPropagation();
+    switch (aEvent.type) {
+      case "command":
+        if (aEvent.target.isAddTagItem)
+          return this.addNewTag(aEvent);
+        return this.tag(aEvent);
+      case "popupshowing":
+        return this.tagsPopupShowing(aEvent);
+      case "popuphiding":
+        return true;
+    }
+  },
+  tagsPopupShowing: function(aEvent) {
     let item;
-    while ((item = popup.firstChild) && item.localName != "menuseparator")
+    while ((item = this.popup.firstChild) && item.localName != "menuseparator")
       item.remove();
 
     if (this.target) {
       var tags = this.target.contact.getTags();
       var groupId = this.target.group.groupId;
     }
 
     let allTags = Services.tags.getTags().reverse();
@@ -48,39 +79,42 @@ TagMenu.prototype = {
       if (this.target) {
         item.setAttribute("type", "checkbox");
         if (tags.some(function(t) t.id == id)) {
           item.setAttribute("checked", "true");
           if (tags.length == 1)
             item.setAttribute("disabled", "true"); // can't remove the last tag.
         }
       }
-      popup.insertBefore(item, popup.firstChild);
+      this.popup.insertBefore(item, this.popup.firstChild);
     }
+    return true;
   },
-  tag: function(aEvent, aCallback) {
+  tag: function(aEvent) {
     let id = aEvent.originalTarget.groupId;
     if (!id)
       return false;
 
     try {
-      return aCallback(Services.tags.getTagById(id));
+      return this.onTag.call(this.parent, Services.tags.getTagById(id));
     } catch(e) {
       Cu.reportError(e);
       return false;
     }
   },
-  addNewTag: function(aCallback) {
+  addNewTag: function(aEvent) {
     let name = {};
-    if (!Services.prompt.prompt(this.window, _("newTagPromptTitle"),
+    if (!Services.prompt.prompt(this.document.defaultView,
+                                _("newTagPromptTitle"),
                                 _("newTagPromptMessage"), name, null,
                                 {value: false}) || !name.value)
       return false; // the user canceled
 
     try {
       // If the tag already exists, createTag will return it.
-      return aCallback(Services.tags.createTag(name.value));
+      return this.onAddTag.call(this.parent,
+                                Services.tags.createTag(name.value));
     } catch(e) {
       Cu.reportError(e);
       return false;
     }
   }
 };