Bug 953867 - Add support for tabs with arbitrary content in the conversation window, r=Mic,florian.
authorNihanth Subramanya <nhnt11@gmail.com>
Fri, 21 Jun 2013 16:36:07 +0530
changeset 22663 e66c41fe9b48559f3e439810465e40afbfd541dd
parent 22662 6382b153ac35277b3c61e9267823122496299b3a
child 22664 1e72434937ef1a2ab24d5d5e7574cb60ff5768c4
push id1225
push userflorian@queze.net
push dateSat, 11 Jan 2014 23:24:55 +0000
treeherdertry-comm-central@1d7aa08cb2d7 [default view] [failures only]
reviewersMic, florian
bugs953867
Bug 953867 - Add support for tabs with arbitrary content in the conversation window, r=Mic,florian.
im/content/buddytooltip.xml
im/content/convZoom.js
im/content/conversation.xml
im/content/instantbird.js
im/content/instantbird.xul
im/content/macgestures.js
im/content/tabbrowser.css
im/content/tabbrowser.xml
im/locales/en-US/chrome/instantbird/conversation.properties
im/locales/en-US/chrome/instantbird/tabbrowser.dtd
im/locales/jar.mn
im/modules/imWindows.jsm
--- a/im/content/buddytooltip.xml
+++ b/im/content/buddytooltip.xml
@@ -400,20 +400,19 @@
 
          let elt = document.tooltipNode;
          // No tooltip for elements that have already been removed.
          if (!elt.parentNode)
            return false;
 
          let localName = elt.localName;
          if (localName == "tab") {
-           let conv = elt.linkedConversation.conv;
-           if (conv)
-             return updateTooltipFromConversation(conv, elt);
-           return false;
+           if (!elt.linkedConversation || !elt.linkedConversation.conv)
+             return false;
+           return updateTooltipFromConversation(elt.linkedConversation.conv, elt);
          }
 
          if (localName == "conv")
            return updateTooltipFromConversation(elt.conv, elt);
 
          if (localName == "buddy")
            return updateTooltipFromBuddy(elt.buddy.preferredAccountBuddy, elt);
 
--- a/im/content/convZoom.js
+++ b/im/content/convZoom.js
@@ -76,16 +76,19 @@ var FullZoom = {
    * Set the zoom level for the current browser.
    *
    * Per nsPresContext::setFullZoom, we can set the zoom to its current value
    * without significant impact on performance, as the setting is only applied
    * if it differs from the current setting.  In fact getting the zoom and then
    * checking ourselves if it differs costs more.
    **/
   applyPrefValue: function FullZoom_applyPrefValue() {
+    // If there's no browser (non-conversation tabs), don't do anything.
+    if (!getBrowser())
+      return;
     let value = parseFloat(Services.prefs.getCharPref(FullZoom.prefName));
     if (isNaN(value))
       value = 1;
     else if (value < ZoomManager.MIN)
       value = ZoomManager.MIN;
     else if (value > ZoomManager.MAX)
       value = ZoomManager.MAX;
     ZoomManager.zoom = value;
--- a/im/content/conversation.xml
+++ b/im/content/conversation.xml
@@ -142,16 +142,20 @@
         ]]>
         </body>
       </method>
 
       <method name="finishImport">
         <parameter name="aConversation"/>
         <body>
         <![CDATA[
+          // Swap the docshells.
+          this.browser.swapDocShells(aConversation.browser);
+          // Ensure observers are removed.
+          aConversation.conv = null;
           this.editor.value = aConversation.editor.value;
           this.browser.browserResize();
           this.updateTyping();
           this.inputValueChanged();
           this.loaded = true;
           Services.obs.notifyObservers(this.browser, "conversation-loaded", null);
         ]]>
         </body>
@@ -913,17 +917,17 @@
             input.style.overflowY = "";
             // Set it to the maximum possible value.
             deck.height = oldDeckHeight + (topSize - topMinSize);
           }
         ]]>
         </body>
       </method>
 
-      <method name="onConvResize">
+      <method name="onResize">
         <parameter name="event"/>
         <body>
         <![CDATA[
           let splitter = this.getElt("splitter-bottom");
           let textbox = this.editor;
 
           if (!splitter.hasAttribute("state")) {
             this.calculateTextboxDefaultHeight();
@@ -1472,25 +1476,95 @@
 
           this.tab.removeAttribute("unread");
           this.tab.removeAttribute("attention");
           this._conv.markAsRead();
         ]]>
         </body>
       </method>
 
+      <method name="switchingToPanel">
+        <body>
+        <![CDATA[
+          if (this._visibleTimer)
+            return;
+
+          // Start a timer to detect if the tab has been visible to the
+          // user for long enough to actually be seen (as opposed to the
+          // tab only being visible "accidentally in passing").
+          delete this._wasVisible;
+          this._visibleTimer = setTimeout(function() {
+            this._wasVisible = true;
+            delete this._visibleTimer;
+          }.bind(this), 1000);
+        ]]>
+        </body>
+      </method>
+
       <method name="focus">
         <body>
         <![CDATA[
           this.editor.focus();
-          this.onSelect();
         ]]>
         </body>
       </method>
 
+      <method name="switchingAwayFromPanel">
+        <body>
+          <![CDATA[
+            if (this._visibleTimer) {
+              clearTimeout(this._visibleTimer);
+              delete this._visibleTimer;
+            }
+            // Remove the unread ruler if the tab has been visible without
+            // interruptions for sufficiently long.
+            if (this._wasVisible)
+              this.browser.removeUnreadRuler();
+          ]]>
+        </body>
+      </method>
+
+      <method name="getPanelSpecificMenuItems">
+        <body>
+          <![CDATA[
+            let items = [];
+            const XUL_NS =
+              "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+            let bundle =
+              Services.strings.createBundle("chrome://instantbird/locale/conversation.properties");
+            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;
+            }
+            let showLogsItem = createMenuItem("contextShowLogs", function() conv.showLogs());
+            showLogsItem.disabled = !this.hasLogs();
+            items.push(showLogsItem);
+
+            let hideConvItem = createMenuItem("contextHideConv", function() {
+              conv.hide();
+              document.getBindingParent(conv).removeTab(conv.tab);
+            });
+            items.push(hideConvItem);
+
+            let closeConvItem = createMenuItem("contextCloseConv", function() {
+              conv.close();
+              document.getBindingParent(conv).removeTab(conv.tab);
+            });
+            items.push(closeConvItem);
+
+            return items;
+          ]]>
+        </body>
+      </method>
+
       <method name="hasLogs">
         <body>
         <![CDATA[
           return Services.logs.getLogsForConversation(this.conv).hasMoreElements();
         ]]>
         </body>
       </method>
 
@@ -1658,18 +1732,20 @@
                        .forEach(function(a) this.addBuddy(a.buddy, true), this);
             this.updateParticipantCount();
             if (Services.prefs.getBoolPref("messenger.conversations.showNicks"))
               this.browser.addTextModifier(this.getShowNickModifier());
           }
           else if (this._conv.contact && this._conv.contact.getBuddies().length > 1)
             this.getElt("conv-top-info").allowTargetChange();
 
-          if (this.tab)
+          if (this.tab) {
             this.tab.setAttribute("label", this._conv.title);
+            this.tab.setAttribute("tooltip", "buddyTooltip");
+          }
 
           if (!("Status" in window))
             Components.utils.import("resource:///modules/imStatusUtils.jsm");
           this.updateConvStatus();
           this.initTextboxFormat();
         ]]>
         </body>
       </method>
--- a/im/content/instantbird.js
+++ b/im/content/instantbird.js
@@ -10,17 +10,17 @@ var convWindow = {
     Components.utils.import("resource:///modules/imWindows.jsm");
     Conversations.registerWindow(window);
 
     if ("arguments" in window) {
       while (window.arguments[0] instanceof XULElement) {
         // swap the given tab with the default dummy conversation tab
         // and then close the original tab in the other window.
         let tab = window.arguments.shift();
-        document.getElementById("conversations").importConversation(tab);
+        getTabBrowser().importPanel(tab);
       }
     }
 
     window.addEventListener("unload", convWindow.unload);
     window.addEventListener("resize", convWindow.onresize);
     window.addEventListener("activate", convWindow.onactivate, true);
     window.QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIWebNavigation)
@@ -31,39 +31,43 @@ var convWindow = {
   },
   unload: function mo_unload() {
     Conversations.unregisterWindow(window);
   },
   onactivate: function mo_onactivate(aEvent) {
     Conversations.onWindowFocus(window);
     setTimeout(function () {
       // setting the focus to the textbox just after the window is
-      // activated puts the textbox in an unconsistant state, some
+      // activated puts the textbox in an inconsistent state, some
       // special characters like ^ don't work, so delay the focus
       // operation...
-      getBrowser().selectedConversation.focus();
+      let panel = getTabBrowser().selectedPanel;
+      panel.focus();
+      if ("onSelect" in panel)
+        panel.onSelect();
     }, 0);
   },
   onresize: function mo_onresize(aEvent) {
     if (aEvent.originalTarget != window)
       return;
 
     // Resize each textbox (if the splitter has not been used).
-    let convs = getBrowser().conversations;
-    for each (let conv in convs)
-      conv.onConvResize(aEvent);
+    let panels = getTabBrowser().tabPanels;
+    for (let panel of panels) {
+      if ("onResize" in panel)
+        panel.onResize(aEvent);
+    }
   }
 };
 
 function getConvWindowURL() "chrome://instantbird/content/instantbird.xul"
 
-function getBrowser()
-{
-  return document.getElementById("conversations");
-}
+function getTabBrowser() document.getElementById("conversations")
+
+function getBrowser() getTabBrowser().selectedBrowser
 
 // Copied from mozilla/browser/base/content/browser.js (and simplified)
 var XULBrowserWindow = {
   // Stored Status
   status: "",
   defaultStatus: "",
   jsStatus: "",
   jsDefaultStatus: "",
--- a/im/content/instantbird.xul
+++ b/im/content/instantbird.xul
@@ -27,17 +27,17 @@
 <window
   id     = "convWindow"
   windowtype="Messenger:convs"
   title  = "&convWindow.title;"
   titlemenuseparator="&convWindow.titlemodifiermenuseparator;"
   titlemodifier="&convWindow.titlemodifier;"
   width  = "500"
   height = "600"
-  onclose= "return getBrowser().warnAboutClosingTabs(true);"
+  onclose= "return getTabBrowser().warnAboutClosingTabs(true);"
   persist= "width height screenX screenY"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns  = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script type="application/javascript" src="chrome://instantbird/content/utilities.js"/>
   <script type="application/javascript" src="chrome://instantbird/content/instantbird.js"/>
 #ifdef XP_MACOSX
   <script type="application/javascript" src="chrome://instantbird/content/macgestures.js"/>
 #else
@@ -47,30 +47,37 @@
   <script type="application/javascript" src="chrome://instantbird/content/convZoom.js"/>
   <script type="application/javascript" src="chrome://instantbird/content/nsContextMenu.js"/>
 
 #ifdef XP_MACOSX
 #include menus.xul.inc
 #endif
 
   <commandset id="conversationsCommands">
-    <command id="cmd_close" oncommand="document.getElementById('conversations').removeCurrentTab()"/>
-    <command id="cmd_putOnHold" oncommand="var tabbrowser = document.getElementById('conversations');
-                                           tabbrowser.mCurrentTab.linkedConversation.hide();
-                                           tabbrowser.removeTab(tabbrowser.mCurrentTab);"/>
-    <command id="cmd_showLogs" oncommand="document.getElementById('conversations').mCurrentTab.linkedConversation.showLogs();"/>
-    <command id="cmd_textZoomReduce" oncommand="FullZoom.reduce();"/>
-    <command id="cmd_textZoomEnlarge" oncommand="FullZoom.enlarge();"/>
-    <command id="cmd_textZoomReset" oncommand="FullZoom.reset();"/>
+    <command id="cmd_close" oncommand="getTabBrowser().removeCurrentTab()"/>
+    <command id="cmd_putOnHold"
+             oncommand="var tabbrowser = getTabBrowser();
+                        if (!tabbrowser.selectedConversation) return;
+                        tabbrowser.selectedConversation.hide();
+                        tabbrowser.removeCurrentTab();"/>
+    <command id="cmd_showLogs"
+             oncommand="var conv = getTabBrowser().selectedConversation;
+                        if (conv) conv.showLogs();"/>
+    <command id="cmd_textZoomReduce" oncommand="if (getBrowser()) FullZoom.reduce();"/>
+    <command id="cmd_textZoomEnlarge" oncommand="if (getBrowser()) FullZoom.enlarge();"/>
+    <command id="cmd_textZoomReset" oncommand="if (getBrowser()) FullZoom.reset();"/>
     <command id="cmd_find"
-             oncommand="document.getElementById('conversations').findbar.onFindCommand();"/>
+             oncommand="var conv = getTabBrowser().selectedConversation;
+                        if (conv) conv.findbar.onFindCommand();"/>
     <command id="cmd_findAgain"
-             oncommand="document.getElementById('conversations').findbar.onFindAgainCommand(false);"/>
+             oncommand="var conv = getTabBrowser().selectedConversation;
+                        if (conv) conv.findbar.onFindAgainCommand(false);"/>
     <command id="cmd_findPrevious"
-             oncommand="document.getElementById('conversations').findbar.onFindAgainCommand(true);"/>
+             oncommand="var conv = getTabBrowser().selectedConversation;
+                        if (conv) conv.findbar.onFindAgainCommand(true);"/>
     <commandset id="editMenuCommands"/>
   </commandset>
 
   <keyset id="conversationsKeys">
     <key id="key_close" key="w" modifiers="accel" command="cmd_close"/>
     <key id="key_putOnHold" keycode="VK_ESCAPE" command="cmd_putOnHold"/>
     <key id="key_showLogs" key="h" modifiers="accel,shift" command="cmd_showLogs"/>
     <key id="key_textZoomEnlarge" key="&textEnlarge.commandkey;" command="cmd_textZoomEnlarge" modifiers="accel"/>
@@ -91,38 +98,38 @@
 #define XP_LINUX
 #endif
 #endif
 #ifdef XP_LINUX
 #define NUM_SELECT_TAB_MODIFIER alt
 #else
 #define NUM_SELECT_TAB_MODIFIER accel
 #endif
-#expand    <key id="key_selectTab1" oncommand="getBrowser().selectTabAtIndex(0, event);" key="1" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
-#expand    <key id="key_selectTab2" oncommand="getBrowser().selectTabAtIndex(1, event);" key="2" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
-#expand    <key id="key_selectTab3" oncommand="getBrowser().selectTabAtIndex(2, event);" key="3" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
-#expand    <key id="key_selectTab4" oncommand="getBrowser().selectTabAtIndex(3, event);" key="4" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
-#expand    <key id="key_selectTab5" oncommand="getBrowser().selectTabAtIndex(4, event);" key="5" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
-#expand    <key id="key_selectTab6" oncommand="getBrowser().selectTabAtIndex(5, event);" key="6" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
-#expand    <key id="key_selectTab7" oncommand="getBrowser().selectTabAtIndex(6, event);" key="7" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
-#expand    <key id="key_selectTab8" oncommand="getBrowser().selectTabAtIndex(7, event);" key="8" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
-#expand    <key id="key_selectLastTab" oncommand="getBrowser().selectTabAtIndex(-1, event);" key="9" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand    <key id="key_selectTab1" oncommand="getTabBrowser().selectTabAtIndex(0, event);" key="1" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand    <key id="key_selectTab2" oncommand="getTabBrowser().selectTabAtIndex(1, event);" key="2" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand    <key id="key_selectTab3" oncommand="getTabBrowser().selectTabAtIndex(2, event);" key="3" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand    <key id="key_selectTab4" oncommand="getTabBrowser().selectTabAtIndex(3, event);" key="4" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand    <key id="key_selectTab5" oncommand="getTabBrowser().selectTabAtIndex(4, event);" key="5" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand    <key id="key_selectTab6" oncommand="getTabBrowser().selectTabAtIndex(5, event);" key="6" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand    <key id="key_selectTab7" oncommand="getTabBrowser().selectTabAtIndex(6, event);" key="7" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand    <key id="key_selectTab8" oncommand="getTabBrowser().selectTabAtIndex(7, event);" key="8" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
+#expand    <key id="key_selectLastTab" oncommand="getTabBrowser().selectTabAtIndex(-1, event);" key="9" modifiers="__NUM_SELECT_TAB_MODIFIER__"/>
   </keyset>
 
   <stringbundleset id="stringbundleset">
     <stringbundle id="bundle_instantbird" src="chrome://instantbird/locale/instantbird.properties"/>
   </stringbundleset>
 
   <popupset id="mainPopupSet">
     <tooltip id="aHTMLTooltip"
-             onpopupshowing="return getBrowser().selectedBrowser.FillInHTMLTooltip(document.tooltipNode);"/>
+             onpopupshowing="return getBrowser().FillInHTMLTooltip(document.tooltipNode);"/>
     <tooltip id="buddyTooltip" type="buddy"/>
 
     <menupopup id="contentAreaContextMenu"
-               onpopupshowing="if (event.target != this) return true; gContextMenu = new nsContextMenu(this, window.getBrowser()); return gContextMenu.shouldDisplay;"
+               onpopupshowing="if (event.target != this) return true; gContextMenu = new nsContextMenu(this, window.getTabBrowser()); return gContextMenu.shouldDisplay;"
                onpopuphiding="if (event.target == this &amp;&amp; gContextMenu) { gContextMenu.cleanup(); gContextMenu = null; }">
       <menuitem id="context-openlink"
                 label="&openLinkCmd.label;"
                 accesskey="&openLinkCmd.accesskey;"
                 oncommand="gContextMenu.openLink();"/>
       <menuitem id="context-copyemail"
                 label="&copyEmailCmd.label;"
                 accesskey="&copyEmailCmd.accesskey;"
--- a/im/content/macgestures.js
+++ b/im/content/macgestures.js
@@ -143,24 +143,36 @@ let gGestureSupport = {
         if (this._tabs)
           this._tabs.selectedIndex--;
         break;
       case "twist-right":
         if (this._tabs)
           this._tabs.selectedIndex++;
         break;
       case "swipe-down":
+        // This gesture isn't available if there's no browser.
+        if (!getBrowser())
+          break;
         if (aEvent.originalTarget.ownerDocument == getBrowser().contentDocument)
           getBrowser().contentWindow.focus();
-        getBrowser().selectedBrowser.scrollToNextSection();
+        if (getTabBrowser().selectedConversation)
+          getBrowser().scrollToNextSection();
+        else
+          goDoCommand("cmd_scrollBottom");
         break;
       case "swipe-up":
+        // This gesture isn't available if there's no browser.
+        if (!getBrowser())
+          break;
         if (aEvent.originalTarget.ownerDocument == getBrowser().contentDocument)
           getBrowser().contentWindow.focus();
-        getBrowser().selectedBrowser.scrollToPreviousSection();
+        if (getTabBrowser().selectedConversation)
+          getBrowser().scrollToPreviousSection();
+        else
+          goDoCommand("cmd_scrollTop");
         break;
       case "swipe-left":
       case "swipe-right":
         var newIndex = -1;
         if (this._lastSelectedTab)
           newIndex = this._tabs.getIndexOfItem(this._lastSelectedTab);
         if (newIndex == -1)
           newIndex =
--- a/im/content/tabbrowser.css
+++ b/im/content/tabbrowser.css
@@ -48,8 +48,13 @@ tabconversation {
   display: none;
 }
 
 .tabs-newtab-button,
 #context_newTab,
 #context_newTabSeparator {
   display: none;
 }
+
+/* Ensure two menuseparators aren't shown together when no tab-specific menuitems exist */
+#context_tabSpecificStartSeparator + #context_tabSpecificEndSeparator {
+  display: none;
+}
--- a/im/content/tabbrowser.xml
+++ b/im/content/tabbrowser.xml
@@ -20,82 +20,69 @@
   <binding id="tabbrowser">
     <resources>
       <stylesheet src="chrome://instantbird/content/tabbrowser.css"/>
     </resources>
 
     <content>
       <xul:stringbundle anonid="tbstringbundle" src="chrome://instantbird/locale/tabbrowser.properties"/>
       <xul:tabbox anonid="tabbox" flex="1" eventnode="document" xbl:inherits="handleCtrlPageUpDown"
-                  onselect="if (!('updateCurrentBrowser' in this.parentNode) || event.target.localName != 'tabpanels') return; this.parentNode.updateCurrentBrowser();">
+                  onselect="if (event.target.localName != 'tabpanels') return;
+                            document.getBindingParent(this).updateCurrentTab();">
         <xul:hbox class="tab-drop-indicator-bar" collapsed="true" chromedir="&locale.dir;"
-                  ondragover="this.parentNode.parentNode._onDragOver(event);"
-                  ondragleave="this.parentNode.parentNode._onDragLeave(event);"
-                  ondrop="this.parentNode.parentNode._onDrop(event);">
+                  ondragover="document.getBindingParent(this)._onDragOver(event);"
+                  ondragleave="document.getBindingParent(this)._onDragLeave(event);"
+                  ondrop="document.getBindingParent(this)._onDrop(event);">
           <xul:hbox class="tab-drop-indicator" mousethrough="always"/>
         </xul:hbox>
         <xul:hbox class="tabbrowser-strip" collapsed="true"
-                  tooltip="buddyTooltip" context="_child"
+                  context="_child"
                   anonid="strip"
-                  ondragstart="this.parentNode.parentNode._onDragStart(event);"
-                  ondragover="this.parentNode.parentNode._onDragOver(event);"
-                  ondrop="this.parentNode.parentNode._onDrop(event);"
-                  ondragend="this.parentNode.parentNode._onDragEnd(event);"
-                  ondragleave="this.parentNode.parentNode._onDragLeave(event);">
-          <xul:menupopup id="tabContextMenu" onpopupshowing="return this.parentNode.parentNode.parentNode.updatePopupMenu(this);">
+                  ondragstart="document.getBindingParent(this)._onDragStart(event);"
+                  ondragover="document.getBindingParent(this)._onDragOver(event);"
+                  ondrop="document.getBindingParent(this)._onDrop(event);"
+                  ondragend="document.getBindingParent(this)._onDragEnd(event);"
+                  ondragleave="document.getBindingParent(this)._onDragLeave(event);">
+          <xul:menupopup id="tabContextMenu" onpopupshowing="return document.getBindingParent(this).tabContextMenuShowing(this);"
+                  onpopuphiding="return document.getBindingParent(this).tabContextMenuHiding(this)">
             <xul:menuitem id="context_newTab" label="&newTab.label;" accesskey="&newTab.accesskey;"
                           xbl:inherits="oncommand=onnewtab"/>
             <xul:menuseparator id="context_newTabSeparator"/>
             <xul:menuitem id="context_openTabInWindow" label="&openTabInNewWindow.label;"
                           accesskey="&openTabInNewWindow.accesskey;"
                           tbattr="tabbrowser-multiple"
-                          oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode;
+                          oncommand="var tabbrowser = document.getBindingParent(this);
                                      tabbrowser.replaceTabsWithWindow([tabbrowser.mContextTab]);"/>
-            <xul:menuseparator/>
-            <xul:menuitem id="context_showLogs" label="&showLogs.label;" accesskey="&showLogs.accesskey;"
-                          oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode;
-                                     tabbrowser.mContextTab.linkedConversation.showLogs();"/>
-            <xul:menuseparator/>
-            <xul:menuitem id="context_closeConv" label="&closeConv.label;" accesskey="&closeConv.accesskey;"
-                          oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode;
-                                     tabbrowser.mContextTab.linkedConversation.close();
-                                     tabbrowser.removeTab(tabbrowser.mContextTab);"/>
-            <xul:menuitem id="context_hideConv" label="&hideConv.label;" accesskey="&hideConv.accesskey;"
-                          oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode;
-                                     tabbrowser.mContextTab.linkedConversation.hide();
-                                     tabbrowser.removeTab(tabbrowser.mContextTab);"/>
-            <xul:menuseparator/>
+            <xul:menuseparator id="context_tabSpecificStartSeparator"/>
+            <xul:menuseparator id="context_tabSpecificEndSeparator"/>
             <xul:menuitem id="context_closeOtherTabs" label="&closeOtherTabs.label;" accesskey="&closeOtherTabs.accesskey;"
                           tbattr="tabbrowser-multiple"
-                          oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode;
+                          oncommand="var tabbrowser = document.getBindingParent(this);
                                      tabbrowser.removeAllTabsBut(tabbrowser.mContextTab);"/>
             <xul:menuitem id="context_closeTab" label="&closeTab.label;" accesskey="&closeTab.accesskey;"
-                          oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode;
+                          oncommand="var tabbrowser = document.getBindingParent(this);
                                      tabbrowser.removeTab(tabbrowser.mContextTab);"/>
           </xul:menupopup>
 
           <xul:tabs class="tabbrowser-tabs" flex="1"
                     anonid="tabcontainer"
                     setfocus="false"
-                    onclick="this.parentNode.parentNode.parentNode.onTabClick(event);"
+                    onclick="document.getBindingParent(this).onTabClick(event);"
                     xbl:inherits="onnewtab"
-                    ondblclick="this.parentNode.parentNode.parentNode.onTabBarDblClick(event);"
-                    onclosetab="var node = this.parentNode;
-                                while (node.localName != 'tabbrowser')
-                                  node = node.parentNode;
-                                node.removeCurrentTab();"
-                    onkeypress="this.parentNode.parentNode.parentNode.onTabKeypress(event);">
+                    ondblclick="document.getBindingParent(this).onTabBarDblClick(event);"
+                    onclosetab="document.getBindingParent(this).removeCurrentTab();"
+                    onkeypress="document.getBindingParent(this).onTabKeypress(event);">
             <xul:tab selected="true" validate="never"
                      onerror="this.removeAttribute('image');"
                      maxwidth="250" width="0" minwidth="100" flex="100"
                      class="tabbrowser-tab" label="&untitledTab;" crop="end"/>
           </xul:tabs>
         </xul:hbox>
         <xul:tabpanels flex="1" class="tabbrowser-tabpanels plain" selectedIndex="0" anonid="panelcontainer">
-          <xul:conversation selected="true"/>
+          <xul:tabpanel selected="true"/>
         </xul:tabpanels>
       </xul:tabbox>
       <children/>
     </content>
     <implementation implements="nsIDOMEventListener">
       <field name="mTabBox" readonly="true">
         document.getAnonymousElementByAttribute(this, "anonid", "tabbox");
       </field>
@@ -115,78 +102,59 @@
         this.mTabContainer.childNodes
       </field>
       <field name="mStringBundle">
         document.getAnonymousElementByAttribute(this, "anonid", "tbstringbundle");
       </field>
       <field name="mCurrentTab">
         null
       </field>
-      <field name="mCurrentBrowser">
-        null
-      </field>
       <field name="mFirstTabIsDummy">
         true
       </field>
       <field name="mContextTab">
         null
       </field>
       <field name="arrowKeysShouldWrap" readonly="true">
 #ifdef XP_MACOSX
         true
 #else
         false
 #endif
       </field>
-      <field name="_browsers">
+
+      <!-- _conversations and _tabPanels are used as caches
+           to avoid creating arrays multiple times
+           (See conversations and tabPanels properties) -->
+      <field name="_conversations">
         null
       </field>
-      <field name="_conversations">
+      <field name="_tabPanels">
         null
       </field>
 
       <field name="_blockDblClick">
         false
       </field>
       <field name="_autoScrollPopup">
         null
       </field>
 
-      <method name="getBrowserAtIndex">
-        <parameter name="aIndex"/>
-        <body>
-          <![CDATA[
-            return this.browsers[aIndex];
-          ]]>
-        </body>
-      </method>
-
-      <method name="getConversationAtIndex">
-        <parameter name="aIndex"/>
-        <body>
-          <![CDATA[
-            return this.conversations[aIndex];
-          ]]>
-        </body>
-      </method>
-
       <method name="updateTitlebar">
         <body>
           <![CDATA[
-            if (!this.mCurrentConversation) // tabbrowser not initialized yet
+            if (!this.mCurrentTab) // tabbrowser not initialized yet
               return;
 
             var newTitle = "";
             var docTitle;
             var docElement = this.ownerDocument.documentElement;
             var sep = docElement.getAttribute("titlemenuseparator");
 
-            if (this.mCurrentTab)
-              docTitle = this.mCurrentTab.getAttribute("label");
-
+            docTitle = this.mCurrentTab.getAttribute("label");
             if (!docTitle)
               docTitle = docElement.getAttribute("titledefault");
 
             var modifier = docElement.getAttribute("titlemodifier");
             if (docTitle) {
               newTitle += docElement.getAttribute("titlepreface");
               newTitle += docTitle;
               if (modifier)
@@ -194,53 +162,86 @@
             }
             newTitle += modifier;
 
             this.ownerDocument.title = newTitle;
           ]]>
         </body>
       </method>
 
-      <method name="updatePopupMenu">
+      <method name="tabContextMenuShowing">
         <parameter name="aPopupMenu"/>
         <body>
           <![CDATA[
             let tagName = document.popupNode.localName;
             if (tagName == "tabs")
               return false;
             this.mContextTab = tagName == "tab" ?
                                document.popupNode : this.selectedTab;
             var disabled = this.mTabs.length == 1;
-            var menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple");
-            for (var i = 0; i < menuItems.length; i++)
-              menuItems[i].disabled = disabled;
-            document.getElementById("context_showLogs").disabled =
-              !this.mContextTab.linkedConversation.hasLogs();
+            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)
+                aPopupMenu.insertBefore(item, tabSpecificEndSeparator);
+            }
+            return true;
+          ]]>
+        </body>
+      </method>
+
+      <method name="tabContextMenuHiding">
+        <parameter name="aPopupMenu"/>
+        <body>
+          <![CDATA[
+            // Remove tab specific menu items added onpopupshowing.
+            let range = document.createRange();
+            range.setStartAfter(document.getElementById("context_tabSpecificStartSeparator"));
+            range.setEndBefore(document.getElementById("context_tabSpecificEndSeparator"));
+            range.deleteContents();
             return true;
           ]]>
         </body>
       </method>
 
-      <method name="updateCurrentBrowser">
+      <method name="updateCurrentTab">
         <parameter name="aForceUpdate"/>
         <body>
           <![CDATA[
-            var newConversation = this.getConversationAtIndex(this.mTabContainer.selectedIndex);
-            if (!aForceUpdate && this.mCurrentConversation == newConversation)
+            /* This method handles transitioning when switching tabs.
+             * When a new tab is selected, mCurrentTab still refers to the
+             * previously selected tab until we set it in this method.
+             * this.selectedTab always refers to the actual selected tab
+             * (see the "selectedTab" property).
+             * Also, the selected* properties other than selectedTab use
+             * mCurrentTab and not selectedTab. This ensures a newly selected
+             * tab's properties are not accessed till this method is called.
+             */
+            // We check that the currently selected tab is different from
+            // mCurrentTab (i.e.  a different tab was selected) before updating.
+            if (!aForceUpdate && this.mCurrentTab == this.selectedTab)
               return;
 
-            this.mCurrentConversation = newConversation;
+            // Deactivate the previous browser if it existed...
+            if (this.selectedBrowser)
+              this.selectedBrowser.docShell.isActive = false;
+            // ... set mCurrentTab to newly selected tab...
+            this.mCurrentTab = this.selectedTab;
+            // ... and activate the new browser if it exists.
+            if (this.selectedBrowser) {
+              this.mCurrentTab.linkedBrowser.docShell.isActive =
+                (window.windowState != window.STATE_MINIMIZED);
+            }
 
-            this.mCurrentBrowser.docShell.isActive = false;
-            this.mCurrentBrowser = newConversation.browser;
-            this.mCurrentBrowser.docShell.isActive =
-              (window.windowState != window.STATE_MINIMIZED);
-
-            this.mCurrentTab = this.selectedTab;
-            this.mCurrentTab.switchingToTab();
+            if ("switchingToPanel" in this.selectedPanel)
+              this.selectedPanel.switchingToPanel();
 
             // Update the window title.
             this.updateTitlebar();
 
             // We've selected the new tab, so go ahead and notify listeners.
             var event = document.createEvent("Events");
             event.initEvent("TabSelect", true, false);
             this.mCurrentTab.dispatchEvent(event);
@@ -254,60 +255,64 @@
               // Nevertheless update the visible conversation, but only if
               // the user stays on the tab for more than a moment, to prevent
               // tabs from being marked as read if the user is just scrolling
               // past them with the arrow keys. 400ms is between "200ms - a bit
               // too quick for people who repeat-keypress more slowly than me"
               // and "600ms - a bit too noticeable already".
               if (this._tabSelectTimer)
                 clearTimeout(this._tabSelectTimer);
+              if (!("onSelect" in this.selectedPanel))
+                return;
               this._tabSelectTimer = setTimeout(function() {
-                this.mCurrentConversation.onSelect();
+                this.selectedPanel.onSelect();
               }.bind(this), 400);
               return;
             }
 
             delete this._tabSelectTimer;
-            this.mCurrentConversation.focus();
+            this.selectedPanel.focus();
+            if ("onSelect" in this.selectedPanel)
+              this.selectedPanel.onSelect();
           ]]>
         </body>
       </method>
 
       <method name="onTabKeypress">
         <parameter name="event"/>
         <body>
           <![CDATA[
             const tabKeyCodes = [KeyEvent.DOM_VK_TAB,
                                  KeyEvent.DOM_VK_HOME, KeyEvent.DOM_VK_END,
                                  KeyEvent.DOM_VK_UP, KeyEvent.DOM_VK_DOWN,
                                  KeyEvent.DOM_VK_LEFT, KeyEvent.DOM_VK_RIGHT];
 
             if (tabKeyCodes.indexOf(event.keyCode) != -1)
               return;
 
-            // Focus the editbox and pass the key to it.
+            // Focus the panel and pass the key to it.
             event.preventDefault();
             event.stopPropagation();
-            this.mCurrentConversation.editor.focus();
+            this.selectedPanel.focus();
 
             const masks = Components.interfaces.nsIDOMNSEvent;
             var modifiers = 0;
             if (event.shiftKey)
               modifiers |= masks.SHIFT_MASK;
             if (event.ctrlKey)
               modifiers |= masks.CONTROL_MASK;
             if (event.altKey)
               modifiers |= masks.ALT_MASK;
             if (event.metaKey)
               modifiers |= masks.META_MASK;
             if (event.accelKey)
               modifiers |= (navigator.platform.indexOf("Mac") >= 0) ? masks.META_MASK
                                                                     : masks.CONTROL_MASK;
             // Can't use dispatchEvent to the textbox as these refuse untrusted key events.
-            this.mCurrentConversation.ownerDocument.defaultView
+            this.selectedPanel.ownerDocument.defaultView
               .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
               .getInterface(Components.interfaces.nsIDOMWindowUtils)
               .sendKeyEvent(event.type, event.keyCode, event.charCode, modifiers);
           ]]>
         </body>
       </method>
 
       <method name="onTabClick">
@@ -394,76 +399,115 @@
         </body>
       </method>
 
       <method name="addConversation">
         <parameter name="aConv"/>
         <body>
           <![CDATA[
             if (!this.mFirstTabIsDummy) {
-              if (!Services.prefs.getBoolPref("messenger.conversations.openInTabs"))
-                return null;
-
               if (Services.prefs.getBoolPref("messenger.conversations.useSeparateWindowsForMUCs") &&
-                  aConv.isChat != this.conversations[0].hasAttribute("chat"))
+                  this.conversations[0] && aConv.isChat != this.conversations[0].hasAttribute("chat"))
                 return null;
             }
 
-            return this._addConversation(aConv);
-        ]]>
+            let convPanel = document.createElementNS(
+              "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+              "conversation");
+            return this.addPanel(convPanel, aConv);
+          ]]>
         </body>
       </method>
 
-      <method name="_addConversation">
+      <!-- Adds a tab panel.
+           Conversation specific properties and attributes are set if aConv is defined.
+
+           Note that a panel must focus a child element in its focus method to
+           prevent a previously focused element from retaining focus and continuing
+           to receive input.
+
+           Panels can implement the following methods to customize behavior in certain situations:
+           destroy:
+             Called before panel is removed.
+             The destructor doesn't always get called when the panel is removed,
+             so use this method to force any required cleanup.
+           finishImport:
+             When a tab is moved to a different window, a new instance of the panel
+             is created. This method is called on the new instance after adding it.
+             Use it to initialize the panel from the instance in the previous window,
+             which is passed as a parameter.
+           getPanelSpecificMenuItems:
+             Called before showing the tab's context menu.
+             Use it to return an array of menu items specific to this panel.
+           onResize:
+             Called when the window is resized.
+             Use it to perform any changes required due to the new size.
+           onSelect:
+             Called when the tab is selected and the user is not just scrolling past it.
+             Use it for things like marking conversations as read.
+           switchingToPanel:
+             Called when switching to the panel, even just in passing.
+             Use it to customize behavior when the panel is displayed.
+           switchingAwayFromPanel:
+             Called when switching away from the panel.
+             Use it to customize behavior when the panel is hidden. -->
+      <method name="addPanel">
+        <parameter name="aPanel"/>
         <parameter name="aConv"/>
+        <!-- aPanel is a node containing the content of the panel
+             aConv is an (optional) imIConversation instance -->
         <body>
           <![CDATA[
-            // invalidate cache, because mTabContainer is about to change
-            this._browsers = null;
-            this._conversations = null;
-
-            var conv;
-            if (this.mFirstTabIsDummy)
-              conv = this.mPanelContainer.firstChild;
-            else {
-              conv = document.createElementNS(
-                "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
-                                              "conversation");
-              this.mPanelContainer.appendChild(conv);
-              if (this.mStrip.collapsed)
-                this.setStripVisibilityTo(true);
+            if (!this.mFirstTabIsDummy) {
+              if (!Services.prefs.getBoolPref("messenger.conversations.openInTabs"))
+                return null;
             }
-            conv.setAttribute("contenttooltip", this.getAttribute("contenttooltip"));
-            conv.setAttribute("contentcontextmenu", this.getAttribute("contentcontextmenu"));
-            conv.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
+            // invalidate cache, because mTabContainer is about to change
+            this._conversations = null;
+            this._tabPanels = null;
+
+            this.mPanelContainer.appendChild(aPanel);
+
+            var t = this.mTabContainer.addTab();
+            aPanel.tab = t;
 
-            var t;
+            if (aConv) {
+              aConv.QueryInterface(Components.interfaces.imIConversation);
+              // set up the shared autoscroll popup if it doesn't exist yet
+              if (!this._autoScrollPopup) {
+                this._autoScrollPopup = aPanel.browser._createAutoScrollPopup();
+                this._autoScrollPopup.id = "autoscroller";
+                this.appendChild(this._autoScrollPopup);
+              }
+              aPanel.setAttribute("contenttooltip", this.getAttribute("contenttooltip"));
+              aPanel.setAttribute("contentcontextmenu", this.getAttribute("contentcontextmenu"));
+              aPanel.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
+              aPanel.conv = aConv;
+              t.linkedConversation = aPanel;
+              t.linkedBrowser = aPanel.browser;
+              // We start our browsers out as inactive, and then maintain
+              // activeness in updateCurrentTab.
+              aPanel.browser.docShell.isActive = false;
+            }
+
+            this.setStripVisibilityTo(
+              !(this.mFirstTabIsDummy && Services.prefs.getBoolPref("browser.tabs.autoHide")));
+
+            var uniqueId = "panel" + Date.now() + t._tPos;
+            aPanel.id = uniqueId;
+            t.linkedPanel = uniqueId;
+            t.linkedTabPanel = aPanel;
+
+            this.updateCurrentTab(true);
+
             if (this.mFirstTabIsDummy)
-              t = this.mTabContainer.firstChild;
-            else
-              t = this.mTabContainer.addTab();
-            conv.tab = t;
-            conv.conv = aConv;
-
-            if (this.mStrip.collapsed &&
-                !Services.prefs.getBoolPref("browser.tabs.autoHide"))
-              this.setStripVisibilityTo(true);
+              this.removeTab(this.mTabContainer.firstChild);
             this.mFirstTabIsDummy = false;
 
-            var uniqueId = "panel" + Date.now() + t._tPos;
-            this.mPanelContainer.lastChild.id = uniqueId;
-            t.linkedPanel = uniqueId;
-            t.linkedConversation = conv;
-            t.linkedBrowser = conv.browser;
-            // We start our browsers out as inactive, and then maintain
-            // activeness in updateCurrentBrowser.
-            t.linkedBrowser.docShell.isActive = false;
-            this.updateCurrentBrowser(true);
-
-            return conv;
+            return aPanel;
           ]]>
         </body>
       </method>
 
       <method name="warnAboutClosingTabs">
       <parameter name="aAll"/>
       <body>
         <![CDATA[
@@ -567,19 +611,19 @@
         <parameter name="aTab"/>
         <parameter name="aTabWillBeMoved"/>
         <parameter name="aCloseWindowFastpath"/>
         <body>
           <![CDATA[
             if (this._removingTabs.indexOf(aTab) > -1 || this._windowIsClosing)
               return null;
 
-            var browser = this.getBrowserForTab(aTab);
+            var browser = aTab.linkedBrowser;
 
-            if (!aTabWillBeMoved) {
+            if (!aTabWillBeMoved && browser) {
               let ds = browser.docShell;
               if (ds.contentViewer && !ds.contentViewer.permitUnload())
                 return null;
             }
 
             var closeWindow = false;
             var l = this.mTabs.length - this._removingTabs.length;
             if (l == 1) {
@@ -635,36 +679,32 @@
               // see notes in addTab
               function _delayedUpdate(aTabContainer) {
                 aTabContainer.adjustTabstrip();
                 aTabContainer.mTabstrip._updateScrollButtonsDisabledState();
               };
               setTimeout(_delayedUpdate, 0, this.tabContainer);
             }
 
-            var conversation = this.getConversationForTab(aTab);
-            var browser = this.getBrowserForTab(aTab);
+            var panel = aTab.linkedTabPanel;
 
             // Because of the way XBL works (fields just set JS
             // properties on the element) and the code we have in place
             // to preserve the JS objects for any elements that have
             // JS properties set on them, the browser element won't be
             // destroyed until the document goes away.  So we force a
             // cleanup ourselves.
             // This has to happen before we remove the child so that the
             // XBL implementation of nsIObserver still works.
-            conversation.destroy();
-
-            if (conversation == this.mCurrentConversation)
-              this.mCurrentConversation = null;
+            if ("destroy" in panel)
+              panel.destroy();
 
-            // Invalidate browsers cache, as the tab is removed from the
-            // tab container.
-            this._browsers = null;
+            // Invalidate caches, as the tab is removed from the tab container.
             this._conversations = null;
+            this._tabPanels = null;
 
             // Remove the tab ...
             this.tabContainer.removeChild(aTab);
 
             // ... and fix up the _tPos properties immediately.
             for (let i = aTab._tPos; i < this.mTabs.length; i++)
               this.mTabs[i]._tPos = i;
 
@@ -672,23 +712,23 @@
             if (this.selectedTab) // can be null if we removed the last tab
               this.selectedTab._selected = true;
 
             // This will unload the document. An unload handler could remove
             // dependant tabs, so it's important that the tabbrowser is now in
             // a consistent state (tab removed, tab positions updated, etc.).
             // Also, it's important that another tab has been selected before
             // the panel is removed; otherwise, a random sibling panel can flash.
-            this.mPanelContainer.removeChild(conversation);
+            this.mPanelContainer.removeChild(panel);
 
             // As the panel is removed, the removal of a dependent document can
             // cause the whole window to close. So at this point, it's possible
             // that the binding is destructed.
             if (this.mTabBox)
-              this.mTabBox.selectedPanel = this.getConversationForTab(this.mCurrentTab);
+              this.mTabBox.selectedPanel = this.selectedPanel;
 
             if (aCloseWindow)
               this._windowIsClosing = closeWindow(true);
           ]]>
         </body>
       </method>
 
       <method name="_blurTab">
@@ -712,51 +752,47 @@
               } while (tab && this._removingTabs.indexOf(tab) != -1);
             }
 
             this.selectedTab = tab;
           ]]>
         </body>
       </method>
 
-      <method name="importConversation">
+      <method name="importPanel">
         <parameter name="aOtherTab"/>
         <body>
           <![CDATA[
-            // That's gBrowser for the other window, not the tab's browser!
-            var remoteBrowser =
-              aOtherTab.ownerDocument.defaultView.getBrowser();
+            var remoteTabBrowser = aOtherTab.ownerDocument.defaultView.getTabBrowser();
 
             // First, start teardown of the other browser.  Make sure to not
             // fire the beforeunload event in the process.  Close the other
             // window if this was its last tab.
-            var endRemoveArgs = remoteBrowser._beginRemoveTab(aOtherTab, true);
-
-
-            var conv = this._addConversation(aOtherTab.linkedConversation.conv);
+            var endRemoveArgs = remoteTabBrowser._beginRemoveTab(aOtherTab, true);
 
-            var aOurTab = conv.tab;
-            var ourBrowser = this.getBrowserForTab(aOurTab);
-            // make sure it has a docshell
-            ourBrowser.docShell;
+            var newPanel = document.createElementNS(
+              "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+              aOtherTab.linkedTabPanel.nodeName);
+            this.addPanel(newPanel, aOtherTab.linkedTabPanel.conv || null);
 
-            // Swap the docshells
-            ourBrowser.swapDocShells(aOtherTab.linkedBrowser);
-            aOtherTab.linkedConversation.conv = null;
-            conv.finishImport(aOtherTab.linkedConversation);
+            var aOurTab = newPanel.tab;
+
+            // Tell the new panel to sync up with the other one.
+            if ("finishImport" in newPanel)
+              newPanel.finishImport(aOtherTab.linkedTabPanel);
 
             // Finish tearing down the tab that's going away.
-            remoteBrowser._endRemoveTab(endRemoveArgs);
+            remoteTabBrowser._endRemoveTab(endRemoveArgs);
 
             // If the tab was already selected (this happpens in the scenario
             // of replaceTabsWithWindow), notify onLocationChange, etc.
             if (aOurTab == this.selectedTab)
-              this.updateCurrentBrowser(true);
+              this.updateCurrentTab(true);
 
-            return conv;
+            return newPanel;
           ]]>
         </body>
       </method>
 
       <method name="onTabBarDblClick">
         <parameter name="aEvent"/>
         <body>
           <![CDATA[
@@ -767,34 +803,16 @@
               var e = document.createEvent("Events");
               e.initEvent("NewTab", true, true);
               this.dispatchEvent(e);
             }
           ]]>
         </body>
       </method>
 
-      <method name="getBrowserForTab">
-        <parameter name="aTab"/>
-        <body>
-        <![CDATA[
-          return aTab.linkedBrowser;
-        ]]>
-        </body>
-      </method>
-
-      <method name="getConversationForTab">
-        <parameter name="aTab"/>
-        <body>
-        <![CDATA[
-          return aTab.linkedConversation;
-        ]]>
-        </body>
-      </method>
-
       <method name="selectTabAtIndex">
         <parameter name="aIndex"/>
         <parameter name="aEvent"/>
         <body>
         <![CDATA[
           // count backwards for aIndex < 0
           if (aIndex < 0)
             aIndex += this.mTabs.length;
@@ -827,38 +845,45 @@
           // Update the tab
           this.mTabBox.selectedTab = val;
           return val;
           ]]>
         </setter>
       </property>
 
       <property name="selectedBrowser"
-                onget="return this.mCurrentBrowser;"
+                onget="return this.mCurrentTab.linkedBrowser || null;"
                 readonly="true"/>
 
       <property name="selectedConversation"
-                onget="return this.mCurrentConversation;"
+                onget="return this.mCurrentTab.linkedConversation || null;"
                 readonly="true"/>
 
-      <property name="browsers" readonly="true">
+      <property name="selectedPanel"
+                onget="return this.mCurrentTab.linkedTabPanel || null;"
+                readonly="true"/>
+
+      <property name="tabPanels" readonly="true">
        <getter>
           <![CDATA[
-            return this._browsers ||
-                   (this._browsers = Array.map(this.mTabs, function (tab) tab.linkedBrowser));
+            return this._tabPanels ||
+                   (this._tabPanels = Array.map(this.mTabs, function (tab) tab.linkedTabPanel));
           ]]>
         </getter>
       </property>
 
       <property name="conversations" readonly="true">
        <getter>
-          <![CDATA[
-            return this._conversations ||
-                   (this._conversations = Array.map(this.mTabs, function (tab) tab.linkedConversation));
-          ]]>
+        <![CDATA[
+          if (!this._conversations) {
+            this._conversations = Array.map(this.mTabs, function(aTab) aTab.linkedConversation)
+                                       .filter(function(aConv) aConv);
+          }
+          return this._conversations;
+        ]]>
         </getter>
       </property>
 
       <method name="_onDragStart">
         <parameter name="aEvent"/>
         <body>
         <![CDATA[
           var target = aEvent.target;
@@ -1031,21 +1056,21 @@
             }
             else {
               // swap the dropped tab with a new one we create and then close
               // it in the other window (making it seem to have moved between
               // windows)
 
               // Compute the new index *before* we add a tab
               newIndex = this.getNewIndex(aEvent);
-              var conv = this.importConversation(draggedTab);
-              this.moveTabTo(conv.tab, newIndex);
+              var panel = this.importPanel(draggedTab);
+              this.moveTabTo(panel.tab, newIndex);
 
               // We need to set selectedTab after we've swapped the docShells
-              this.selectedTab = conv.tab;
+              this.selectedTab = panel.tab;
             }
           ]]>
         </body>
       </method>
 
       <method name="_onDragEnd">
         <parameter name="aEvent"/>
         <body>
@@ -1120,29 +1145,30 @@
         </body>
       </method>
 
       <method name="moveTabTo">
         <parameter name="aTab"/>
         <parameter name="aIndex"/>
         <body>
         <![CDATA[
-          this._browsers = null; // invalidate cache
+          // Invalidate caches.
           this._conversations = null;
+          this._tabPanels = null;
 
           var oldPosition = aTab._tPos;
 
           aIndex = aIndex < aTab._tPos ? aIndex: aIndex+1;
           this.mCurrentTab._selected = false;
           // use .item() instead of [] because dragging to the end of the strip goes out of
           // bounds: .item() returns null (so it acts like appendChild), but [] throws
           this.mTabContainer.insertBefore(aTab, this.mTabContainer.childNodes.item(aIndex));
           // invalidate cache, because mTabContainer is about to change
-          this._browsers = null;
           this._conversations = null;
+          this._tabPanels = null;
 
           var i;
           for (i = 0; i < this.mTabContainer.childNodes.length; i++) {
             this.mTabContainer.childNodes[i]._tPos = i;
             this.mTabContainer.childNodes[i]._selected = false;
           }
           this.mCurrentTab._selected = true;
           this.mTabContainer.mTabstrip.scrollBoxObject.ensureElementIsVisible(this.mCurrentTab);
@@ -1175,17 +1201,17 @@
         </body>
       </method>
 
 
       <method name="moveTabForward">
         <body>
           <![CDATA[
             var tabPos = this.mCurrentTab._tPos;
-            if (tabPos < this.browsers.length - 1) {
+            if (tabPos < this.mTabs.length - 1) {
               this.moveTabTo(this.mCurrentTab, tabPos + 1);
               this.mCurrentTab.focus();
             }
             else if (this.arrowKeysShouldWrap)
               this.moveTabToStart();
           ]]>
         </body>
       </method>
@@ -1215,19 +1241,18 @@
           ]]>
         </body>
       </method>
 
       <method name="moveTabToEnd">
         <body>
           <![CDATA[
             var tabPos = this.mCurrentTab._tPos;
-            if (tabPos < this.browsers.length - 1) {
-              this.moveTabTo(this.mCurrentTab,
-                             this.browsers.length - 1);
+            if (tabPos < this.mTabs.length - 1) {
+              this.moveTabTo(this.mCurrentTab, this.mTabs.length - 1);
               this.mCurrentTab.focus();
             }
           ]]>
         </body>
       </method>
 
       <method name="moveTabOver">
         <parameter name="aEvent"/>
@@ -1238,43 +1263,16 @@
                 (direction == "rtl" && aEvent.keyCode == KeyEvent.DOM_VK_LEFT))
               this.moveTabForward();
             else
               this.moveTabBackward();
           ]]>
         </body>
       </method>
 
-      <!-- BEGIN FORWARDED BROWSER PROPERTIES.  IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT
-           MAKE SURE TO ADD IT HERE AS WELL. -->
-
-      <property name="docShell"
-                onget="return this.mCurrentBrowser.docShell"
-                readonly="true"/>
-
-      <property name="contentWindow"
-                readonly="true"
-                onget="return this.mCurrentBrowser.contentWindow"/>
-
-      <property name="markupDocumentViewer"
-                onget="return this.mCurrentBrowser.markupDocumentViewer;"
-                readonly="true"/>
-
-      <property name="contentDocument"
-                onget="return this.mCurrentBrowser.contentDocument;"
-                readonly="true"/>
-
-      <property name="contentTitle"
-                onget="return this.mCurrentBrowser.contentTitle;"
-                readonly="true"/>
-
-      <property name="findbar"
-                onget="return this.mCurrentConversation.findbar;"
-                readonly="true"/>
-
       <method name="dragDropSecurityCheck">
         <parameter name="aEvent"/>
         <parameter name="aDragSession"/>
         <parameter name="aUri"/>
         <body>
           <![CDATA[
             nsDragAndDrop.dragDropSecurityCheck(aEvent, aDragSession, aUri);
           ]]>
@@ -1366,64 +1364,62 @@
       })]]>
       </field>
 
       <method name="handleEvent">
         <parameter name="aEvent"/>
         <body><![CDATA[
           switch (aEvent.type) {
             case "sizemodechange":
-              if (aEvent.target == window) {
-                this.mCurrentBrowser.docShell.isActive =
+              if (this.selectedBrowser && aEvent.target == window) {
+                this.selectedBrowser.docShell.isActive =
                   (window.windowState != window.STATE_MINIMIZED);
               }
               break;
           }
         ]]></body>
       </method>
 
       <field name="_windowActivateHandler" readonly="true">
       <![CDATA[({
         tabbrowser: this,
-        handleEvent: function handleEvent(aEvent)
-          this.tabbrowser.mCurrentTab.switchingToTab()
+        handleEvent: function handleEvent() {
+          if ("switchingToPanel" in this.tabbrowser.selectedPanel)
+            this.tabbrowser.selectedPanel.switchingToPanel();
+        }
       })]]>
       </field>
 
       <field name="_windowDeactivateHandler" readonly="true">
       <![CDATA[({
         tabbrowser: this,
-        handleEvent: function handleEvent(aEvent)
-          this.tabbrowser.mCurrentTab.switchingAwayFromTab()
+        handleEvent: function handleEvent() {
+          if ("switchingAwayFromPanel" in this.tabbrowser.selectedPanel)
+            this.tabbrowser.selectedPanel.switchingAwayFromPanel();
+        }
       })]]>
       </field>
 
       <constructor>
         <![CDATA[
-          this.mCurrentConversation = this.mPanelContainer.firstChild;
-          this.mCurrentBrowser = this.mCurrentConversation.browser;
           this.mCurrentTab = this.mTabContainer.firstChild;
+          // Dummy tab is not a conversation
+          this.mCurrentTab.linkedTabPanel = this.mPanelContainer.firstChild;
+          this.mCurrentTab.linkedTabPanel.tab = this.mCurrentTab;
 
           document.addEventListener("keypress", this._keyEventHandler);
           document.addEventListener("sizemodechange", this);
           window.addEventListener("deactivate", this._windowDeactivateHandler);
           window.addEventListener("activate", this._windowActivateHandler);
 
           var uniqueId = "panel" + Date.now();
-          this.mCurrentConversation.id = uniqueId;
+          this.mCurrentTab.linkedTabPanel.id = uniqueId;
           this.mCurrentTab.linkedPanel = uniqueId;
           this.mCurrentTab._tPos = 0;
-          this.mCurrentTab.linkedConversation = this.mCurrentConversation;
-          this.mCurrentTab.linkedBrowser = this.mCurrentBrowser;
 
-          // set up the shared autoscroll popup
-          this._autoScrollPopup = this.mCurrentBrowser._createAutoScrollPopup();
-          this._autoScrollPopup.id = "autoscroller";
-          this.appendChild(this._autoScrollPopup);
-          this.mCurrentBrowser.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
           Services.prefs.addObserver("browser.tabs.autoHide", this._prefObserver, false);
           Services.prefs.addObserver("messenger.conversations.useSeparateWindowsForMUCs", this._prefObserver, false);
         ]]>
       </constructor>
 
       <destructor>
         <![CDATA[
           document.removeEventListener("keypress", this._keyEventHandler);
@@ -2072,50 +2068,16 @@
           <xul:toolbarbutton anonid="close-button" tabindex="-1" class="tab-close-button" tooltiptext="&closeTab.label;"/>
         </xul:hbox>
       </xul:stack>
     </content>
 
     <implementation>
       <field name="mOverCloseButton">false</field>
       <field name="mCorrespondingMenuitem">null</field>
-
-      <method name="switchingToTab">
-        <body>
-          <![CDATA[
-            if (this._visibleTimer)
-              return;
-
-            // Start a timer to detect if the tab has been visible to the
-            // user for long enough to actually be seen (as opposed to the
-            // tab only being visible "accidentally in passing").
-            delete this._wasVisible;
-            this._visibleTimer = setTimeout(function() {
-              this._wasVisible = true;
-              delete this._visibleTimer;
-            }.bind(this), 1000);
-          ]]>
-        </body>
-      </method>
-
-      <method name="switchingAwayFromTab">
-        <body>
-          <![CDATA[
-            if (this._visibleTimer) {
-              clearTimeout(this._visibleTimer);
-              delete this._visibleTimer;
-            }
-
-            // Remove the unread ruler if the tab has been visible without
-            // interruptions for sufficiently long.
-            if (this._wasVisible)
-              this.linkedBrowser.removeUnreadRuler();
-          ]]>
-        </body>
-      </method>
     </implementation>
 
     <handlers>
       <handler event="mouseover">
         var anonid = event.originalTarget.getAttribute("anonid");
         if (anonid == "close-button")
           this.mOverCloseButton = true;
       </handler>
@@ -2132,19 +2094,20 @@
         if (this.mOverCloseButton) {
           event.stopPropagation();
         }
         else {
           // If this tab is not currently selected, call the onSelect method of
           // the current tab to mark the conversation as read before leaving it.
           // This is necessary when Instantbird (and therefore the current tab)
           // did not have focus before this click.
-          if (!this.linkedConversation.hasAttribute("selected")) {
-            let tabbrowser = document.getBindingParent(this);
-            tabbrowser.mCurrentConversation.onSelect();
+          if (!this.linkedTabPanel.hasAttribute("selected")) {
+            let panel = document.getBindingParent(this).selectedPanel;
+            if ("onSelect" in panel)
+              panel.onSelect();
           }
 
           this.style.MozUserFocus = 'ignore';
           this.clientTop; // just using this to flush style updates
         }
       ]]>
       </handler>
       <handler event="mousedown" button="1">
@@ -2155,26 +2118,40 @@
         this.style.MozUserFocus = 'ignore';
         this.clientTop;
       </handler>
       <handler event="mouseup">
         this.style.MozUserFocus = '';
       </handler>
       <handler event="DOMAttrModified">
        <![CDATA[
-        if (event.attrName == "label" && ("getBrowser" in window))
-          getBrowser().updateTitlebar();
-
-        if (event.attrName != "selected")
-          return;
-
-        if (event.attrChange == event.REMOVAL) {
-          this.linkedConversation.removeAttribute("selected");
-          this.switchingAwayFromTab();
+        if (event.attrName == "label") {
+          if ("getTabBrowser" in window)
+            getTabBrowser().updateTitlebar();
+          // Update our tooltiptext, but only if a tooltip hasn't been set.
+          if (!this.hasAttribute("tooltip"))
+            this.setAttribute("tooltiptext", event.newValue);
         }
-        else
-          this.linkedConversation.setAttribute("selected", event.newValue);
+        else if (event.attrName == "tooltip") {
+          if (event.attrChange == event.ADDITION) {
+            // Tooltip was added. Stop using our tooltiptext attribute.
+            this.removeAttribute("tooltiptext");
+          }
+          else if (event.attrChange == event.REMOVAL) {
+            // Tooltip was removed. Switch to using our label as tooltiptext.
+            this.setAttribute("tooltiptext", this.getAttribute("label"));
+          }
+        }
+        else if (event.attrName == "selected") {
+          if (event.attrChange == event.REMOVAL) {
+            this.linkedTabPanel.removeAttribute("selected");
+            if ("switchingAwayFromPanel" in this.linkedTabPanel)
+              this.linkedTabPanel.switchingAwayFromPanel();
+          }
+          else
+            this.linkedTabPanel.setAttribute("selected", event.newValue);
+        }
        ]]>
       </handler>
     </handlers>
   </binding>
 
 </bindings>
new file mode 100644
--- /dev/null
+++ b/im/locales/en-US/chrome/instantbird/conversation.properties
@@ -0,0 +1,10 @@
+# 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/.
+
+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/tabbrowser.dtd
+++ b/im/locales/en-US/chrome/instantbird/tabbrowser.dtd
@@ -6,16 +6,10 @@
 <!ENTITY  newTab.label           "New Tab">
 <!ENTITY  newTab.accesskey       "N">
 <!ENTITY  closeTab.label         "Close Tab">
 <!ENTITY  closeTab.accesskey     "c">
 <!ENTITY  closeOtherTabs.accesskey "o">
 <!ENTITY  closeOtherTabs.label     "Close Other Tabs">
 <!ENTITY  openTabInNewWindow.label     "Open in a New Window">
 <!ENTITY  openTabInNewWindow.accesskey "W">
-<!ENTITY  showLogs.label         "Show Logs">
-<!ENTITY  showLogs.accesskey     "L">
-<!ENTITY  closeConv.label        "Close Conversation">
-<!ENTITY  closeConv.accesskey    "v">
-<!ENTITY  hideConv.label         "Put Conversation on Hold">
-<!ENTITY  hideConv.accesskey     "h">
 <!ENTITY  listAllTabs.label      "List all tabs">
 <!ENTITY  newTabButton.tooltip   "Open a new tab">
--- a/im/locales/jar.mn
+++ b/im/locales/jar.mn
@@ -9,16 +9,17 @@
 	locale/@AB_CD@/instantbird/aboutDialog.dtd	(%chrome/instantbird/aboutDialog.dtd)
 	locale/@AB_CD@/instantbird/account.dtd		(%chrome/instantbird/account.dtd)
 	locale/@AB_CD@/instantbird/accounts.dtd		(%chrome/instantbird/accounts.dtd)
 	locale/@AB_CD@/instantbird/accounts.properties	(%chrome/instantbird/accounts.properties)
 	locale/@AB_CD@/instantbird/accountWizard.dtd	(%chrome/instantbird/accountWizard.dtd)
 	locale/@AB_CD@/instantbird/accountWizard.properties	(%chrome/instantbird/accountWizard.properties)
 	locale/@AB_CD@/instantbird/addbuddy.dtd		(%chrome/instantbird/addbuddy.dtd)
 	locale/@AB_CD@/instantbird/buddytooltip.properties (%chrome/instantbird/buddytooltip.properties)
+	locale/@AB_CD@/instantbird/conversation.properties	(%chrome/instantbird/conversation.properties)
 	locale/@AB_CD@/instantbird/core.properties	(%chrome/instantbird/core.properties)
 	locale/@AB_CD@/instantbird/credits.dtd		(%chrome/instantbird/credits.dtd)
 	locale/@AB_CD@/instantbird/engineManager.dtd	(%chrome/instantbird/engineManager.dtd)
 	locale/@AB_CD@/instantbird/engineManager.properties (%chrome/instantbird/engineManager.properties)
 	locale/@AB_CD@/instantbird/extensions.properties (%chrome/instantbird/extensions.properties)
 	locale/@AB_CD@/instantbird/extensions-discover.dtd (%chrome/instantbird/extensions-discover.dtd)
 	locale/@AB_CD@/instantbird/instantbird.dtd	(%chrome/instantbird/instantbird.dtd)
 	locale/@AB_CD@/instantbird/instantbird.properties (%chrome/instantbird/instantbird.properties)
--- a/im/modules/imWindows.jsm
+++ b/im/modules/imWindows.jsm
@@ -72,16 +72,18 @@ var Conversations = {
     // The conversation may still not be displayed if we are waiting
     // for a new window. In this case the conversation will be focused
     // automatically anyway.
     if (this.isUIConversationDisplayed(uiConv)) {
       let conv = this._uiConv[uiConv.id];
       let doc = conv.ownerDocument;
       doc.getElementById("conversations").selectedTab = conv.tab;
       conv.focus();
+      // Tell it to mark itself as read.
+      conv.onSelect();
       doc.defaultView.focus();
 #ifdef XP_MACOSX
       Components.classes["@mozilla.org/widget/macdocksupport;1"]
                 .getService(Components.interfaces.nsIMacDockSupport)
                 .activateApplication(true);
 #endif
     }
     return uiConv;