bug 880911 implement tear off chat windows, r=felipe
authorShane Caraveo <scaraveo@mozilla.com>
Fri, 21 Jun 2013 10:07:27 -0700
changeset 147550 36d494a9b7952e4d502ec9382939cbcf36b34eea
parent 147549 768a6a62fbc664ef88feb62c7897d774e9ab5b32
child 147551 13d534ef657be9038141536071c638c96f04fb88
push id2697
push userbbajaj@mozilla.com
push dateMon, 05 Aug 2013 18:49:53 +0000
treeherdermozilla-beta@dfec938c7b63 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfelipe
bugs880911
milestone24.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 880911 implement tear off chat windows, r=felipe
browser/base/content/browser-social.js
browser/base/content/chatWindow.xul
browser/base/content/socialchat.xml
browser/base/content/test/social/Makefile.in
browser/base/content/test/social/browser_chat_tearoff.js
browser/base/content/test/social/browser_social_chatwindow.js
browser/base/content/test/social/browser_social_chatwindowfocus.js
browser/base/content/test/social/browser_social_errorPage.js
browser/base/jar.mn
browser/themes/linux/jar.mn
browser/themes/linux/social/chat-close.png
browser/themes/linux/social/chat-icons.png
browser/themes/osx/browser.css
browser/themes/osx/jar.mn
browser/themes/osx/social/chat-close.png
browser/themes/osx/social/chat-icons.png
browser/themes/shared/social/chat.inc.css
browser/themes/windows/jar.mn
browser/themes/windows/social/chat-close.png
browser/themes/windows/social/chat-icons.png
toolkit/components/social/MozSocialAPI.jsm
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -83,16 +83,17 @@ SocialUI = {
       switch (topic) {
         case "social:provider-set":
           // Social.provider has changed (possibly to null), update any state
           // which depends on it.
           this._updateActiveUI();
           this._updateMenuItems();
 
           SocialFlyout.unload();
+          SocialChatBar.closeWindows();
           SocialChatBar.update();
           SocialShare.update();
           SocialSidebar.update();
           SocialMark.update();
           SocialToolbar.update();
           SocialMenu.populate();
           break;
         case "social:providers-changed":
@@ -342,16 +343,24 @@ SocialUI = {
     return !!Social.provider;
   },
 
 }
 
 SocialChatBar = {
   init: function() {
   },
+  closeWindows: function() {
+    // close all windows of type Social:Chat
+    let windows = Services.wm.getEnumerator("Social:Chat");
+    while (windows.hasMoreElements()) {
+      let win = windows.getNext();
+      win.close();
+    }
+  },
   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 && Social.haveLoggedInUser();
   },
new file mode 100644
--- /dev/null
+++ b/browser/base/content/chatWindow.xul
@@ -0,0 +1,89 @@
+#filter substitution
+<?xml version="1.0"?>
+
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# 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/.
+
+<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
+
+#include browser-doctype.inc
+
+<window id="chat-window"
+        windowtype="Social:Chat"
+        xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        title="&mainWindow.title;@PRE_RELEASE_SUFFIX@"
+        onload="gChatWindow.onLoad();"
+        onunload="gChatWindow.onUnload();"
+        macanimationtype="document"
+        fullscreenbutton="true"
+# width and height are also used in socialchat.xml: chatbar dragend handler
+        width="400px"
+        height="420px"
+        persist="screenX screenY width height sizemode">
+
+  <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+  <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
+  <script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/>
+
+#include global-scripts.inc
+
+<script type="application/javascript">
+
+var gChatWindow = {
+  // cargo-culted from browser.js for nonBrowserStartup, but we're slightly
+  // different what what we need to leave enabled
+  onLoad: function() {
+    // Disable inappropriate commands / submenus
+    var disabledItems = ['Browser:SavePage',
+                         'Browser:SendLink', 'cmd_pageSetup', 'cmd_print', 'cmd_find', 'cmd_findAgain',
+                         'viewToolbarsMenu', 'viewSidebarMenuMenu',
+                         'viewFullZoomMenu', 'pageStyleMenu', 'charsetMenu',
+                         'viewHistorySidebar', 'Browser:AddBookmarkAs', 'Browser:BookmarkAllTabs',
+                         'Browser:ToggleTabView', 'Browser:ToggleAddonBar'];
+
+    for (let disabledItem of disabledItems) {
+      document.getElementById(disabledItem).setAttribute("disabled", "true");
+    }
+
+    // initialise the offline listener
+    BrowserOffline.init();
+  },
+
+  onUnload: function() {
+    BrowserOffline.uninit();
+  }
+}
+
+</script>
+
+#include browser-sets.inc
+
+#include browser-menubar.inc
+
+  <popupset id="mainPopupSet">
+    <tooltip id="aHTMLTooltip" page="true"/>
+    <menupopup id="contentAreaContextMenu" pagemenu="start"
+               onpopupshowing="if (event.target != this)
+                                 return true;
+                               gContextMenu = new nsContextMenu(this, event.shiftKey);
+                               if (gContextMenu.shouldDisplay)
+                                 document.popupNode = this.triggerNode;
+                               return gContextMenu.shouldDisplay;"
+               onpopuphiding="if (event.target != this)
+                                return;
+                              gContextMenu.hiding();
+                              gContextMenu = null;">
+#include browser-context.inc
+    </menupopup>
+  </popupset>
+
+  <commandset id="editMenuCommands"/>
+  <chatbox id="chatter" flex="1"/>
+</window>
--- a/browser/base/content/socialchat.xml
+++ b/browser/base/content/socialchat.xml
@@ -2,135 +2,211 @@
 
 <bindings id="socialChatBindings"
     xmlns="http://www.mozilla.org/xbl"
     xmlns:xbl="http://www.mozilla.org/xbl"
     xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <binding id="chatbox">
     <content orient="vertical" mousethrough="never">
-      <xul:hbox class="chat-titlebar" xbl:inherits="minimized,selected,activity"
-                onclick="document.getBindingParent(this).onTitlebarClick(event);" align="baseline">
-        <xul:image class="chat-status-icon" xbl:inherits="src=image"/>
-        <xul:label class="chat-title" flex="1" xbl:inherits="value=label" crop="center"/>
-        <xul:toolbarbutton class="chat-close-button chat-toolbarbutton"
+      <xul:hbox class="chat-titlebar" xbl:inherits="minimized,selected,activity" align="baseline">
+        <xul:hbox flex="1" onclick="document.getBindingParent(this).onTitlebarClick(event);">
+          <xul:image class="chat-status-icon" xbl:inherits="src=image"/>
+          <xul:label class="chat-title" flex="1" xbl:inherits="value=label" crop="center"/>
+        </xul:hbox>
+        <xul:toolbarbutton anonid="minimize" class="chat-minimize-button chat-toolbarbutton"
+                           oncommand="document.getBindingParent(this).toggle();"/>
+        <xul:toolbarbutton anonid="swap" class="chat-swap-button chat-toolbarbutton"
+                           oncommand="document.getBindingParent(this).swapWindows();"/>
+        <xul:toolbarbutton anonid="close" class="chat-close-button chat-toolbarbutton"
                            oncommand="document.getBindingParent(this).close();"/>
       </xul:hbox>
-      <xul:iframe anonid="iframe" class="chat-frame" flex="1"
+      <xul:browser anonid="content" class="chat-frame" flex="1"
                   context="contentAreaContextMenu"
+                  disableglobalhistory="true"
                   tooltip="aHTMLTooltip"
-                  xbl:inherits="src,origin,collapsed=minimized" type="content"/>
+                  xbl:inherits="src,origin" type="content"/>
     </content>
 
     <implementation implements="nsIDOMEventListener">
       <constructor><![CDATA[
         let Social = Components.utils.import("resource:///modules/Social.jsm", {}).Social;
-        Social.setErrorListener(this.iframe, function(iframe) {
-          iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo", null, null, null, null);
+        Social.setErrorListener(this.content, function(aBrowser) {
+          aBrowser.webNavigation.loadURI("about:socialerror?mode=compactInfo", null, null, null, null);
         });
-        let iframeWindow = this.iframe.contentWindow;
+        if (!this.chatbar) {
+          document.getAnonymousElementByAttribute(this, "anonid", "minimize").hidden = true;
+          document.getAnonymousElementByAttribute(this, "anonid", "close").hidden = true;
+        }
+        let contentWindow = this.contentWindow;
         this.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) {
-          if (event.target != this.iframe.contentDocument)
+          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.
-          for (let callback of this._callbacks) {
-            if (callback)
-              callback(iframeWindow);
+          if (this._callbacks) {
+            for (let callback of this._callbacks) {
+              if (callback)
+                callback(contentWindow);
+            }
+            this._callbacks = null;
           }
-          this._callbacks = null;
 
           // content can send a socialChatActivity event to have the UI update.
           let chatActivity = function() {
             this.setAttribute("activity", true);
-            this.parentNode.updateTitlebar(this);
+            if (this.chatbar)
+              this.chatbar.updateTitlebar(this);
           }.bind(this);
-          iframeWindow.addEventListener("socialChatActivity", chatActivity);
-          iframeWindow.addEventListener("unload", function unload() {
-            iframeWindow.removeEventListener("unload", unload);
-            iframeWindow.removeEventListener("socialChatActivity", chatActivity);
+          contentWindow.addEventListener("socialChatActivity", chatActivity);
+          contentWindow.addEventListener("unload", function unload() {
+            contentWindow.removeEventListener("unload", unload);
+            contentWindow.removeEventListener("socialChatActivity", chatActivity);
           });
         }, true);
-        this.setAttribute("src", this.src);
+        if (this.src)
+          this.setAttribute("src", this.src);
       ]]></constructor>
 
-      <field name="iframe" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "iframe");
+      <field name="content" readonly="true">
+        document.getAnonymousElementByAttribute(this, "anonid", "content");
       </field>
 
+      <property name="contentWindow">
+        <getter>
+          return this.content.contentWindow;
+        </getter>
+      </property>
+
+      <property name="contentDocument">
+        <getter>
+          return this.content.contentDocument;
+        </getter>
+      </property>
+
       <property name="minimized">
         <getter>
           return this.getAttribute("minimized") == "true";
         </getter>
         <setter><![CDATA[
           // Note that this.isActive is set via our transitionend handler so
           // the content doesn't see intermediate values.
-          let parent = this.parentNode;
+          let parent = this.chatbar;
           if (val) {
             this.setAttribute("minimized", "true");
             // If this chat is the selected one a new one needs to be selected.
-            if (parent.selectedChat == this)
+            if (parent && parent.selectedChat == this)
               parent._selectAnotherChat();
           } else {
             this.removeAttribute("minimized");
             // this chat gets selected.
-            parent.selectedChat = this;
+            if (parent)
+              parent.selectedChat = this;
           }
         ]]></setter>
       </property>
 
+      <property name="chatbar">
+        <getter>
+          if (this.parentNode.nodeName == "chatbar")
+            return this.parentNode;
+          return null;
+        </getter>
+      </property>
+
       <property name="isActive">
         <getter>
-          return this.iframe.docShell.isActive;
+          return this.content.docShell.isActive;
         </getter>
         <setter>
-          this.iframe.docShell.isActive = !!val;
+          this.content.docShell.isActive = !!val;
 
           // let the chat frame know if it is being shown or hidden
-          let evt = this.iframe.contentDocument.createEvent("CustomEvent");
+          let evt = this.contentDocument.createEvent("CustomEvent");
           evt.initCustomEvent(val ? "socialFrameShow" : "socialFrameHide", true, true, {});
-          this.iframe.contentDocument.documentElement.dispatchEvent(evt);
+          this.contentDocument.documentElement.dispatchEvent(evt);
         </setter>
       </property>
 
+      <method name="swapDocShells">
+        <parameter name="aTarget"/>
+        <body><![CDATA[
+          aTarget.setAttribute('label', this.contentDocument.title);
+          aTarget.content.setAttribute("origin", this.content.getAttribute("origin"));
+          this.content.socialErrorListener.remove();
+          this.content.swapDocShells(aTarget.content);
+          Social.setErrorListener(this.content, function(aBrowser) {
+            aBrowser.webNavigation.loadURI("about:socialerror?mode=compactInfo", null, null, null, null);
+          });
+        ]]></body>
+      </method>
+
       <method name="onTitlebarClick">
         <parameter name="aEvent"/>
         <body><![CDATA[
+          if (!this.chatbar)
+            return;
           if (aEvent.button == 0) { // left-click: toggle minimized.
             this.toggle();
             // if we restored it, we want to focus it.
             if (!this.minimized)
-              this.parentNode.focus();
+              this.chatbar.focus();
           } else if (aEvent.button == 1) // middle-click: close chat
             this.close();
         ]]></body>
       </method>
 
       <method name="close">
         <body><![CDATA[
-          this.parentNode.remove(this);
+        if (this.chatbar)
+          this.chatbar.remove(this);
+        else
+          window.close();
+        ]]></body>
+      </method>
+
+      <method name="swapWindows">
+        <body><![CDATA[
+        let provider = Social._getProviderFromOrigin(this.content.getAttribute("origin"));
+        if (this.chatbar) {
+          this.chatbar.detachChatbox(this, { "centerscreen": "yes" }, win => {
+            win.document.title = provider.name;
+          });
+        } 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 => {
+            chatbar.selectedChat.swapDocShells(this);
+            chatbar.focus();
+            this.close();
+          });
+        }
         ]]></body>
       </method>
 
       <method name="toggle">
         <body><![CDATA[
           this.minimized = !this.minimized;
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="focus" phase="capturing">
-        this.parentNode.selectedChat = this;
+        if (this.chatbar)
+          this.chatbar.selectedChat = this;
       </handler>
       <handler event="DOMTitleChanged"><![CDATA[
-        this.setAttribute('label', this.iframe.contentDocument.title);
-        this.parentNode.updateTitlebar(this);
+        this.setAttribute('label', this.contentDocument.title);
+        if (this.chatbar)
+          this.chatbar.updateTitlebar(this);
       ]]></handler>
       <handler event="DOMLinkAdded"><![CDATA[
         // much of this logic is from DOMLinkHandler in browser.js
         // this sets the presence icon for a chat user, we simply use favicon style updating
         let link = event.originalTarget;
         let rel = link.rel && link.rel.toLowerCase();
         if (!link || !link.ownerDocument || !rel || !link.href)
           return;
@@ -138,17 +214,18 @@
           return;
 
         let uri = DOMLinkHandler.getLinkIconURI(link);
         if (!uri)
           return;
 
         // we made it this far, use it
         this.setAttribute('image', uri.spec);
-        this.parentNode.updateTitlebar(this);
+        if (this.chatbar)
+          this.chatbar.updateTitlebar(this);
       ]]></handler>
       <handler event="transitionend">
         if (this.isActive == this.minimized)
           this.isActive = !this.minimized;
       </handler>
     </handlers>
   </binding>
 
@@ -181,35 +258,35 @@
       <field name="nub" readonly="true">
         document.getAnonymousElementByAttribute(this, "anonid", "nub");
       </field>
 
       <method name="focus">
         <body><![CDATA[
           if (!this.selectedChat)
             return;
-          Services.focus.focusedWindow = this.selectedChat.iframe.contentWindow;
+          Services.focus.focusedWindow = this.selectedChat.contentWindow;
         ]]></body>
       </method>
 
       <method name="_isChatFocused">
         <parameter name="aChatbox"/>
         <body><![CDATA[
           // If there are no XBL bindings for the chat it can't be focused.
-          if (!aChatbox.iframe)
+          if (!aChatbox.content)
             return false;
           let fw = Services.focus.focusedWindow;
           if (!fw)
             return false;
-          // We want to see if the focused window is in the subtree below our iframe...
+          // We want to see if the focused window is in the subtree below our browser...
           let containingBrowser = fw.QueryInterface(Ci.nsIInterfaceRequestor)
                                     .getInterface(Ci.nsIWebNavigation)
                                     .QueryInterface(Ci.nsIDocShell)
                                     .chromeEventHandler;
-          return containingBrowser == aChatbox.iframe;
+          return containingBrowser == aChatbox.content;
         ]]></body>
       </method>
 
       <property name="selectedChat">
         <getter><![CDATA[
           return this._selectedChat;
         ]]></getter>
         <setter><![CDATA[
@@ -342,17 +419,17 @@
         <body><![CDATA[
           // we ensure that the cached width for a child of this type is
           // up-to-date so we can use it when resizing.
           this.getTotalChildWidth(aChatbox);
           aChatbox.collapsed = true;
           aChatbox.isActive = false;
           let menu = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "menuitem");
           menu.setAttribute("class", "menuitem-iconic");
-          menu.setAttribute("label", aChatbox.iframe.contentDocument.title);
+          menu.setAttribute("label", aChatbox.contentDocument.title);
           menu.setAttribute("image", aChatbox.getAttribute("image"));
           menu.chat = aChatbox;
           this.menuitemMap.set(aChatbox, menu);
           this.menupopup.appendChild(menu);
           this.nub.collapsed = false;
         ]]></body>
       </method>
 
@@ -399,17 +476,17 @@
             this._selectAnotherChat();
           }
         ]]></body>
       </method>
 
       <method name="_remove">
         <parameter name="aChatbox"/>
         <body><![CDATA[
-          aChatbox.iframe.socialErrorListener.remove();
+          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.getAttribute('src'));
@@ -436,17 +513,17 @@
           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.iframe.contentWindow);
+                  aCallback(cb.contentWindow);
                 } else {
                   // DOMContentLoaded for this chat is yet to fire...
                   cb._callbacks.push(aCallback);
                 }
               }
               return;
             }
             this.chatboxForURL.delete(aURL);
@@ -530,23 +607,127 @@
         <parameter name="aEvent"/>
         <body><![CDATA[
           if (aEvent.type == "resize") {
             this.resize();
           }
         ]]></body>
       </method>
 
+      <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. -->
+      <method name="detachChatbox">
+        <parameter name="aChatbox"/>
+        <parameter name="aOptions"/>
+        <parameter name="aCallback"/>
+        <body><![CDATA[
+          let options = "";
+          for (let name in aOptions)
+            options += "," + name + "=" + aOptions[name];
+
+          let otherWin = window.openDialog("chrome://browser/content/chatWindow.xul", null, "chrome,all" + 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);
+          }, true);
+        ]]></body>
+      </method>
+
     </implementation>
+
     <handlers>
       <handler event="popupshown"><![CDATA[
         this.nub.removeAttribute("activity");
       ]]></handler>
       <handler event="load"><![CDATA[
         window.addEventListener("resize", this);
       ]]></handler>
       <handler event="unload"><![CDATA[
         window.removeEventListener("resize", this);
       ]]></handler>
+
+      <handler event="dragstart"><![CDATA[
+        // chat window dragging is essentially duplicated from tabbrowser.xml
+        // to acheive the same visual experience
+        let chatbox = this._getDragTarget(event);
+        if (!chatbox) {
+          return;
+        }
+
+        let dt = event.dataTransfer;
+        // we do not set a url in the drag data to prevent moving to tabbrowser
+        // or otherwise having unexpected drop handlers do something with our
+        // chatbox
+        dt.mozSetDataAt("application/x-moz-chatbox", chatbox, 0);
+
+        // Set the cursor to an arrow during tab drags.
+        dt.mozCursor = "default";
+
+        // Create a canvas to which we capture the current tab.
+        // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
+        // canvas size (in CSS pixels) to the window's backing resolution in order
+        // to get a full-resolution drag image for use on HiDPI displays.
+        let windowUtils = window.getInterface(Ci.nsIDOMWindowUtils);
+        let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom;
+        let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+        canvas.mozOpaque = true;
+        canvas.width = 160 * scale;
+        canvas.height = 90 * scale;
+        PageThumbs.captureToCanvas(chatbox.contentWindow, canvas);
+        dt.setDragImage(canvas, -16 * scale, -16 * scale);
+
+        event.stopPropagation();
+      ]]></handler>
+
+      <handler event="dragend"><![CDATA[
+        let dt = event.dataTransfer;
+        let draggedChat = dt.mozGetDataAt("application/x-moz-chatbox", 0);
+        if (dt.mozUserCancelled || dt.dropEffect != "none") {
+          return;
+        }
+
+        let eX = event.screenX;
+        let eY = event.screenY;
+        // screen.availLeft et. al. only check the screen that this window is on,
+        // but we want to look at the screen the tab is being dropped onto.
+        let sX = {}, sY = {}, sWidth = {}, sHeight = {};
+        Cc["@mozilla.org/gfx/screenmanager;1"]
+          .getService(Ci.nsIScreenManager)
+          .screenForRect(eX, eY, 1, 1)
+          .GetAvailRect(sX, sY, sWidth, sHeight);
+        // default size for the chat window as used in chatWindow.xul, use them
+        // here to attempt to keep the window fully within the screen when
+        // opening at the drop point. If the user has resized the window to
+        // something larger (which gets persisted), at least a good portion of
+        // the window should still be within the screen.
+        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;
+        });
+
+        event.stopPropagation();
+      ]]></handler>
     </handlers>
   </binding>
 
 </bindings>
--- a/browser/base/content/test/social/Makefile.in
+++ b/browser/base/content/test/social/Makefile.in
@@ -12,16 +12,17 @@ include $(DEPTH)/config/autoconf.mk
 
 _BROWSER_FILES = \
                  head.js \
 		 blocklist.xml \
 		 blocklistEmpty.xml \
 		 browser_blocklist.js \
 		 browser_defaults.js \
 		 browser_addons.js \
+		 browser_chat_tearoff.js \
                  browser_social_activation.js \
                  browser_social_perwindowPB.js \
                  browser_social_toolbar.js \
                  browser_social_markButton.js \
                  browser_social_sidebar.js \
                  browser_social_flyout.js \
                  browser_social_mozSocial_API.js \
                  browser_social_isVisible.js \
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_chat_tearoff.js
@@ -0,0 +1,95 @@
+/* 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/. */
+
+function test() {
+  requestLongerTimeout(2); // only debug builds seem to need more time...
+  waitForExplicitFinish();
+
+  let manifest = { // normal provider
+    name: "provider 1",
+    origin: "https://example.com",
+    sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
+    workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
+    iconURL: "https://example.com/browser/browser/base/content/test/moz.png"
+  };
+
+  let postSubTest = function(cb) {
+    let chats = document.getElementById("pinnedchats");
+    ok(chats.children.length == 0, "no chatty children left behind");
+    cb();
+  };
+  runSocialTestWithProvider(manifest, function (finishcb) {
+    runSocialTests(tests, undefined, postSubTest, function() {
+      finishcb();
+    });
+  });
+}
+
+var tests = {
+  testTearoffChat: function(next) {
+    let chats = document.getElementById("pinnedchats");
+    let port = Social.provider.getWorkerPort();
+    ok(port, "provider has a port");
+    port.onmessage = function (e) {
+      let topic = e.data.topic;
+      switch (topic) {
+        case "got-sidebar-message":
+          port.postMessage({topic: "test-chatbox-open"});
+          break;
+        case "got-chatbox-visibility":
+          // chatbox is open, lets detach. The new chat window will be caught in
+          // the window watcher below
+          let doc = chats.selectedChat.contentDocument;
+          let div = doc.createElement("div");
+          div.setAttribute("id", "testdiv");
+          div.setAttribute("test", "1");
+          doc.body.appendChild(div);
+          let swap = document.getAnonymousElementByAttribute(chats.selectedChat, "anonid", "swap");
+          swap.click();
+          break;
+        case "got-chatbox-message":
+          ok(true, "got chatbox message");
+          ok(e.data.result == "ok", "got chatbox windowRef result: "+e.data.result);
+          chats.selectedChat.toggle();
+          break;
+      }
+    }
+
+    Services.wm.addListener({
+      onWindowTitleChange: function() {},
+      onCloseWindow: function(xulwindow) {
+        Services.wm.removeListener(this);
+        info("window has been closed");
+        waitForCondition(function() {
+          return chats.selectedChat && chats.selectedChat.contentDocument &&
+                 chats.selectedChat.contentDocument.readyState == "complete";
+        },function () {
+          ok(chats.selectedChat, "should have a chatbox in our window again");
+          let testdiv = chats.selectedChat.contentDocument.getElementById("testdiv");
+          is(testdiv.getAttribute("test"), "2", "docshell should have been swapped");
+          chats.selectedChat.close();
+          next();
+        });
+      },
+      onOpenWindow: function(xulwindow) {
+        var domwindow = xulwindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+                              .getInterface(Components.interfaces.nsIDOMWindow);
+        waitForFocus(function() {
+          let doc = domwindow.document
+          is(doc.documentElement.getAttribute("windowtype"), "Social:Chat", "Social:Chat window opened");
+          is(doc.location.href, "chrome://browser/content/chatWindow.xul", "Should have seen the right window open");
+          let chatbox = doc.getElementById("chatter");
+          let testdiv = chatbox.contentDocument.getElementById("testdiv");
+          is(testdiv.getAttribute("test"), "1", "docshell should have been swapped");
+          testdiv.setAttribute("test", "2");
+          // swap the window back to the chatbar
+          let swap = doc.getAnonymousElementByAttribute(chatbox, "anonid", "swap");
+          swap.click();
+        }, domwindow);
+      }
+    });
+
+    port.postMessage({topic: "test-init", data: { id: 1 }});
+  }
+}
\ No newline at end of file
--- a/browser/base/content/test/social/browser_social_chatwindow.js
+++ b/browser/base/content/test/social/browser_social_chatwindow.js
@@ -43,19 +43,19 @@ var tests = {
           break;
         case "got-chatbox-visibility":
           if (e.data.result == "hidden") {
             ok(true, "chatbox got minimized");
             chats.selectedChat.toggle();
           } else if (e.data.result == "shown") {
             ok(true, "chatbox got shown");
             // close it now
-            let iframe = chats.selectedChat.iframe;
-            iframe.addEventListener("unload", function chatUnload() {
-              iframe.removeEventListener("unload", chatUnload, true);
+            let content = chats.selectedChat.content;
+            content.addEventListener("unload", function chatUnload() {
+              content.removeEventListener("unload", chatUnload, true);
               ok(true, "got chatbox unload on close");
               port.close();
               next();
             }, true);
             chats.selectedChat.close();
           }
           break;
         case "got-chatbox-message":
@@ -182,17 +182,17 @@ var tests = {
           port.postMessage({topic: "test-chatbox-open"});
           break;
         case "got-chatbox-visibility":
           is(e.data.result, "shown", "chatbox shown");
           port.close(); // don't want any more visibility messages.
           let chat = chats.selectedChat;
           ok(chat.parentNode, "chat has a parent node before it is closed");
           // ask it to close itself.
-          let doc = chat.iframe.contentDocument;
+          let doc = chat.contentDocument;
           let evt = doc.createEvent("CustomEvent");
           evt.initCustomEvent("socialTest-CloseSelf", true, true, {});
           doc.documentElement.dispatchEvent(evt);
           ok(!chat.parentNode, "chat is now closed");
           port.close();
           next();
           break;
       }
@@ -346,52 +346,52 @@ var tests = {
     let port = Social.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 iframe2 = second.iframe;
-      let evt = iframe2.contentDocument.createEvent("CustomEvent");
+      let chat2 = second.content;
+      let evt = chat2.contentDocument.createEvent("CustomEvent");
       evt.initCustomEvent("socialChatActivity", true, true, {});
-      iframe2.contentDocument.documentElement.dispatchEvent(evt);
+      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 iframe1 = first.iframe;
-      let evt = iframe1.contentDocument.createEvent("CustomEvent");
+      let chat1 = first.content;
+      let evt = chat1.contentDocument.createEvent("CustomEvent");
       evt.initCustomEvent("socialChatActivity", true, true, {});
-      iframe1.contentDocument.documentElement.dispatchEvent(evt);
+      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(Social.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 iframe in the second chat and
+      // 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 iframe didn't cause our event handlers to be removed.)
+      // the browser didn't cause our event handlers to be removed.)
       ok(!second.hasAttribute("activity"), "second chat should have no activity");
-      let subiframe = iframe2.contentDocument.getElementById("iframe");
+      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 = iframe2.contentDocument.createEvent("CustomEvent");
+          let evt = chat2.contentDocument.createEvent("CustomEvent");
           evt.initCustomEvent("socialChatActivity", true, true, {});
-          iframe2.contentDocument.documentElement.dispatchEvent(evt);
+          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");
     });
--- a/browser/base/content/test/social/browser_social_chatwindowfocus.js
+++ b/browser/base/content/test/social/browser_social_chatwindowfocus.js
@@ -272,17 +272,17 @@ var tests = {
   // cause focus to move between all elements in our chat window before moving
   // to the next chat window.
   testTab: function(next) {
     function sendTabAndWaitForFocus(chat, eltid, callback) {
       // ideally we would use the 'focus' event here, but that doesn't work
       // as expected for the iframe - the iframe itself never gets the focus
       // event (apparently the sub-document etc does.)
       // So just poll for the correct element getting focus...
-      let doc = chat.iframe.contentDocument;
+      let doc = chat.contentDocument;
       EventUtils.sendKey("tab");
       waitForCondition(function() {
         let elt = eltid ? doc.getElementById(eltid) : doc.documentElement;
         return doc.activeElement == elt;
       }, callback, "element " + eltid + " never got focus");
     }
 
     let chatbar = SocialChatBar.chatbar;
@@ -293,26 +293,26 @@ var tests = {
           let chat2 = chat1.nextElementSibling || chat1.previousElementSibling;
           chatbar.selectedChat = chat2;
           chatbar.focus();
           waitForCondition(function() isChatFocused(chatbar.selectedChat),
                            function() {
             // Our chats have 3 focusable elements, so it takes 4 TABs to move
             // to the new chat.
             sendTabAndWaitForFocus(chat2, "input1", function() {
-              is(chat2.iframe.contentDocument.activeElement.getAttribute("id"), "input1",
+              is(chat2.contentDocument.activeElement.getAttribute("id"), "input1",
                  "first input field has focus");
               ok(isChatFocused(chat2), "new chat still focused after first tab");
               sendTabAndWaitForFocus(chat2, "input2", function() {
                 ok(isChatFocused(chat2), "new chat still focused after tab");
-                is(chat2.iframe.contentDocument.activeElement.getAttribute("id"), "input2",
+                is(chat2.contentDocument.activeElement.getAttribute("id"), "input2",
                    "second input field has focus");
                 sendTabAndWaitForFocus(chat2, "iframe", function() {
                   ok(isChatFocused(chat2), "new chat still focused after tab");
-                  is(chat2.iframe.contentDocument.activeElement.getAttribute("id"), "iframe",
+                  is(chat2.contentDocument.activeElement.getAttribute("id"), "iframe",
                      "iframe has focus");
                   // this tab now should move to the next chat, but focus the
                   // document element itself (hence the null eltid)
                   sendTabAndWaitForFocus(chat1, null, function() {
                     ok(isChatFocused(chat1), "first chat is focused");
                     next();
                   });
                 });
@@ -330,30 +330,30 @@ var tests = {
   testFocusedElement: function(next) {
     let chatbar = SocialChatBar.chatbar;
     startTestAndWaitForSidebar(function(port) {
       openChatViaUser();
       let chat = chatbar.firstElementChild;
       // need to wait for the content to load before we can focus it.
       chat.addEventListener("DOMContentLoaded", function DOMContentLoaded() {
         chat.removeEventListener("DOMContentLoaded", DOMContentLoaded);
-        chat.iframe.contentDocument.getElementById("input2").focus();
+        chat.contentDocument.getElementById("input2").focus();
         waitForCondition(function() isChatFocused(chat),
                          function() {
-          is(chat.iframe.contentDocument.activeElement.getAttribute("id"), "input2",
+          is(chat.contentDocument.activeElement.getAttribute("id"), "input2",
              "correct input field has focus");
           // set focus to the tab.
           let tabb = gBrowser.getBrowserForTab(gBrowser.selectedTab);
           Services.focus.moveFocus(tabb.contentWindow, null, Services.focus.MOVEFOCUS_ROOT, 0);
           waitForCondition(function() isTabFocused(),
                            function() {
             chatbar.focus();
             waitForCondition(function() isChatFocused(chat),
                              function() {
-              is(chat.iframe.contentDocument.activeElement.getAttribute("id"), "input2",
+              is(chat.contentDocument.activeElement.getAttribute("id"), "input2",
                  "correct input field still has focus");
               next();
             }, "chat took focus");
           }, "tab has focus");
         }, "chat took focus");
       });
     });
   },
--- a/browser/base/content/test/social/browser_social_errorPage.js
+++ b/browser/base/content/test/social/browser_social_errorPage.js
@@ -148,20 +148,20 @@ var tests = {
     openChat(
       "https://example.com/browser/browser/base/content/test/social/social_chat.html",
       function() { // the panel api callback
         panelCallbackCount++;
       },
       function() { // the "load" callback.
         executeSoon(function() {
           todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads.");
-          let iframe = SocialChatBar.chatbar.selectedChat.iframe;
-          waitForCondition(function() iframe.contentDocument.location.href.indexOf("about:socialerror?")==0,
+          let chat = SocialChatBar.chatbar.selectedChat;
+          waitForCondition(function() chat.contentDocument.location.href.indexOf("about:socialerror?")==0,
                            function() {
-                            SocialChatBar.chatbar.selectedChat.close();
+                            chat.close();
                             next();
                             },
                            "error page didn't appear");
         });
       }
     );
   }
 }
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -54,16 +54,17 @@ browser.jar:
 #endif
         content/browser/aboutRobots-icon.png          (content/aboutRobots-icon.png)
         content/browser/aboutRobots-widget-left.png   (content/aboutRobots-widget-left.png)
         content/browser/aboutSocialError.xhtml        (content/aboutSocialError.xhtml)
 *       content/browser/browser.css                   (content/browser.css)
 *       content/browser/browser.js                    (content/browser.js)
 *       content/browser/browser.xul                   (content/browser.xul)
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)
+*       content/browser/chatWindow.xul                (content/chatWindow.xul)
         content/browser/content.js                    (content/content.js)
         content/browser/newtab/newTab.xul             (content/newtab/newTab.xul)
 *       content/browser/newtab/newTab.js              (content/newtab/newTab.js)
         content/browser/newtab/newTab.css             (content/newtab/newTab.css)
 *       content/browser/pageinfo/pageInfo.xul         (content/pageinfo/pageInfo.xul)
         content/browser/pageinfo/pageInfo.js          (content/pageinfo/pageInfo.js)
         content/browser/pageinfo/pageInfo.css         (content/pageinfo/pageInfo.css)
         content/browser/pageinfo/pageInfo.xml         (content/pageinfo/pageInfo.xml)
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -104,17 +104,17 @@ browser.jar:
 * skin/classic/browser/preferences/preferences.css    (preferences/preferences.css)
   skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
   skin/classic/browser/preferences/applications.css   (preferences/applications.css)
   skin/classic/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css)
   skin/classic/browser/social/services-16.png         (social/services-16.png)
   skin/classic/browser/social/services-64.png         (social/services-64.png)
   skin/classic/browser/social/share-button.png        (social/share-button.png)
   skin/classic/browser/social/share-button-active.png (social/share-button-active.png)
-  skin/classic/browser/social/chat-close.png          (social/chat-close.png)
+  skin/classic/browser/social/chat-icons.png          (social/chat-icons.png)
   skin/classic/browser/tabbrowser/connecting.png      (tabbrowser/connecting.png)
   skin/classic/browser/tabbrowser/loading.png         (tabbrowser/loading.png)
   skin/classic/browser/tabbrowser/tab.png             (tabbrowser/tab.png)
   skin/classic/browser/tabbrowser/tab-overflow-border.png (tabbrowser/tab-overflow-border.png)
   skin/classic/browser/tabbrowser/tabDragIndicator.png (tabbrowser/tabDragIndicator.png)
   skin/classic/browser/tabview/edit-light.png         (tabview/edit-light.png)
   skin/classic/browser/tabview/search.png             (tabview/search.png)
   skin/classic/browser/tabview/stack-expander.png     (tabview/stack-expander.png)
deleted file mode 100644
index cc773cbc1c45756ba57d191fa2c41afedd420287..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..fd9d2d633395f1540ba6096d236e1a5fdd41cf82
GIT binary patch
literal 1394
zc%17D@N?(olHy`uVBq!ia0vp^7C<b+!3HEvcGuVeDb50q$YKTtZeb8+WSBKaf`Ngt
zI5Q-oB*NFnDmgz_FEJ%QDOIl`w}1fzY$~jP%-qzHM1_jnoV;SI3R@+x3M(KRB&@Hb
z09I0xZL1XF8=&BvUzDm~qGzIKpk&9TprBw=l#*r@<l+X^4&>P?Wt5Z@Sn2DRmzV36
z8|&p4rRy77T3YHG80i}s=>k>g7FXt#Bv$C=6)QswftllyTAW;zSx}OhpQixgCnn{W
zme?vOLDfJ2)SbBnaEtPap}qq8Pro9uK;KZ$Kp$>0$Z())E9aur#FG4?ko^1{J4cXm
zg@BCwlKkR~`~n5%U^4{`-^Aq1Jdl8<jXt^tm@D<soCyg7eP|%qaRGJ00@RMHXyr0P
z1_tJ(o-U3d5u9(s&w58Y3d}!QZD=U5+KGRudH1nkO_K{z!j7vt)~-0Y(DurrfQ>@^
zf30n_Dm1cQS#p|bIr^(jlY4Y<o|Mh%y+3u$;&0!qnVB+q$5zc56IzeoHqS5n{(bh$
zyK^0nNrV(%4l%YoxAqn1s-JRO`mOgII3Qr##k+3JnRq__dV{Q4U$U2!KZu)EJ9~Zn
zyY~wh_OmUGEL1$SGQ05IS>MmMk1hMFDcgF_u&St_OD-jVzr!|b^R7Jk**W2}N?&i8
z!P;PYcgv0a|Kw|)XM{$F@GE^YZsT9_tb4O$U;n&wP8O_9;%{m?Y<946+-<lez)*J4
zaf_sN*Ucqjf4o(1&p4c3{a)hX;VoKcF0bzI+4<MH>{C>l*7xa_{MXOFP%83Io8@rf
zc}d)nM>$4|9p@I9*c?1;8Xb1dPV0r*BIzeP<M^gIUEZ{9PmR`Qw%nBtw>UX#k5$Y7
zYWi^XMaJSq2cGw^m6ohJck;X2wlz6_rySjU^5mJbg=v$E_0N9YC?UMa$0DWV)ZWmj
z1>N&{3tD`yuH13paA<DWzDaLVHy(Za__C{h$n{4$V$$q)9&M?d#Jjs<huYzFFFKC&
z=ggU?;ZvBHCDZz*RG33)lk=|MwOgLQiR1GAbcbQ$XQ5xsuM?B{zpeD2a?0bwjBASx
znxAN{IOu=nJY!_Ww3<1sB|CVsXZjdk4(W)oNYP^1ZgNJ-F*EpX;L%y^?^h%*N#y*#
z;AGBMC4<9~F}&~Co2P%b);TtfEn@MXFA7_Pf4skX#Ch}0gp{v(UV47@Y=6zegw-}5
zJ%3Y)xm7V#xYFXNK<b)#SE{9E)cw6UIsL6J?~c9(o9O&WX18@kef!t0<z4VyU}qhh
z&$?L+KX{J2-wa4soWpc*@{5}SvHzl{NS9?_-6fzJ7~*?Ctuo}q=e(23=@IQW9~sY#
z?@?)qnxQ_Sc8Y24asNd@S6wuIoIU*Y^5meIp}S`N*t)ygac8WbsbJzqPPuh2Jx~30
z3%IjvdDtqo1=E{(S^rMkyZ3$2RTgkW$%rp4Sh~7%+qUPuQM%7Ugri*4?@K>bJNDc=
z+Gh5gGgdO`W;-wR&9~9Nw&sxH;+@Oasp&A~FdwpDjoNnL@YHW9C$knE&QX0Vo>vmF
z<TR_JV&=yu`TO+v!{y4FlIAI}r?(UYdH0@{)D~D`Ti~HS@x9FYH^1$hra#%qsQhBD
z$-WfRgU6pFPWZLtZ>^7ef<iG@liRvkCk{vEY>YAuJYzIfNqk+W7o%SH=2x8m7}+AS
UYJRwVbpe%%p00i_>zopr0R9+MwEzGB
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -3906,16 +3906,23 @@ toolbar[mode="icons"] > *|* > .toolbarbu
   background-image: radial-gradient(circle farthest-corner at center 2px, rgb(254,254,255) 3%, rgba(210,235,255,0.9) 12%, rgba(148,205,253,0.6) 30%, rgba(148,205,253,0.2) 70%);
 }
 
 chatbox {
   border-top-left-radius: @toolbarbuttonCornerRadius@;
   border-top-right-radius: @toolbarbuttonCornerRadius@;
 }
 
+window > chatbox {
+  border-top-left-radius: @toolbarbuttonCornerRadius@;
+  border-top-right-radius: @toolbarbuttonCornerRadius@;
+  border-bottom-left-radius: @toolbarbuttonCornerRadius@;
+  border-bottom-right-radius: @toolbarbuttonCornerRadius@;
+}
+
 panel[type="arrow"][popupid="click-to-play-plugins"] > .panel-arrowcontainer > .panel-arrowbox > .panel-arrow[side="top"],
 panel[type="arrow"][popupid="click-to-play-plugins"] > .panel-arrowcontainer > .panel-arrowbox > .panel-arrow[side="bottom"] {
   list-style-image: url("chrome://global/skin/arrow/panelarrow-light-vertical.png");
 }
 @media (min-resolution: 2dppx) {
   panel[type="arrow"][popupid="click-to-play-plugins"] > .panel-arrowcontainer > .panel-arrowbox > .panel-arrow[side="top"],
   panel[type="arrow"][popupid="click-to-play-plugins"] > .panel-arrowcontainer > .panel-arrowbox > .panel-arrow[side="bottom"] {
     list-style-image: url("chrome://global/skin/arrow/panelarrow-light-vertical@2x.png");
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -169,17 +169,17 @@ browser.jar:
   skin/classic/browser/preferences/applications.css         (preferences/applications.css)
   skin/classic/browser/preferences/aboutPermissions.css     (preferences/aboutPermissions.css)
   skin/classic/browser/social/services-16.png               (social/services-16.png)
   skin/classic/browser/social/services-16@2x.png            (social/services-16@2x.png)
   skin/classic/browser/social/services-64.png               (social/services-64.png)
   skin/classic/browser/social/services-64@2x.png            (social/services-64@2x.png)
   skin/classic/browser/social/share-button.png              (social/share-button.png)
   skin/classic/browser/social/share-button-active.png       (social/share-button-active.png)
-  skin/classic/browser/social/chat-close.png                             (social/chat-close.png)
+  skin/classic/browser/social/chat-icons.png                             (social/chat-icons.png)
   skin/classic/browser/tabbrowser/alltabs-box-bkgnd-icon.png             (tabbrowser/alltabs-box-bkgnd-icon.png)
   skin/classic/browser/tabbrowser/newtab.png                             (tabbrowser/newtab.png)
   skin/classic/browser/tabbrowser/newtab@2x.png                          (tabbrowser/newtab@2x.png)
   skin/classic/browser/tabbrowser/connecting.png                         (tabbrowser/connecting.png)
   skin/classic/browser/tabbrowser/connecting@2x.png                      (tabbrowser/connecting@2x.png)
   skin/classic/browser/tabbrowser/loading.png                            (tabbrowser/loading.png)
   skin/classic/browser/tabbrowser/loading@2x.png                         (tabbrowser/loading@2x.png)
   skin/classic/browser/tabbrowser/tab-arrow-left.png                     (tabbrowser/tab-arrow-left.png)
deleted file mode 100644
index cc773cbc1c45756ba57d191fa2c41afedd420287..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..fd9d2d633395f1540ba6096d236e1a5fdd41cf82
GIT binary patch
literal 1394
zc%17D@N?(olHy`uVBq!ia0vp^7C<b+!3HEvcGuVeDb50q$YKTtZeb8+WSBKaf`Ngt
zI5Q-oB*NFnDmgz_FEJ%QDOIl`w}1fzY$~jP%-qzHM1_jnoV;SI3R@+x3M(KRB&@Hb
z09I0xZL1XF8=&BvUzDm~qGzIKpk&9TprBw=l#*r@<l+X^4&>P?Wt5Z@Sn2DRmzV36
z8|&p4rRy77T3YHG80i}s=>k>g7FXt#Bv$C=6)QswftllyTAW;zSx}OhpQixgCnn{W
zme?vOLDfJ2)SbBnaEtPap}qq8Pro9uK;KZ$Kp$>0$Z())E9aur#FG4?ko^1{J4cXm
zg@BCwlKkR~`~n5%U^4{`-^Aq1Jdl8<jXt^tm@D<soCyg7eP|%qaRGJ00@RMHXyr0P
z1_tJ(o-U3d5u9(s&w58Y3d}!QZD=U5+KGRudH1nkO_K{z!j7vt)~-0Y(DurrfQ>@^
zf30n_Dm1cQS#p|bIr^(jlY4Y<o|Mh%y+3u$;&0!qnVB+q$5zc56IzeoHqS5n{(bh$
zyK^0nNrV(%4l%YoxAqn1s-JRO`mOgII3Qr##k+3JnRq__dV{Q4U$U2!KZu)EJ9~Zn
zyY~wh_OmUGEL1$SGQ05IS>MmMk1hMFDcgF_u&St_OD-jVzr!|b^R7Jk**W2}N?&i8
z!P;PYcgv0a|Kw|)XM{$F@GE^YZsT9_tb4O$U;n&wP8O_9;%{m?Y<946+-<lez)*J4
zaf_sN*Ucqjf4o(1&p4c3{a)hX;VoKcF0bzI+4<MH>{C>l*7xa_{MXOFP%83Io8@rf
zc}d)nM>$4|9p@I9*c?1;8Xb1dPV0r*BIzeP<M^gIUEZ{9PmR`Qw%nBtw>UX#k5$Y7
zYWi^XMaJSq2cGw^m6ohJck;X2wlz6_rySjU^5mJbg=v$E_0N9YC?UMa$0DWV)ZWmj
z1>N&{3tD`yuH13paA<DWzDaLVHy(Za__C{h$n{4$V$$q)9&M?d#Jjs<huYzFFFKC&
z=ggU?;ZvBHCDZz*RG33)lk=|MwOgLQiR1GAbcbQ$XQ5xsuM?B{zpeD2a?0bwjBASx
znxAN{IOu=nJY!_Ww3<1sB|CVsXZjdk4(W)oNYP^1ZgNJ-F*EpX;L%y^?^h%*N#y*#
z;AGBMC4<9~F}&~Co2P%b);TtfEn@MXFA7_Pf4skX#Ch}0gp{v(UV47@Y=6zegw-}5
zJ%3Y)xm7V#xYFXNK<b)#SE{9E)cw6UIsL6J?~c9(o9O&WX18@kef!t0<z4VyU}qhh
z&$?L+KX{J2-wa4soWpc*@{5}SvHzl{NS9?_-6fzJ7~*?Ctuo}q=e(23=@IQW9~sY#
z?@?)qnxQ_Sc8Y24asNd@S6wuIoIU*Y^5meIp}S`N*t)ygac8WbsbJzqPPuh2Jx~30
z3%IjvdDtqo1=E{(S^rMkyZ3$2RTgkW$%rp4Sh~7%+qUPuQM%7Ugri*4?@K>bJNDc=
z+Gh5gGgdO`W;-wR&9~9Nw&sxH;+@Oasp&A~FdwpDjoNnL@YHW9C$knE&QX0Vo>vmF
z<TR_JV&=yu`TO+v!{y4FlIAI}r?(UYdH0@{)D~D`Ti~HS@x9FYH^1$hra#%qsQhBD
z$-WfRgU6pFPWZLtZ>^7ef<iG@liRvkCk{vEY>YAuJYzIfNqk+W7o%SH=2x8m7}+AS
UYJRwVbpe%%p00i_>zopr0R9+MwEzGB
--- a/browser/themes/shared/social/chat.inc.css
+++ b/browser/themes/shared/social/chat.inc.css
@@ -18,26 +18,53 @@
   background: none;
 }
 
 .chat-toolbarbutton > .toolbarbutton-text {
   display: none;
 }
 
 .chat-close-button {
-  list-style-image: url('chrome://browser/skin/social/chat-close.png');
+  list-style-image: url('chrome://browser/skin/social/chat-icons.png');
   -moz-image-region: rect(0, 14px, 14px, 0);
 }
 
+.chat-close-button:hover,
 .chat-close-button:hover:active {
   -moz-image-region: rect(14px, 14px, 28px, 0);
 }
 
-.chat-close-button:hover {
-  -moz-image-region: rect(28px, 14px, 42px, 0);
+.chat-minimize-button {
+  list-style-image: url('chrome://browser/skin/social/chat-icons.png');
+  -moz-image-region: rect(0, 28px, 14px, 14px);
+}
+
+.chat-minimize-button:hover:active,
+.chat-minimize-button:hover {
+  -moz-image-region: rect(14px, 28px, 28px, 14px);
+}
+
+.chat-swap-button {
+  list-style-image: url('chrome://browser/skin/social/chat-icons.png');
+  -moz-image-region: rect(0, 42px, 14px, 28px);
+}
+
+.chat-swap-button:hover:active,
+.chat-swap-button:hover {
+  -moz-image-region: rect(14px, 42px, 28px, 28px);
+}
+
+chatbar > chatbox > .chat-titlebar > .chat-swap-button {
+  list-style-image: url('chrome://browser/skin/social/chat-icons.png');
+  -moz-image-region: rect(0, 56px, 14px, 42px);
+}
+
+chatbar > chatbox > .chat-titlebar > .chat-swap-button:hover:active,
+chatbar > chatbox > .chat-titlebar > .chat-swap-button:hover {
+  -moz-image-region: rect(14px, 56px, 28px, 42px);
 }
 
 .chat-title {
   font-weight: bold;
   color: black;
   text-shadow: none;
   cursor: inherit;
 }
@@ -118,8 +145,15 @@ chatbar {
 }
 
 chatbox {
   -moz-margin-start: 4px;
   background-color: white;
   border: 1px solid #ccc;
   border-bottom: none;
 }
+
+window > chatbox {
+  -moz-margin-start: 0px;
+  margin: 0px;
+  border: none;
+  padding: 0px;
+}
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -123,17 +123,17 @@ browser.jar:
 *       skin/classic/browser/preferences/preferences.css             (preferences/preferences.css)
         skin/classic/browser/preferences/in-content/preferences.css  (preferences/in-content/preferences.css)
         skin/classic/browser/preferences/applications.css            (preferences/applications.css)
         skin/classic/browser/preferences/aboutPermissions.css        (preferences/aboutPermissions.css)
         skin/classic/browser/social/services-16.png                  (social/services-16.png)
         skin/classic/browser/social/services-64.png                  (social/services-64.png)
         skin/classic/browser/social/share-button.png                 (social/share-button.png)
         skin/classic/browser/social/share-button-active.png          (social/share-button-active.png)
-        skin/classic/browser/social/chat-close.png                   (social/chat-close.png)
+        skin/classic/browser/social/chat-icons.png                   (social/chat-icons.png)
         skin/classic/browser/tabbrowser/newtab.png                   (tabbrowser/newtab.png)
         skin/classic/browser/tabbrowser/newtab-inverted.png          (tabbrowser/newtab-inverted.png)
         skin/classic/browser/tabbrowser/connecting.png               (tabbrowser/connecting.png)
         skin/classic/browser/tabbrowser/loading.png                  (tabbrowser/loading.png)
         skin/classic/browser/tabbrowser/tab.png                      (tabbrowser/tab.png)
         skin/classic/browser/tabbrowser/tab-arrow-left.png           (tabbrowser/tab-arrow-left.png)
         skin/classic/browser/tabbrowser/tab-arrow-left-inverted.png  (tabbrowser/tab-arrow-left-inverted.png)
         skin/classic/browser/tabbrowser/tab-overflow-border.png      (tabbrowser/tab-overflow-border.png)
@@ -375,17 +375,17 @@ browser.jar:
 *       skin/classic/aero/browser/preferences/preferences.css        (preferences/preferences.css)
         skin/classic/aero/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
         skin/classic/aero/browser/preferences/applications.css       (preferences/applications.css)
         skin/classic/aero/browser/preferences/aboutPermissions.css   (preferences/aboutPermissions.css)
         skin/classic/aero/browser/social/services-16.png             (social/services-16.png)
         skin/classic/aero/browser/social/services-64.png             (social/services-64.png)
         skin/classic/aero/browser/social/share-button.png            (social/share-button.png)
         skin/classic/aero/browser/social/share-button-active.png     (social/share-button-active.png)
-        skin/classic/aero/browser/social/chat-close.png              (social/chat-close.png)
+        skin/classic/aero/browser/social/chat-icons.png              (social/chat-icons.png)
         skin/classic/aero/browser/tabbrowser/newtab.png              (tabbrowser/newtab.png)
         skin/classic/aero/browser/tabbrowser/newtab-inverted.png     (tabbrowser/newtab-inverted.png)
         skin/classic/aero/browser/tabbrowser/connecting.png          (tabbrowser/connecting.png)
         skin/classic/aero/browser/tabbrowser/loading.png             (tabbrowser/loading.png)
         skin/classic/aero/browser/tabbrowser/tab.png                 (tabbrowser/tab.png)
         skin/classic/aero/browser/tabbrowser/tab-arrow-left.png      (tabbrowser/tab-arrow-left.png)
         skin/classic/aero/browser/tabbrowser/tab-arrow-left-inverted.png (tabbrowser/tab-arrow-left-inverted.png)
         skin/classic/aero/browser/tabbrowser/tab-overflow-border.png (tabbrowser/tab-overflow-border.png)
deleted file mode 100644
index 5260f1019668f8c9c6c64f1ec61f5346609cc5c3..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..fd9d2d633395f1540ba6096d236e1a5fdd41cf82
GIT binary patch
literal 1394
zc%17D@N?(olHy`uVBq!ia0vp^7C<b+!3HEvcGuVeDb50q$YKTtZeb8+WSBKaf`Ngt
zI5Q-oB*NFnDmgz_FEJ%QDOIl`w}1fzY$~jP%-qzHM1_jnoV;SI3R@+x3M(KRB&@Hb
z09I0xZL1XF8=&BvUzDm~qGzIKpk&9TprBw=l#*r@<l+X^4&>P?Wt5Z@Sn2DRmzV36
z8|&p4rRy77T3YHG80i}s=>k>g7FXt#Bv$C=6)QswftllyTAW;zSx}OhpQixgCnn{W
zme?vOLDfJ2)SbBnaEtPap}qq8Pro9uK;KZ$Kp$>0$Z())E9aur#FG4?ko^1{J4cXm
zg@BCwlKkR~`~n5%U^4{`-^Aq1Jdl8<jXt^tm@D<soCyg7eP|%qaRGJ00@RMHXyr0P
z1_tJ(o-U3d5u9(s&w58Y3d}!QZD=U5+KGRudH1nkO_K{z!j7vt)~-0Y(DurrfQ>@^
zf30n_Dm1cQS#p|bIr^(jlY4Y<o|Mh%y+3u$;&0!qnVB+q$5zc56IzeoHqS5n{(bh$
zyK^0nNrV(%4l%YoxAqn1s-JRO`mOgII3Qr##k+3JnRq__dV{Q4U$U2!KZu)EJ9~Zn
zyY~wh_OmUGEL1$SGQ05IS>MmMk1hMFDcgF_u&St_OD-jVzr!|b^R7Jk**W2}N?&i8
z!P;PYcgv0a|Kw|)XM{$F@GE^YZsT9_tb4O$U;n&wP8O_9;%{m?Y<946+-<lez)*J4
zaf_sN*Ucqjf4o(1&p4c3{a)hX;VoKcF0bzI+4<MH>{C>l*7xa_{MXOFP%83Io8@rf
zc}d)nM>$4|9p@I9*c?1;8Xb1dPV0r*BIzeP<M^gIUEZ{9PmR`Qw%nBtw>UX#k5$Y7
zYWi^XMaJSq2cGw^m6ohJck;X2wlz6_rySjU^5mJbg=v$E_0N9YC?UMa$0DWV)ZWmj
z1>N&{3tD`yuH13paA<DWzDaLVHy(Za__C{h$n{4$V$$q)9&M?d#Jjs<huYzFFFKC&
z=ggU?;ZvBHCDZz*RG33)lk=|MwOgLQiR1GAbcbQ$XQ5xsuM?B{zpeD2a?0bwjBASx
znxAN{IOu=nJY!_Ww3<1sB|CVsXZjdk4(W)oNYP^1ZgNJ-F*EpX;L%y^?^h%*N#y*#
z;AGBMC4<9~F}&~Co2P%b);TtfEn@MXFA7_Pf4skX#Ch}0gp{v(UV47@Y=6zegw-}5
zJ%3Y)xm7V#xYFXNK<b)#SE{9E)cw6UIsL6J?~c9(o9O&WX18@kef!t0<z4VyU}qhh
z&$?L+KX{J2-wa4soWpc*@{5}SvHzl{NS9?_-6fzJ7~*?Ctuo}q=e(23=@IQW9~sY#
z?@?)qnxQ_Sc8Y24asNd@S6wuIoIU*Y^5meIp}S`N*t)ygac8WbsbJzqPPuh2Jx~30
z3%IjvdDtqo1=E{(S^rMkyZ3$2RTgkW$%rp4Sh~7%+qUPuQM%7Ugri*4?@K>bJNDc=
z+Gh5gGgdO`W;-wR&9~9Nw&sxH;+@Oasp&A~FdwpDjoNnL@YHW9C$knE&QX0Vo>vmF
z<TR_JV&=yu`TO+v!{y4FlIAI}r?(UYdH0@{)D~D`Ti~HS@x9FYH^1$hra#%qsQhBD
z$-WfRgU6pFPWZLtZ>^7ef<iG@liRvkCk{vEY>YAuJYzIfNqk+W7o%SH=2x8m7}+AS
UYJRwVbpe%%p00i_>zopr0R9+MwEzGB
--- a/toolkit/components/social/MozSocialAPI.jsm
+++ b/toolkit/components/social/MozSocialAPI.jsm
@@ -5,17 +5,17 @@
 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, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
-this.EXPORTED_SYMBOLS = ["MozSocialAPI", "openChatWindow"];
+this.EXPORTED_SYMBOLS = ["MozSocialAPI", "openChatWindow", "findChromeWindowForChats"];
 
 this.MozSocialAPI = {
   _enabled: false,
   _everEnabled: false,
   set enabled(val) {
     let enable = !!val;
     if (enable == this._enabled) {
       return;