Bug 1002914 (part 1) - refactor the chat window so it can be used by loop and social. r=mixedpuppy
☠☠ backed out by 8be0e21fd300 ☠ ☠
authorMark Hammond <mhammond@skippinet.com.au>
Wed, 07 May 2014 09:58:18 +1000
changeset 181927 7824a38269633d18eab35d100c9eb1facc32c7f7
parent 181926 9fb5cb82cb131613ea079819b480afee7ddecc62
child 181928 2f3f35b8cea3faf36a03e900df80ccee180b8b0b
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmixedpuppy
bugs1002914
milestone32.0a1
Bug 1002914 (part 1) - refactor the chat window so it can be used by loop and social. r=mixedpuppy
browser/base/content/browser-sets.inc
browser/base/content/browser-social.js
browser/base/content/socialchat.xml
browser/base/content/test/social/browser_social_chatwindow.js
browser/base/content/test/social/browser_social_errorPage.js
browser/base/content/test/social/head.js
browser/modules/Chat.jsm
browser/modules/moz.build
toolkit/components/social/MozSocialAPI.jsm
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -114,18 +114,18 @@
       oncommand="OpenBrowserWindow({remote: true});"/>
     <command id="Tools:NonRemoteWindow"
       oncommand="OpenBrowserWindow({remote: false});"/>
     <command id="History:UndoCloseTab" oncommand="undoCloseTab();"/>
     <command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/>
     <command id="Social:SharePage" oncommand="SocialShare.sharePage();" disabled="true"/>
     <command id="Social:ToggleSidebar" oncommand="SocialSidebar.toggleSidebar();" hidden="true"/>
     <command id="Social:ToggleNotifications" oncommand="Social.toggleNotifications();" hidden="true"/>
-    <command id="Social:FocusChat" oncommand="SocialChatBar.focus();" hidden="true" disabled="true"/>
     <command id="Social:Addons" oncommand="BrowserOpenAddonsMgr('addons://list/service');"/>
+    <command id="Chat:Focus" oncommand="Cu.import('resource:///modules/Chat.jsm', {}).Chat.focus(window);"/>
   </commandset>
 
   <commandset id="placesCommands">
     <command id="Browser:ShowAllBookmarks"
              oncommand="PlacesCommandHook.showPlacesOrganizer('AllBookmarks');"/>
     <command id="Browser:ShowAllHistory"
              oncommand="PlacesCommandHook.showPlacesOrganizer('History');"/>
   </commandset>
@@ -375,17 +375,25 @@
     <key id="viewBookmarksSidebarKb" key="&bookmarksCmd.commandkey;" command="viewBookmarksSidebar" modifiers="accel"/>
 #ifdef XP_WIN
 # Cmd+I is conventially mapped to Info on MacOS X, thus it should not be
 # overridden for other purposes there.
     <key id="viewBookmarksSidebarWinKb" key="&bookmarksWinCmd.commandkey;" command="viewBookmarksSidebar" modifiers="accel"/>
 #endif
 
     <!--<key id="markPage" key="&markPageCmd.commandkey;" command="Social:TogglePageMark" modifiers="accel,shift"/>-->
-    <key id="focusChatBar" key="&social.chatBar.commandkey;" command="Social:FocusChat" modifiers="accel,shift"/>
+    <key id="focusChatBar" key="&social.chatBar.commandkey;" command="Chat:Focus"
+#ifdef XP_MACOSX
+# Sadly the devtools uses shift-accel-c on non-mac and alt-accel-c everywhere else
+# So we just use the other
+         modifiers="accel,shift"
+#else
+         modifiers="accel,alt"
+#endif
+    />
 
     <key id="key_stop" keycode="VK_ESCAPE" command="Browser:Stop"/>
 
 #ifdef XP_MACOSX
     <key id="key_stop_mac" modifiers="accel" key="&stopCmd.macCommandKey;" command="Browser:Stop"/>
 #endif
 
     <key id="key_gotoHistory"
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -168,17 +168,16 @@ SocialUI = {
       Components.utils.reportError(e + "\n" + e.stack);
       throw e;
     }
   },
 
   _providersChanged: function() {
     SocialSidebar.clearProviderMenus();
     SocialSidebar.update();
-    SocialChatBar.update();
     SocialShare.populateProviderMenu();
     SocialStatus.populateToolbarPalette();
     SocialMarks.populateToolbarPalette();
     SocialShare.update();
   },
 
   // This handles "ActivateSocialFeature" events fired against content documents
   // in this window.
@@ -296,49 +295,23 @@ SocialUI = {
     SocialShare.update();
   }
 }
 
 SocialChatBar = {
   get chatbar() {
     return document.getElementById("pinnedchats");
   },
-  // Whether the chatbar is available for this window.  Note that in full-screen
-  // mode chats are available, but not shown.
-  get isAvailable() {
-    return SocialUI.enabled;
-  },
-  // Does this chatbar have any chats (whether minimized, collapsed or normal)
-  get hasChats() {
-    return !!this.chatbar.firstElementChild;
-  },
   openChat: function(aProvider, aURL, aCallback, aMode) {
-    this.update();
-    if (!this.isAvailable)
-      return false;
-    this.chatbar.openChat(aProvider, aURL, aCallback, aMode);
-    // We only want to focus the chat if it is as a result of user input.
-    let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                    .getInterface(Ci.nsIDOMWindowUtils);
-    if (dwu.isHandlingUserInput)
-      this.chatbar.focus();
+    // Until we refactor the tests we can't remove this.
+    // Reach into the social API.
+    let openChatWindow = Cu.import("resource://gre/modules/MozSocialAPI.jsm", {}).openChatWindow;
+    openChatWindow(window, aProvider, aURL, aCallback, aMode);
     return true;
   },
-  update: function() {
-    let command = document.getElementById("Social:FocusChat");
-    if (!this.isAvailable) {
-      this.chatbar.hidden = command.hidden = true;
-    } else {
-      this.chatbar.hidden = command.hidden = false;
-    }
-    command.setAttribute("disabled", command.hidden ? "true" : "false");
-  },
-  focus: function SocialChatBar_focus() {
-    this.chatbar.focus();
-  }
 }
 
 SocialFlyout = {
   get panel() {
     return document.getElementById("social-flyout-panel");
   },
 
   get iframe() {
--- a/browser/base/content/socialchat.xml
+++ b/browser/base/content/socialchat.xml
@@ -25,60 +25,53 @@
                   context="contentAreaContextMenu"
                   disableglobalhistory="true"
                   tooltip="aHTMLTooltip"
                   xbl:inherits="src,origin" type="content"/>
     </content>
 
     <implementation implements="nsIDOMEventListener">
       <constructor><![CDATA[
-        let Social = Components.utils.import("resource:///modules/Social.jsm", {}).Social;
         this.content.__defineGetter__("popupnotificationanchor",
                                       () => document.getAnonymousElementByAttribute(this, "anonid", "notification-icon"));
-        Social.setErrorListener(this.content, function(aBrowser) {
-          aBrowser.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" +
-                                 encodeURIComponent(aBrowser.getAttribute("origin")),
-                                 null, null, null, null);
-        });
+
         if (!this.chatbar) {
           document.getAnonymousElementByAttribute(this, "anonid", "minimize").hidden = true;
           document.getAnonymousElementByAttribute(this, "anonid", "close").hidden = true;
         }
         let contentWindow = this.contentWindow;
+        // process this._callbacks, then set to null so the chatbox creator
+        // knows to make new callbacks immediately.
+        if (this._callbacks) {
+          for (let callback of this._callbacks) {
+            callback(this);
+          }
+          this._callbacks = null;
+        }
         this.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) {
           if (event.target != this.contentDocument)
             return;
           this.removeEventListener("DOMContentLoaded", DOMContentLoaded, true);
           this.isActive = !this.minimized;
-          // process this._callbacks, then set to null so the chatbox creator
-          // knows to make new callbacks immediately.
-          if (this._callbacks) {
-            for (let callback of this._callbacks) {
-              if (callback)
-                callback(contentWindow);
-            }
-            this._callbacks = null;
-          }
-
-          // content can send a socialChatActivity event to have the UI update.
-          let chatActivity = function() {
-            this.setAttribute("activity", true);
-            if (this.chatbar)
-              this.chatbar.updateTitlebar(this);
-          }.bind(this);
-          contentWindow.addEventListener("socialChatActivity", chatActivity);
-          contentWindow.addEventListener("unload", function unload() {
-            contentWindow.removeEventListener("unload", unload);
-            contentWindow.removeEventListener("socialChatActivity", chatActivity);
-          });
+          this._deferredChatLoaded.resolve(this);
         }, true);
         if (this.src)
           this.setAttribute("src", this.src);
       ]]></constructor>
 
+      <field name="_deferredChatLoaded" readonly="true">
+        Promise.defer();
+      </field>
+
+      <property name="promiseChatLoaded">
+        <getter>
+          return this._deferredChatLoaded.promise;
+        </getter>
+      </property>
+
       <field name="content" readonly="true">
         document.getAnonymousElementByAttribute(this, "anonid", "content");
       </field>
 
       <property name="contentWindow">
         <getter>
           return this.content.contentWindow;
         </getter>
@@ -128,40 +121,32 @@
           this.content.docShell.isActive = !!val;
 
           // let the chat frame know if it is being shown or hidden
           let evt = this.contentDocument.createEvent("CustomEvent");
           evt.initCustomEvent(val ? "socialFrameShow" : "socialFrameHide", true, true, {});
           this.contentDocument.documentElement.dispatchEvent(evt);
         </setter>
       </property>
-      
+
       <method name="showNotifications">
         <body><![CDATA[
         PopupNotifications._reshowNotifications(this.content.popupnotificationanchor,
                                                 this.content);
         ]]></body>
       </method>
 
       <method name="swapDocShells">
         <parameter name="aTarget"/>
         <body><![CDATA[
           aTarget.setAttribute('label', this.contentDocument.title);
           aTarget.src = this.src;
           aTarget.content.setAttribute("origin", this.content.getAttribute("origin"));
           aTarget.content.popupnotificationanchor.className = this.content.popupnotificationanchor.className;
-          this.content.socialErrorListener.remove();
-          aTarget.content.socialErrorListener.remove();
           this.content.swapDocShells(aTarget.content);
-          Social.setErrorListener(this.content, function(aBrowser) {}); // 'this' will be destroyed soon.
-          Social.setErrorListener(aTarget.content, function(aBrowser) {
-            aBrowser.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" +
-                                 encodeURIComponent(aBrowser.getAttribute("origin")),
-                                 null, null, null, null);
-          });
         ]]></body>
       </method>
 
       <method name="onTitlebarClick">
         <parameter name="aEvent"/>
         <body><![CDATA[
           if (!this.chatbar)
             return;
@@ -181,41 +166,50 @@
           this.chatbar.remove(this);
         else
           window.close();
         ]]></body>
       </method>
 
       <method name="swapWindows">
         <body><![CDATA[
-        let provider = Social._getProviderFromOrigin(this.content.getAttribute("origin"));
+        let deferred = Promise.defer();
+        let title = this.getAttribute("label");
         if (this.chatbar) {
-          this.chatbar.detachChatbox(this, { "centerscreen": "yes" }, win => {
-            win.document.title = provider.name;
-          });
+          this.chatbar.detachChatbox(this, { "centerscreen": "yes" }).then(
+            chatbox => {
+              chatbox.contentWindow.document.title = title;
+              deferred.resolve(chatbox);
+            }
+          );
         } else {
           // attach this chatbox to the topmost browser window
-          let findChromeWindowForChats = Cu.import("resource://gre/modules/MozSocialAPI.jsm").findChromeWindowForChats;
-          let win = findChromeWindowForChats();
-          let chatbar = win.SocialChatBar.chatbar;
-          chatbar.openChat(provider, "about:blank", win => {
-            let cb = chatbar.selectedChat;
-            this.swapDocShells(cb);
+          let Chat = Cu.import("resource:///modules/Chat.jsm").Chat;
+          let win = Chat.findChromeWindowForChats();
+          let chatbar = win.document.getElementById("pinnedchats");
+          let origin = this.content.getAttribute("origin");
+          let cb = chatbar.openChat(origin, title, "about:blank");
+          cb.promiseChatLoaded.then(
+            () => {
+              this.swapDocShells(cb);
 
-            // chatboxForURL is a map of URL -> chatbox used to avoid opening
-            // duplicate chat windows. Ensure reattached chat windows aren't
-            // registered with about:blank as their URL, otherwise reattaching
-            // more than one chat window isn't possible.
-            chatbar.chatboxForURL.delete("about:blank");
-            chatbar.chatboxForURL.set(this.src, Cu.getWeakReference(cb));
+              // chatboxForURL is a map of URL -> chatbox used to avoid opening
+              // duplicate chat windows. Ensure reattached chat windows aren't
+              // registered with about:blank as their URL, otherwise reattaching
+              // more than one chat window isn't possible.
+              chatbar.chatboxForURL.delete("about:blank");
+              chatbar.chatboxForURL.set(this.src, Cu.getWeakReference(cb));
 
-            chatbar.focus();
-            this.close();
-          });
+              chatbar.focus();
+              this.close();
+              deferred.resolve(cb);
+            }
+          );
         }
+        return deferred.promise;
         ]]></body>
       </method>
 
       <method name="toggle">
         <body><![CDATA[
           this.minimized = !this.minimized;
         ]]></body>
       </method>
@@ -505,76 +499,86 @@
             this._selectAnotherChat();
           }
         ]]></body>
       </method>
 
       <method name="_remove">
         <parameter name="aChatbox"/>
         <body><![CDATA[
-          aChatbox.content.socialErrorListener.remove();
           this.removeChild(aChatbox);
           // child might have been collapsed.
           let menuitem = this.menuitemMap.get(aChatbox);
           if (menuitem) {
             this.menuitemMap.delete(aChatbox);
             this.menupopup.removeChild(menuitem);
           }
           this.chatboxForURL.delete(aChatbox.src);
         ]]></body>
       </method>
 
+      <!-- XXX - we should remove this - it is currently used only by tests.
+           No-one has any business removing all chats.  Chat.jsm exposes a
+           method to close all chats from a specific origin which is all that
+           should be necessary.
+      -->
       <method name="removeAll">
         <body><![CDATA[
           this.selectedChat = null;
           while (this.firstElementChild) {
             this._remove(this.firstElementChild);
           }
           // and the nub/popup must also die.
           this.nub.collapsed = true;
         ]]></body>
       </method>
 
       <method name="openChat">
-        <parameter name="aProvider"/>
+        <parameter name="aOrigin"/>
+        <parameter name="aTitle"/>
         <parameter name="aURL"/>
+        <parameter name="aMode"/>
         <parameter name="aCallback"/>
-        <parameter name="aMode"/>
         <body><![CDATA[
           let cb = this.chatboxForURL.get(aURL);
           if (cb) {
             cb = cb.get();
             if (cb.parentNode) {
               this.showChat(cb, aMode);
               if (aCallback) {
                 if (cb._callbacks == null) {
-                  // DOMContentLoaded has already fired, so callback now.
-                  aCallback(cb.contentWindow);
+                  // Chatbox has already been created, so callback now.
+                  aCallback(cb);
                 } else {
-                  // DOMContentLoaded for this chat is yet to fire...
+                  // Chatbox is yet to have bindings created...
                   cb._callbacks.push(aCallback);
                 }
               }
-              return;
+              return cb;
             }
             this.chatboxForURL.delete(aURL);
           }
           cb = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "chatbox");
-          // _callbacks is a javascript property instead of a <field> as it
-          // must exist before the (possibly delayed) bindings are created.
-          cb._callbacks = [aCallback];
+          cb._callbacks = [];
+          if (aCallback) {
+            // _callbacks is a javascript property instead of a <field> as it
+            // must exist before the (possibly delayed) bindings are created.
+            cb._callbacks.push(aCallback);
+          }
           // src also a javascript property; the src attribute is set in the ctor.
           cb.src = aURL;
           if (aMode == "minimized")
             cb.setAttribute("minimized", "true");
-          cb.setAttribute("origin", aProvider.origin);
+          cb.setAttribute("origin", aOrigin);
+          cb.setAttribute("label", aTitle);
           this.insertBefore(cb, this.firstChild);
           this.selectedChat = cb;
           this.chatboxForURL.set(aURL, Cu.getWeakReference(cb));
           this.resize();
+          return cb;
         ]]></body>
       </method>
 
       <method name="resize">
         <body><![CDATA[
         // Checks the current size against the collapsed state of children
         // and collapses or expands as necessary such that as many as possible
         // are shown.
@@ -643,40 +647,42 @@
 
       <method name="_getDragTarget">
         <parameter name="event"/>
         <body><![CDATA[
           return event.target.localName == "chatbox" ? event.target : null;
         ]]></body>
       </method>
 
-      <!-- Moves a chatbox to a new window. -->
+      <!-- Moves a chatbox to a new window. Returns a promise that is resolved
+           once the move to the other window is complete.
+      -->
       <method name="detachChatbox">
         <parameter name="aChatbox"/>
         <parameter name="aOptions"/>
-        <parameter name="aCallback"/>
         <body><![CDATA[
+          let deferred = Promise.defer();
           let options = "";
           for (let name in aOptions)
             options += "," + name + "=" + aOptions[name];
 
           let otherWin = window.openDialog("chrome://browser/content/chatWindow.xul",
                                            "_blank", "chrome,all,dialog=no" + options);
 
           otherWin.addEventListener("load", function _chatLoad(event) {
             if (event.target != otherWin.document)
               return;
 
             otherWin.removeEventListener("load", _chatLoad, true);
             let otherChatbox = otherWin.document.getElementById("chatter");
             aChatbox.swapDocShells(otherChatbox);
             aChatbox.close();
-            if (aCallback)
-              aCallback(otherWin);
+            deferred.resolve(otherChatbox);
           }, true);
+          return deferred.promise;
         ]]></body>
       </method>
 
     </implementation>
 
     <handlers>
       <handler event="popupshown"><![CDATA[
         this.nub.removeAttribute("activity");
@@ -745,19 +751,20 @@
         let winWidth = 400;
         let winHeight = 420;
         // ensure new window entirely within screen
         let left = Math.min(Math.max(eX, sX.value),
                             sX.value + sWidth.value - winWidth);
         let top = Math.min(Math.max(eY, sY.value),
                            sY.value + sHeight.value - winHeight);
 
-        let provider = Social._getProviderFromOrigin(draggedChat.content.getAttribute("origin"));
-        this.detachChatbox(draggedChat, { screenX: left, screenY: top }, win => {
-          win.document.title = provider.name;
-        });
-
+        let title = draggedChat.content.getAttribute("title");
+        this.detachChatbox(draggedChat, { screenX: left, screenY: top }).then(
+          chatbox => {
+            chatbox.contentWindow.document.title = title;
+          }
+        );
         event.stopPropagation();
       ]]></handler>
     </handlers>
   </binding>
 
 </bindings>
--- a/browser/base/content/test/social/browser_social_chatwindow.js
+++ b/browser/base/content/test/social/browser_social_chatwindow.js
@@ -39,16 +39,21 @@ function openChat(provider, callback) {
     }
   }
   let url = chatUrl + "?" + (chatId++);
   port.postMessage({topic: "test-init"});
   port.postMessage({topic: "test-worker-chat", data: url});
   gURLsNotRemembered.push(url);
 }
 
+function windowHasChats(win) {
+  let chatbar = win.document.getElementById("pinnedchats");
+  return !!chatbar.firstElementChild;
+}
+
 function test() {
   requestLongerTimeout(2); // only debug builds seem to need more time...
   waitForExplicitFinish();
 
   let oldwidth = window.outerWidth; // we futz with these, so we restore them
   let oldleft = window.screenX;
   window.moveTo(0, window.screenY)
   let postSubTest = function(cb) {
@@ -336,71 +341,16 @@ var tests = {
       ok(!first.collapsed, "first should no longer be collapsed");
       ok(second.collapsed ||  third.collapsed, false, "one of the others should be collapsed");
       closeAllChats();
       port.close();
       next();
     });
   },
 
-  testActivity: function(next) {
-    let port = SocialSidebar.provider.getWorkerPort();
-    port.postMessage({topic: "test-init"});
-    get3ChatsForCollapsing("normal", function(first, second, third) {
-      let chatbar = window.SocialChatBar.chatbar;
-      is(chatbar.selectedChat, third, "third chat should be selected");
-      ok(!chatbar.selectedChat.hasAttribute("activity"), "third chat should have no activity");
-      // send an activity message to the second.
-      ok(!second.hasAttribute("activity"), "second chat should have no activity");
-      let chat2 = second.content;
-      let evt = chat2.contentDocument.createEvent("CustomEvent");
-      evt.initCustomEvent("socialChatActivity", true, true, {});
-      chat2.contentDocument.documentElement.dispatchEvent(evt);
-      // second should have activity.
-      ok(second.hasAttribute("activity"), "second chat should now have activity");
-      // select the second - it should lose "activity"
-      chatbar.selectedChat = second;
-      ok(!second.hasAttribute("activity"), "second chat should no longer have activity");
-      // Now try the first - it is collapsed, so the 'nub' also gets activity attr.
-      ok(!first.hasAttribute("activity"), "first chat should have no activity");
-      let chat1 = first.content;
-      let evt = chat1.contentDocument.createEvent("CustomEvent");
-      evt.initCustomEvent("socialChatActivity", true, true, {});
-      chat1.contentDocument.documentElement.dispatchEvent(evt);
-      ok(first.hasAttribute("activity"), "first chat should now have activity");
-      ok(chatbar.nub.hasAttribute("activity"), "nub should also have activity");
-      // first is collapsed, so use openChat to get it.
-      chatbar.openChat(SocialSidebar.provider, first.getAttribute("src"));
-      ok(!first.hasAttribute("activity"), "first chat should no longer have activity");
-      // The nub should lose the activity flag here too
-      todo(!chatbar.nub.hasAttribute("activity"), "Bug 806266 - nub should no longer have activity");
-      // TODO: tests for bug 806266 should arrange to have 2 chats collapsed
-      // then open them checking the nub is updated correctly.
-      // Now we will go and change the embedded browser in the second chat and
-      // ensure the activity magic still works (ie, check that the unload for
-      // the browser didn't cause our event handlers to be removed.)
-      ok(!second.hasAttribute("activity"), "second chat should have no activity");
-      let subiframe = chat2.contentDocument.getElementById("iframe");
-      subiframe.contentWindow.addEventListener("unload", function subunload() {
-        subiframe.contentWindow.removeEventListener("unload", subunload);
-        // ensure all other unload listeners have fired.
-        executeSoon(function() {
-          let evt = chat2.contentDocument.createEvent("CustomEvent");
-          evt.initCustomEvent("socialChatActivity", true, true, {});
-          chat2.contentDocument.documentElement.dispatchEvent(evt);
-          ok(second.hasAttribute("activity"), "second chat still has activity after unloading sub-iframe");
-          closeAllChats();
-          port.close();
-          next();
-        })
-      })
-      subiframe.setAttribute("src", "data:text/plain:new location for iframe");
-    });
-  },
-
   testOnlyOneCallback: function(next) {
     let chats = document.getElementById("pinnedchats");
     let port = SocialSidebar.provider.getWorkerPort();
     let numOpened = 0;
     port.onmessage = function (e) {
       let topic = e.data.topic;
       switch (topic) {
         case "test-init-done":
@@ -443,55 +393,55 @@ var tests = {
     }
     port.postMessage({topic: "test-init"});
   },
 
   testChatWindowChooser: function(next) {
     // Tests that when a worker creates a chat, it is opened in the correct
     // window.
     // open a chat (it will open in the main window)
-    ok(!window.SocialChatBar.hasChats, "first window should start with no chats");
+    ok(!windowHasChats(window), "first window should start with no chats");
     openChat(SocialSidebar.provider, function() {
-      ok(window.SocialChatBar.hasChats, "first window has the chat");
+      ok(windowHasChats(window), "first window has the chat");
       // create a second window - this will be the "most recent" and will
       // therefore be the window that hosts the new chat (see bug 835111)
       let secondWindow = OpenBrowserWindow();
       secondWindow.addEventListener("load", function loadListener() {
         secondWindow.removeEventListener("load", loadListener);
-        ok(!secondWindow.SocialChatBar.hasChats, "second window has no chats");
+        ok(!windowHasChats(secondWindow), "second window has no chats");
         openChat(SocialSidebar.provider, function() {
-          ok(secondWindow.SocialChatBar.hasChats, "second window now has chats");
+          ok(windowHasChats(secondWindow), "second window now has chats");
           is(window.SocialChatBar.chatbar.childElementCount, 1, "first window still has 1 chat");
           window.SocialChatBar.chatbar.removeAll();
           // now open another chat - it should still open in the second.
           openChat(SocialSidebar.provider, function() {
-            ok(!window.SocialChatBar.hasChats, "first window has no chats");
-            ok(secondWindow.SocialChatBar.hasChats, "second window has a chat");
+            ok(!windowHasChats(window), "first window has no chats");
+            ok(windowHasChats(secondWindow), "second window has a chat");
 
             // focus the first window, and open yet another chat - it
             // should open in the first window.
             waitForFocus(function() {
               openChat(SocialSidebar.provider, function() {
-                ok(window.SocialChatBar.hasChats, "first window has chats");
+                ok(windowHasChats(window), "first window has chats");
                 window.SocialChatBar.chatbar.removeAll();
-                ok(!window.SocialChatBar.hasChats, "first window has no chats");
+                ok(!windowHasChats(window), "first window has no chats");
 
                 let privateWindow = OpenBrowserWindow({private: true});
                 privateWindow.addEventListener("load", function loadListener() {
                   privateWindow.removeEventListener("load", loadListener);
 
                   // open a last chat - the focused window can't accept
                   // chats (it's a private window), so the chat should open
                   // in the window that was selected before. This is known
                   // to be broken on Linux.
                   openChat(SocialSidebar.provider, function() {
                     let os = Services.appinfo.OS;
                     const BROKEN_WM_Z_ORDER = os != "WINNT" && os != "Darwin";
                     let fn = BROKEN_WM_Z_ORDER ? todo : ok;
-                    fn(window.SocialChatBar.hasChats, "first window has a chat");
+                    fn(windowHasChats(window), "first window has a chat");
                     window.SocialChatBar.chatbar.removeAll();
 
                     privateWindow.close();
                     secondWindow.close();
                     next();
                   });
                 });
               });
--- a/browser/base/content/test/social/browser_social_errorPage.js
+++ b/browser/base/content/test/social/browser_social_errorPage.js
@@ -169,10 +169,41 @@ var tests = {
                            function() {
                             chat.close();
                             next();
                             },
                            "error page didn't appear");
         });
       }
     );
+  },
+
+  testChatWindowAfterTearOff: function(next) {
+    // Ensure that the error listener survives the chat window being detached.
+    let url = "https://example.com/browser/browser/base/content/test/social/social_chat.html";
+    let panelCallbackCount = 0;
+    // open a chat while we are still online.
+    openChat(
+      url,
+      null,
+      function() { // the "load" callback.
+        executeSoon(function() {
+          let chat = SocialChatBar.chatbar.selectedChat;
+          is(chat.contentDocument.location.href, url, "correct url loaded");
+          // toggle to a detached window.
+          chat.swapWindows().then(
+            chat => {
+              // now go offline and reload the chat - about:socialerror should be loaded.
+              goOffline();
+              chat.contentDocument.location.reload();
+              waitForCondition(function() chat.contentDocument.location.href.indexOf("about:socialerror?")==0,
+                               function() {
+                                chat.close();
+                                next();
+                                },
+                               "error page didn't appear");
+            }
+          );
+        });
+      }
+    );
   }
 }
--- a/browser/base/content/test/social/head.js
+++ b/browser/base/content/test/social/head.js
@@ -232,18 +232,16 @@ function checkSocialUI(win) {
       is(a, b, msg)
     else
       ++numGoodTests;
   }
   function isbool(a, b, msg) {
     _is(!!a, !!b, msg);
   }
   isbool(win.SocialSidebar.canShow, sidebarEnabled, "social sidebar active?");
-  isbool(win.SocialChatBar.isAvailable, enabled, "chatbar available?");
-  isbool(!win.SocialChatBar.chatbar.hidden, enabled, "chatbar visible?");
 
   let contextMenus = [
     {
       type: "link",
       id: "context-marklinkMenu",
       label: "social.marklinkMenu.label"
     },
     {
@@ -274,18 +272,16 @@ function checkSocialUI(win) {
     }
     for (let m of menus)
       _is(m.parentNode, parent, "menu has correct parent");
   }
 
   // and for good measure, check all the social commands.
   isbool(!doc.getElementById("Social:ToggleSidebar").hidden, sidebarEnabled, "Social:ToggleSidebar visible?");
   isbool(!doc.getElementById("Social:ToggleNotifications").hidden, enabled, "Social:ToggleNotifications visible?");
-  isbool(!doc.getElementById("Social:FocusChat").hidden, enabled, "Social:FocusChat visible?");
-  isbool(doc.getElementById("Social:FocusChat").getAttribute("disabled"), enabled ? "false" : "true", "Social:FocusChat disabled?");
 
   // and report on overall success of failure of the various checks here.
   is(numGoodTests, numTests, "The Social UI tests succeeded.")
 }
 
 function waitForNotification(topic, cb) {
   function observer(subject, topic, data) {
     Services.obs.removeObserver(observer, topic);
new file mode 100644
--- /dev/null
+++ b/browser/modules/Chat.jsm
@@ -0,0 +1,191 @@
+/* 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/. */
+"use strict";
+
+// A module for working with chat windows.
+
+this.EXPORTED_SYMBOLS = ["Chat"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+  "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+// A couple of internal helper function.
+function isWindowChromeless(win) {
+  // XXX - stolen from browser-social.js, but there's no obvious place to
+  // put this so it can be shared.
+
+  // Is this a popup window that doesn't want chrome shown?
+  let docElem = win.document.documentElement;
+  // extrachrome is not restored during session restore, so we need
+  // to check for the toolbar as well.
+  let chromeless = docElem.getAttribute("chromehidden").contains("extrachrome") ||
+                   docElem.getAttribute('chromehidden').contains("toolbar");
+  return chromeless;
+}
+
+function isWindowGoodForChats(win) {
+  return !win.closed &&
+         !!win.document.getElementById("pinnedchats") &&
+         !isWindowChromeless(win) &&
+         !PrivateBrowsingUtils.isWindowPrivate(win);
+}
+
+function getChromeWindow(contentWin) {
+  return contentWin.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIWebNavigation)
+                   .QueryInterface(Ci.nsIDocShellTreeItem)
+                   .rootTreeItem
+                   .QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIDOMWindow);
+}
+
+/*
+ * The exported Chat object
+ */
+
+let Chat = {
+  /**
+   * Open a new chatbox.
+   *
+   * @param contentWindow [optional]
+   *        The content window that requested this chat.  May be null.
+   * @param origin
+   *        The origin for the chat.  This is primarily used as an identifier
+   *        to help identify all chats from the same provider.
+   * @param title
+   *        The title to be used if a new chat window is created.
+   * @param url
+   *        The URL for the that.  Should be under the origin.  If an existing
+   *        chatbox exists with the same URL, it will be reused and returned.
+   * @param mode [optional]
+   *        May be undefined or 'minimized'
+   * @param focus [optional]
+   *        Indicates if the chatbox should be focused.  If undefined the chat
+   *        will be focused if the window is currently handling user input (ie,
+   *        if the chat is being opened as a direct result of user input)
+
+   * @return A chatbox binding.  This binding has a number of promises which
+   *         can be used to determine when the chatbox is being created and
+   *         has loaded.  Will return null if no chat can be created (Which
+   *         should only happen in edge-cases)
+   */
+  open: function(contentWindow, origin, title, url, mode, focus, callback) {
+    let chromeWindow = this.findChromeWindowForChats(contentWindow);
+    if (!chromeWindow) {
+      Cu.reportError("Failed to open a chat window - no host window could be found.");
+      return null;
+    }
+
+    let chatbar = chromeWindow.document.getElementById("pinnedchats");
+    chatbar.hidden = false;
+    let chatbox = chatbar.openChat(origin, title, url, mode, callback);
+    // getAttention is ignored if the target window is already foreground, so
+    // we can call it unconditionally.
+    chromeWindow.getAttention();
+    // If focus is undefined we want automatic focus handling, and only focus
+    // if a direct result of user action.
+    if (focus === undefined) {
+      let dwu = chromeWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                            .getInterface(Ci.nsIDOMWindowUtils);
+      focus = dwu.isHandlingUserInput;
+    }
+    if (focus) {
+      chatbar.focus();
+    }
+    return chatbox;
+  },
+
+  /**
+   * Close all chats from the specified origin.
+   *
+   * @param origin
+   *        The origin from which all chats should be closed.
+   */
+  closeAll: function(origin) {
+    // close all attached chat windows
+    let winEnum = Services.wm.getEnumerator("navigator:browser");
+    while (winEnum.hasMoreElements()) {
+      let win = winEnum.getNext();
+      let chatbar = win.document.getElementById("pinnedchats");
+      if (!chatbar)
+        continue;
+      let chats = [c for (c of chatbar.children) if (c.content.getAttribute("origin") == origin)];
+      [c.close() for (c of chats)];
+    }
+
+    // close all standalone chat windows
+    winEnum = Services.wm.getEnumerator("Social:Chat");
+    while (winEnum.hasMoreElements()) {
+      let win = winEnum.getNext();
+      if (win.closed)
+        continue;
+      let chatOrigin = win.document.getElementById("chatter").content.getAttribute("origin");
+      if (origin == chatOrigin)
+        win.close();
+    }
+  },
+
+  /**
+   * Focus the chatbar associated with a window
+   *
+   * @param window
+   */
+  focus: function(win) {
+    let chatbar = win.document.getElementById("pinnedchats");
+    if (chatbar && !chatbar.hidden) {
+      chatbar.focus();
+    }
+
+  },
+
+  // This is exported as socialchat.xml needs to find a window when a chat
+  // is re-docked.
+  findChromeWindowForChats: function(preferredWindow) {
+    if (preferredWindow) {
+      preferredWindow = getChromeWindow(preferredWindow);
+      if (isWindowGoodForChats(preferredWindow)) {
+        return preferredWindow;
+      }
+    }
+    // no good - we just use the "most recent" browser window which can host
+    // chats (we used to try and "group" all chats in the same browser window,
+    // but that didn't work out so well - see bug 835111
+
+    // Try first the most recent window as getMostRecentWindow works
+    // even on platforms where getZOrderDOMWindowEnumerator is broken
+    // (ie. Linux).  This will handle most cases, but won't work if the
+    // foreground window is a popup.
+
+    let mostRecent = Services.wm.getMostRecentWindow("navigator:browser");
+    if (isWindowGoodForChats(mostRecent))
+      return mostRecent;
+
+    let topMost, enumerator;
+    // *sigh* - getZOrderDOMWindowEnumerator is broken except on Mac and
+    // Windows.  We use BROKEN_WM_Z_ORDER as that is what some other code uses
+    // and a few bugs recommend searching mxr for this symbol to identify the
+    // workarounds - we want this code to be hit in such searches.
+    let os = Services.appinfo.OS;
+    const BROKEN_WM_Z_ORDER = os != "WINNT" && os != "Darwin";
+    if (BROKEN_WM_Z_ORDER) {
+      // this is oldest to newest and no way to change the order.
+      enumerator = Services.wm.getEnumerator("navigator:browser");
+    } else {
+      // here we explicitly ask for bottom-to-top so we can use the same logic
+      // where BROKEN_WM_Z_ORDER is true.
+      enumerator = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", false);
+    }
+    while (enumerator.hasMoreElements()) {
+      let win = enumerator.getNext();
+      if (!win.closed && isWindowGoodForChats(win))
+        topMost = win;
+    }
+    return topMost;
+  },
+}
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -4,16 +4,17 @@
 # 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/.
 
 TEST_DIRS += ['test']
 
 EXTRA_JS_MODULES += [
     'BrowserNewTabPreloader.jsm',
     'BrowserUITelemetry.jsm',
+    'Chat.jsm',
     'ContentClick.jsm',
     'ContentLinkHandler.jsm',
     'ContentSearch.jsm',
     'CustomizationTabPreloader.jsm',
     'Feeds.jsm',
     'NetworkPrioritizer.jsm',
     'offlineAppCache.jsm',
     'RemotePrompt.jsm',
--- a/toolkit/components/social/MozSocialAPI.jsm
+++ b/toolkit/components/social/MozSocialAPI.jsm
@@ -3,16 +3,18 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "SocialService", "resource://gre/modules/SocialService.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Social", "resource:///modules/Social.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 this.EXPORTED_SYMBOLS = ["MozSocialAPI", "openChatWindow", "findChromeWindowForChats", "closeAllChatWindows"];
 
 this.MozSocialAPI = {
   _enabled: false,
   _everEnabled: false,
   set enabled(val) {
@@ -120,17 +122,17 @@ function attachToWindow(provider, target
       }
     },
     openChatWindow: {
       enumerable: true,
       configurable: true,
       writable: true,
       value: function(toURL, callback) {
         let url = targetWindow.document.documentURIObject.resolve(toURL);
-        openChatWindow(getChromeWindow(targetWindow), provider, url, callback);
+        openChatWindow(targetWindow, provider, url, callback);
       }
     },
     openPanel: {
       enumerable: true,
       configurable: true,
       writable: true,
       value: function(toURL, offset, callback) {
         let chromeWindow = getChromeWindow(targetWindow);
@@ -265,97 +267,36 @@ function getChromeWindow(contentWin) {
   return contentWin.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIWebNavigation)
                    .QueryInterface(Ci.nsIDocShellTreeItem)
                    .rootTreeItem
                    .QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIDOMWindow);
 }
 
-function isWindowGoodForChats(win) {
-  return win.SocialChatBar
-         && win.SocialChatBar.isAvailable
-         && !PrivateBrowsingUtils.isWindowPrivate(win);
-}
-
-function findChromeWindowForChats(preferredWindow) {
-  if (preferredWindow && isWindowGoodForChats(preferredWindow))
-    return preferredWindow;
-  // no good - we just use the "most recent" browser window which can host
-  // chats (we used to try and "group" all chats in the same browser window,
-  // but that didn't work out so well - see bug 835111
-
-  // Try first the most recent window as getMostRecentWindow works
-  // even on platforms where getZOrderDOMWindowEnumerator is broken
-  // (ie. Linux).  This will handle most cases, but won't work if the
-  // foreground window is a popup.
-
-  let mostRecent = Services.wm.getMostRecentWindow("navigator:browser");
-  if (isWindowGoodForChats(mostRecent))
-    return mostRecent;
-
-  let topMost, enumerator;
-  // *sigh* - getZOrderDOMWindowEnumerator is broken except on Mac and
-  // Windows.  We use BROKEN_WM_Z_ORDER as that is what some other code uses
-  // and a few bugs recommend searching mxr for this symbol to identify the
-  // workarounds - we want this code to be hit in such searches.
-  let os = Services.appinfo.OS;
-  const BROKEN_WM_Z_ORDER = os != "WINNT" && os != "Darwin";
-  if (BROKEN_WM_Z_ORDER) {
-    // this is oldest to newest and no way to change the order.
-    enumerator = Services.wm.getEnumerator("navigator:browser");
-  } else {
-    // here we explicitly ask for bottom-to-top so we can use the same logic
-    // where BROKEN_WM_Z_ORDER is true.
-    enumerator = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", false);
-  }
-  while (enumerator.hasMoreElements()) {
-    let win = enumerator.getNext();
-    if (!win.closed && isWindowGoodForChats(win))
-      topMost = win;
-  }
-  return topMost;
-}
-
 this.openChatWindow =
- function openChatWindow(chromeWindow, provider, url, callback, mode) {
-  chromeWindow = findChromeWindowForChats(chromeWindow);
-  if (!chromeWindow) {
-    Cu.reportError("Failed to open a social chat window - no host window could be found.");
-    return;
-  }
+ function openChatWindow(contentWindow, provider, url, callback, mode) {
   let fullURI = provider.resolveUri(url);
   if (!provider.isSameOrigin(fullURI)) {
     Cu.reportError("Failed to open a social chat window - the requested URL is not the same origin as the provider.");
     return;
   }
-  if (!chromeWindow.SocialChatBar.openChat(provider, fullURI.spec, callback, mode)) {
-    Cu.reportError("Failed to open a social chat window - the chatbar is not available in the target window.");
-    return;
+
+  let thisCallback = function(chatbox) {
+    // All social chat windows get a special error listener.
+    Social.setErrorListener(chatbox.content, function(aBrowser) {
+      aBrowser.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" +
+                             encodeURIComponent(aBrowser.getAttribute("origin")),
+                             null, null, null, null);
+    });
   }
-  // getAttention is ignored if the target window is already foreground, so
-  // we can call it unconditionally.
-  chromeWindow.getAttention();
+  let chatbox = Chat.open(contentWindow, provider.origin, provider.name,
+                          fullURI.spec, mode, undefined, thisCallback);
+  if (callback) {
+    chatbox.promiseChatLoaded.then(() => {
+      callback(chatbox.contentWindow);
+    });
+  }
 }
 
-this.closeAllChatWindows =
- function closeAllChatWindows(provider) {
-  // close all attached chat windows
-  let winEnum = Services.wm.getEnumerator("navigator:browser");
-  while (winEnum.hasMoreElements()) {
-    let win = winEnum.getNext();
-    if (!win.SocialChatBar)
-      continue;
-    let chats = [c for (c of win.SocialChatBar.chatbar.children) if (c.content.getAttribute("origin") == provider.origin)];
-    [c.close() for (c of chats)];
-  }
-
-  // close all standalone chat windows
-  winEnum = Services.wm.getEnumerator("Social:Chat");
-  while (winEnum.hasMoreElements()) {
-    let win = winEnum.getNext();
-    if (win.closed)
-      continue;
-    let origin = win.document.getElementById("chatter").content.getAttribute("origin");
-    if (provider.origin == origin)
-      win.close();
-  }
+this.closeAllChatWindows = function closeAllChatWindows(provider) {
+  return Chat.closeAll(provider.origin);
 }