Backed out 6 changesets (bug 1289549) for mass failures in various tests CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Thu, 04 Aug 2016 13:04:57 -0700
changeset 308232 c60886d5c003908affbe74e56a7c5b88021b4c6e
parent 308231 d912055f8e8ad75d189bd480017fdb5a33ee4d7e
child 308233 975ba208687a97ecb9fd439c1ee52bfa3350e25b
child 308370 fff9d624dca283235c916d8769786e04f5622282
push id31092
push usercbook@mozilla.com
push dateFri, 05 Aug 2016 10:16:59 +0000
treeherderautoland@b97dd7dd3cb9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1289549
milestone51.0a1
backs out5ad07719e3bdf424673e7f1c7d0e0dc9b6880cae
27e2621947f26a189743243acfa75ed0d7753409
40d5477b89606ca1c56e99b668a7fe669f5cce89
af2c234795a96b578ba94f3d372d676b2d70e95d
6847acfd9362d6f97f21e2d9235c0db682589f30
fc771254be8ff7a9fb11365787e0e136cd4bf225
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
Backed out 6 changesets (bug 1289549) for mass failures in various tests CLOSED TREE Backed out changeset 5ad07719e3bd (bug 1289549) Backed out changeset 27e2621947f2 (bug 1289549) Backed out changeset 40d5477b8960 (bug 1289549) Backed out changeset af2c234795a9 (bug 1289549) Backed out changeset 6847acfd9362 (bug 1289549) Backed out changeset fc771254be8f (bug 1289549)
browser/base/content/browser-context.inc
browser/base/content/browser-menubar.inc
browser/base/content/browser-sets.inc
browser/base/content/browser-social.js
browser/base/content/browser.css
browser/base/content/browser.js
browser/base/content/browser.xul
browser/base/content/chatWindow.xul
browser/base/content/content.js
browser/base/content/nsContextMenu.js
browser/base/content/social-content.js
browser/base/content/socialchat.xml
browser/base/content/socialmarks.xml
browser/base/content/test/chat/.eslintrc
browser/base/content/test/chat/browser.ini
browser/base/content/test/chat/browser_chatwindow.js
browser/base/content/test/chat/browser_focus.js
browser/base/content/test/chat/browser_tearoff.js
browser/base/content/test/chat/chat.html
browser/base/content/test/chat/head.js
browser/base/content/test/social/browser.ini
browser/base/content/test/social/browser_aboutHome_activation.js
browser/base/content/test/social/browser_addons.js
browser/base/content/test/social/browser_blocklist.js
browser/base/content/test/social/browser_share.js
browser/base/content/test/social/browser_social_activation.js
browser/base/content/test/social/browser_social_chatwindow.js
browser/base/content/test/social/browser_social_chatwindow_resize.js
browser/base/content/test/social/browser_social_chatwindowfocus.js
browser/base/content/test/social/browser_social_contextmenu.js
browser/base/content/test/social/browser_social_errorPage.js
browser/base/content/test/social/browser_social_flyout.js
browser/base/content/test/social/browser_social_isVisible.js
browser/base/content/test/social/browser_social_marks.js
browser/base/content/test/social/browser_social_marks_context.js
browser/base/content/test/social/browser_social_multiprovider.js
browser/base/content/test/social/browser_social_sidebar.js
browser/base/content/test/social/browser_social_status.js
browser/base/content/test/social/browser_social_window.js
browser/base/content/test/social/checked.jpg
browser/base/content/test/social/head.js
browser/base/content/test/social/social_activate.html
browser/base/content/test/social/social_activate_basic.html
browser/base/content/test/social/social_chat.html
browser/base/content/test/social/social_flyout.html
browser/base/content/test/social/social_mark.html
browser/base/content/test/social/social_panel.html
browser/base/content/test/social/social_sidebar.html
browser/base/content/test/social/social_sidebar_empty.html
browser/base/content/test/social/unchecked.jpg
browser/base/jar.mn
browser/base/moz.build
browser/components/customizableui/CustomizableWidgets.jsm
browser/extensions/pocket/bootstrap.js
browser/locales/en-US/chrome/browser/browser.dtd
browser/modules/Chat.jsm
browser/modules/Social.jsm
browser/modules/SocialService.jsm
browser/modules/moz.build
browser/modules/test/unit/social/head.js
browser/modules/test/unit/social/test_SocialService.js
browser/modules/test/unit/social/test_SocialServiceMigration21.js
browser/modules/test/unit/social/test_SocialServiceMigration22.js
browser/modules/test/unit/social/test_SocialServiceMigration29.js
browser/modules/test/unit/social/test_social.js
browser/modules/test/unit/social/xpcshell.ini
browser/themes/linux/browser.css
browser/themes/osx/browser.css
browser/themes/shared/jar.inc.mn
browser/themes/shared/social/chat-icons.svg
browser/themes/shared/social/chat.inc.css
browser/themes/shared/social/social.inc.css
browser/themes/windows/browser-aero.css
browser/themes/windows/browser.css
testing/mochitest/browser-test.js
toolkit/components/moz.build
toolkit/components/social/MozSocialAPI.jsm
toolkit/components/social/SocialService.jsm
toolkit/components/social/moz.build
toolkit/components/social/test/xpcshell/blocklist.xml
toolkit/components/social/test/xpcshell/head.js
toolkit/components/social/test/xpcshell/test_SocialService.js
toolkit/components/social/test/xpcshell/test_SocialServiceMigration21.js
toolkit/components/social/test/xpcshell/test_SocialServiceMigration22.js
toolkit/components/social/test/xpcshell/test_SocialServiceMigration29.js
toolkit/components/social/test/xpcshell/xpcshell.ini
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -88,16 +88,20 @@
       <menuitem id="context-sharelink"
                 label="&shareLink.label;"
                 accesskey="&shareLink.accesskey;"
                 oncommand="gContextMenu.shareLink();"/>
       <menuitem id="context-savelink"
                 label="&saveLinkCmd.label;"
                 accesskey="&saveLinkCmd.accesskey;"
                 oncommand="gContextMenu.saveLink();"/>
+      <menu id="context-marklinkMenu" label="&social.marklinkMenu.label;"
+            accesskey="&social.marklinkMenu.accesskey;">
+        <menupopup/>
+      </menu>
       <menuitem id="context-copyemail"
                 label="&copyEmailCmd.label;"
                 accesskey="&copyEmailCmd.accesskey;"
                 oncommand="gContextMenu.copyEmail();"/>
       <menuitem id="context-copylink"
                 label="&copyLinkCmd.label;"
                 accesskey="&copyLinkCmd.accesskey;"
                 oncommand="gContextMenu.copyLink();"/>
@@ -282,16 +286,20 @@
       <menuseparator id="context-sep-sendpagetodevice" hidden="true"/>
       <menu id="context-sendpagetodevice"
                 label="&sendPageToDevice.label;"
                 accesskey="&sendPageToDevice.accesskey;"
                 hidden="true">
         <menupopup id="context-sendpagetodevice-popup"
                    onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gFxAccounts.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/>
       </menu>
+      <menu id="context-markpageMenu" label="&social.markpageMenu.label;"
+            accesskey="&social.markpageMenu.accesskey;">
+        <menupopup/>
+      </menu>
       <menuseparator id="context-sep-viewbgimage"/>
       <menuitem id="context-viewbgimage"
                 label="&viewBGImageCmd.label;"
                 accesskey="&viewBGImageCmd.accesskey;"
                 oncommand="gContextMenu.viewBGImage(event);"
                 onclick="checkForMiddleClick(this, event);"/>
       <menuitem id="context-undo"
                 label="&undoCmd.label;"
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -215,16 +215,19 @@
                               observes="viewBookmarksSidebar"/>
                     <menuitem id="menu_historySidebar"
                               key="key_gotoHistory"
                               observes="viewHistorySidebar"
                               label="&historyButton.label;"/>
                     <menuitem id="menu_tabsSidebar"
                               observes="viewTabsSidebar"
                               label="&syncedTabs.sidebar.label;"/>
+                    <!-- Service providers with sidebars are inserted between these two menuseperators -->
+                    <menuseparator hidden="true"/>
+                    <menuseparator class="social-provider-menu" hidden="true"/>
                   </menupopup>
                 </menu>
                 <menuseparator/>
                 <menu id="viewFullZoomMenu" label="&fullZoom.label;"
                       accesskey="&fullZoom.accesskey;"
                       onpopupshowing="FullZoom.updateMenu();">
                   <menupopup>
                     <menuitem id="menu_zoomEnlarge"
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -102,28 +102,31 @@
       oncommand="OpenBrowserWindow({private: true});" reserved="true"/>
 #ifdef E10S_TESTING_ONLY
     <command id="Tools:NonRemoteWindow"
       oncommand="OpenBrowserWindow({remote: false});"/>
 #endif
     <command id="History:UndoCloseTab" oncommand="undoCloseTab();"/>
     <command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/>
     <command id="Social:SharePage" oncommand="SocialShare.sharePage();"/>
+    <command id="Social:ToggleSidebar" oncommand="SocialSidebar.toggleSidebar();" hidden="true"/>
+    <command id="Social:ToggleNotifications" oncommand="Social.toggleNotifications();" hidden="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('UnfiledBookmarks');"/>
     <command id="Browser:ShowAllHistory"
              oncommand="PlacesCommandHook.showPlacesOrganizer('History');"/>
   </commandset>
 
   <broadcasterset id="mainBroadcasterSet">
-    <broadcaster id="Social:PageShareable" disabled="true"/>
+    <broadcaster id="Social:PageShareOrMark" disabled="true"/>
     <broadcaster id="viewBookmarksSidebar" autoCheck="false" label="&bookmarksButton.label;"
                  type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul"
                  oncommand="SidebarUI.toggle('viewBookmarksSidebar');"/>
 
     <!-- for both places and non-places, the sidebar lives at
          chrome://browser/content/history/history-panel.xul so there are no
          problems when switching between versions -->
     <broadcaster id="viewHistorySidebar" autoCheck="false" sidebartitle="&historyButton.label;"
@@ -173,16 +176,17 @@
     <broadcaster id="sync-setup-state"/>
     <broadcaster id="sync-syncnow-state" hidden="true"/>
     <broadcaster id="sync-reauth-state" hidden="true"/>
     <broadcaster id="viewTabsSidebar" autoCheck="false" sidebartitle="&syncedTabs.sidebar.label;"
                  type="checkbox" group="sidebar"
                  sidebarurl="chrome://browser/content/syncedtabs/sidebar.xhtml"
                  oncommand="SidebarUI.toggle('viewTabsSidebar');"/>
     <broadcaster id="workOfflineMenuitemState"/>
+    <broadcaster id="socialSidebarBroadcaster" hidden="true"/>
 
     <broadcaster id="devtoolsMenuBroadcaster_PageSource"
                  label="&pageSourceCmd.label;"
                  key="key_viewSource"
                  command="View:PageSource">
       <observes element="canViewSource" attribute="disabled"/>
     </broadcaster>
   </broadcasterset>
@@ -307,16 +311,27 @@
 #endif
     <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="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"
          key="&historySidebarCmd.commandKey;"
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -1,79 +1,180 @@
 /* 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/. */
 
 // the "exported" symbols
 var SocialUI,
+    SocialFlyout,
+    SocialMarks,
     SocialShare,
+    SocialSidebar,
+    SocialStatus,
     SocialActivationListener;
 
 (function() {
 
+XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame",
+  "resource:///modules/PanelFrame.jsm");
+
 XPCOMUtils.defineLazyGetter(this, "OpenGraphBuilder", function() {
   let tmp = {};
   Cu.import("resource:///modules/Social.jsm", tmp);
   return tmp.OpenGraphBuilder;
 });
 
 XPCOMUtils.defineLazyGetter(this, "DynamicResizeWatcher", function() {
   let tmp = {};
   Cu.import("resource:///modules/Social.jsm", tmp);
   return tmp.DynamicResizeWatcher;
 });
 
+XPCOMUtils.defineLazyGetter(this, "sizeSocialPanelToContent", function() {
+  let tmp = {};
+  Cu.import("resource:///modules/Social.jsm", tmp);
+  return tmp.sizeSocialPanelToContent;
+});
+
+XPCOMUtils.defineLazyGetter(this, "CreateSocialStatusWidget", function() {
+  let tmp = {};
+  Cu.import("resource:///modules/Social.jsm", tmp);
+  return tmp.CreateSocialStatusWidget;
+});
+
+XPCOMUtils.defineLazyGetter(this, "CreateSocialMarkWidget", function() {
+  let tmp = {};
+  Cu.import("resource:///modules/Social.jsm", tmp);
+  return tmp.CreateSocialMarkWidget;
+});
+
+XPCOMUtils.defineLazyGetter(this, "hookWindowCloseForPanelClose", function() {
+  let tmp = {};
+  Cu.import("resource://gre/modules/MozSocialAPI.jsm", tmp);
+  return tmp.hookWindowCloseForPanelClose;
+});
+
 SocialUI = {
   _initialized: false,
 
   // Called on delayed startup to initialize the UI
   init: function SocialUI_init() {
     if (this._initialized) {
       return;
     }
     let mm = window.getGroupMessageManager("social");
     mm.loadFrameScript("chrome://browser/content/content.js", true);
     mm.loadFrameScript("chrome://browser/content/social-content.js", true);
 
+    Services.obs.addObserver(this, "social:ambient-notification-changed", false);
     Services.obs.addObserver(this, "social:providers-changed", false);
+    Services.obs.addObserver(this, "social:provider-reload", false);
+    Services.obs.addObserver(this, "social:provider-enabled", false);
+    Services.obs.addObserver(this, "social:provider-disabled", false);
+
+    Services.prefs.addObserver("social.toast-notifications.enabled", this, false);
 
     CustomizableUI.addListener(this);
     SocialActivationListener.init();
+    messageManager.addMessageListener("Social:Notification", this);
+
+    // menupopups that list social providers. we only populate them when shown,
+    // and if it has not been done already.
+    document.getElementById("viewSidebarMenu").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
+    document.getElementById("social-statusarea-popup").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
 
     Social.init().then((update) => {
       if (update)
         this._providersChanged();
+      // handle SessionStore for the sidebar state
+      SocialSidebar.restoreWindowState();
     });
 
     this._initialized = true;
   },
 
   // Called on window unload
   uninit: function SocialUI_uninit() {
     if (!this._initialized) {
       return;
     }
+    SocialSidebar.saveWindowState();
+
+    Services.obs.removeObserver(this, "social:ambient-notification-changed");
     Services.obs.removeObserver(this, "social:providers-changed");
+    Services.obs.removeObserver(this, "social:provider-reload");
+    Services.obs.removeObserver(this, "social:provider-enabled");
+    Services.obs.removeObserver(this, "social:provider-disabled");
 
+    Services.prefs.removeObserver("social.toast-notifications.enabled", this);
     CustomizableUI.removeListener(this);
     SocialActivationListener.uninit();
+    messageManager.removeMessageListener("Social:Notification", this);
+
+    document.getElementById("viewSidebarMenu").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
+    document.getElementById("social-statusarea-popup").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
 
     this._initialized = false;
   },
 
+  receiveMessage: function(aMessage) {
+    if (aMessage.name == "Social:Notification") {
+      let provider = Social._getProviderFromOrigin(aMessage.data.origin);
+      if (provider) {
+        provider.setAmbientNotification(aMessage.data.detail);
+      }
+    }
+  },
+
   observe: function SocialUI_observe(subject, topic, data) {
     switch (topic) {
+      case "social:provider-enabled":
+        SocialMarks.populateToolbarPalette();
+        SocialStatus.populateToolbarPalette();
+        break;
+      case "social:provider-disabled":
+        SocialMarks.removeProvider(data);
+        SocialStatus.removeProvider(data);
+        SocialSidebar.disableProvider(data);
+        break;
+      case "social:provider-reload":
+        SocialStatus.reloadProvider(data);
+        // if the reloaded provider is our current provider, fall through
+        // to social:providers-changed so the ui will be reset
+        if (!SocialSidebar.provider || SocialSidebar.provider.origin != data)
+          return;
+        // currently only the sidebar and flyout have a selected provider.
+        // sidebar provider has changed (possibly to null), ensure the content
+        // is unloaded and the frames are reset, they will be loaded in
+        // providers-changed below if necessary.
+        SocialSidebar.unloadSidebar();
+        SocialFlyout.unload();
+        // fall through to providers-changed to ensure the reloaded provider
+        // is correctly reflected in any UI and the multi-provider menu
       case "social:providers-changed":
         this._providersChanged();
         break;
+      // Provider-specific notifications
+      case "social:ambient-notification-changed":
+        SocialStatus.updateButton(data);
+        break;
+      case "nsPref:changed":
+        if (data == "social.toast-notifications.enabled") {
+          SocialSidebar.updateToggleNotifications();
+        }
+        break;
     }
   },
 
   _providersChanged: function() {
+    SocialSidebar.clearProviderMenus();
+    SocialSidebar.update();
     SocialShare.populateProviderMenu();
+    SocialStatus.populateToolbarPalette();
+    SocialMarks.populateToolbarPalette();
   },
 
   showLearnMore: function() {
     let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api";
     openUILinkIn(url, "tab");
   },
 
   closeSocialPanelForLinkTraversal: function (target, linkNode) {
@@ -113,40 +214,52 @@ SocialUI = {
 
   get enabled() {
     // Returns whether social is enabled *for this window*.
     if (this._chromeless)
       return false;
     return Social.providers.length > 0;
   },
 
-  canSharePage: function(aURI) {
+  canShareOrMarkPage: function(aURI) {
     return (aURI && (aURI.schemeIs('http') || aURI.schemeIs('https')));
   },
 
   onCustomizeEnd: function(aWindow) {
     if (aWindow != window)
       return;
     // customization mode gets buttons out of sync with command updating, fix
     // the disabled state
-    let canShare = this.canSharePage(gBrowser.currentURI);
+    let canShare = this.canShareOrMarkPage(gBrowser.currentURI);
     let shareButton = SocialShare.shareButton;
     if (shareButton) {
       if (canShare) {
         shareButton.removeAttribute("disabled")
       } else {
         shareButton.setAttribute("disabled", "true")
       }
     }
+    // update the disabled state of the button based on the command
+    for (let node of SocialMarks.nodes) {
+      if (canShare) {
+        node.removeAttribute("disabled")
+      } else {
+        node.setAttribute("disabled", "true")
+      }
+    }
   },
 
   // called on tab/urlbar/location changes and after customization. Update
   // anything that is tab specific.
   updateState: function() {
-    goSetCommandEnabled("Social:PageShareable", this.canSharePage(gBrowser.currentURI));
+    goSetCommandEnabled("Social:PageShareOrMark", this.canShareOrMarkPage(gBrowser.currentURI));
+    if (!SocialUI.enabled)
+      return;
+    // larger update that may change button icons
+    SocialMarks.update();
   }
 }
 
 // message manager handlers
 SocialActivationListener = {
   init: function() {
     messageManager.addMessageListener("Social:Activation", this);
   },
@@ -162,16 +275,19 @@ SocialActivationListener = {
     // social.directories preference
     let options;
     if (browser == SocialShare.iframe && Services.prefs.getBoolPref("social.share.activationPanelEnabled")) {
       options = { bypassContentCheck: true, bypassInstallPanel: true };
     }
 
     Social.installProvider(data, function(manifest) {
       Social.activateFromOrigin(manifest.origin, function(provider) {
+        if (provider.sidebarURL) {
+          SocialSidebar.show(provider.origin);
+        }
         if (provider.shareURL) {
           // Ensure that the share button is somewhere usable.
           // SocialShare.shareButton may return null if it is in the menu-panel
           // and has never been visible, so we check the widget directly. If
           // there is no area for the widget we move it into the toolbar.
           let widget = CustomizableUI.getWidget("social-share-button");
           // If the panel is already open, we can be sure that the provider can
           // already be accessed, possibly anchored to another toolbar button.
@@ -198,16 +314,148 @@ SocialActivationListener = {
           // a background tab
           gBrowser.loadOneTab(provider.postActivationURL, {inBackground: SocialShare.panel.state == "open"});
         }
       });
     }, options);
   }
 }
 
+SocialFlyout = {
+  get panel() {
+    return document.getElementById("social-flyout-panel");
+  },
+
+  get iframe() {
+    if (!this.panel.firstChild)
+      this._createFrame();
+    return this.panel.firstChild;
+  },
+
+  dispatchPanelEvent: function(name) {
+    let doc = this.iframe.contentDocument;
+    let evt = doc.createEvent("CustomEvent");
+    evt.initCustomEvent(name, true, true, {});
+    doc.documentElement.dispatchEvent(evt);
+  },
+
+  _createFrame: function() {
+    let panel = this.panel;
+    if (!SocialUI.enabled || panel.firstChild)
+      return;
+    // create and initialize the panel for this window
+    let iframe = document.createElement("browser");
+    iframe.setAttribute("type", "content");
+    iframe.setAttribute("class", "social-panel-frame");
+    iframe.setAttribute("flex", "1");
+    iframe.setAttribute("message", "true");
+    iframe.setAttribute("messagemanagergroup", "social");
+    iframe.setAttribute("disablehistory", "true");
+    iframe.setAttribute("tooltip", "aHTMLTooltip");
+    iframe.setAttribute("context", "contentAreaContextMenu");
+    iframe.setAttribute("origin", SocialSidebar.provider.origin);
+    panel.appendChild(iframe);
+    this.messageManager.sendAsyncMessage("Social:SetErrorURL",
+                        { template: "about:socialerror?mode=compactInfo&origin=%{origin}" });
+  },
+
+  get messageManager() {
+    // The xbl bindings for the iframe may not exist yet, so we can't
+    // access iframe.messageManager directly - but can get at it with this dance.
+    return this.iframe.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader.messageManager;
+  },
+
+  unload: function() {
+    let panel = this.panel;
+    panel.hidePopup();
+    if (!panel.firstChild)
+      return
+    let iframe = panel.firstChild;
+    panel.removeChild(iframe);
+  },
+
+  onShown: function(aEvent) {
+    let panel = this.panel;
+    let iframe = this.iframe;
+    this._dynamicResizer = new DynamicResizeWatcher();
+    iframe.docShellIsActive = true;
+    if (iframe.contentDocument.readyState == "complete") {
+      this._dynamicResizer.start(panel, iframe);
+    } else {
+      // first time load, wait for load and dispatch after load
+      let mm = this.messageManager;
+      mm.addMessageListener("DOMContentLoaded", function panelBrowserOnload(e) {
+        mm.removeMessageListener("DOMContentLoaded", panelBrowserOnload);
+        setTimeout(function() {
+          if (SocialFlyout._dynamicResizer) { // may go null if hidden quickly
+            SocialFlyout._dynamicResizer.start(panel, iframe);
+          }
+        }, 0);
+      });
+    }
+  },
+
+  onHidden: function(aEvent) {
+    this._dynamicResizer.stop();
+    this._dynamicResizer = null;
+    this.iframe.docShellIsActive = false;
+  },
+
+  load: function(aURL, cb) {
+    if (!SocialSidebar.provider)
+      return;
+
+    this.panel.hidden = false;
+    let iframe = this.iframe;
+    // same url with only ref difference does not cause a new load, so we
+    // want to go right to the callback
+    let src = iframe.contentDocument && iframe.contentDocument.documentURIObject;
+    if (!src || !src.equalsExceptRef(Services.io.newURI(aURL, null, null))) {
+      let mm = this.messageManager;
+      mm.addMessageListener("DOMContentLoaded", function documentLoaded(e) {
+        mm.removeMessageListener("DOMContentLoaded", documentLoaded);
+        cb();
+      });
+      iframe.setAttribute("src", aURL);
+    } else {
+      // we still need to set the src to trigger the contents hashchange event
+      // for ref changes
+      iframe.setAttribute("src", aURL);
+      cb();
+    }
+  },
+
+  open: function(aURL, yOffset, aCallback) {
+    // Hide any other social panels that may be open.
+    document.getElementById("social-notification-panel").hidePopup();
+
+    if (!SocialUI.enabled)
+      return;
+    let panel = this.panel;
+    let iframe = this.iframe;
+
+    this.load(aURL, function() {
+      sizeSocialPanelToContent(panel, iframe);
+      let anchor = document.getElementById("social-sidebar-browser");
+      if (panel.state == "open") {
+        panel.moveToAnchor(anchor, "start_before", 0, yOffset, false);
+      } else {
+        panel.openPopup(anchor, "start_before", 0, yOffset, false, false);
+      }
+      if (aCallback) {
+        try {
+          aCallback(iframe.contentWindow);
+        } catch(e) {
+          Cu.reportError(e);
+        }
+      }
+    });
+  }
+}
+
 SocialShare = {
   get _dynamicResizer() {
     delete this._dynamicResizer;
     this._dynamicResizer = new DynamicResizeWatcher();
     return this._dynamicResizer;
   },
 
   // Share panel may be attached to the overflow or menu button depending on
@@ -229,17 +477,16 @@ SocialShare = {
     return this.panel.lastChild.firstChild;
   },
 
   uninit: function () {
     if (this.iframe) {
       let mm = this.messageManager;
       mm.removeMessageListener("PageVisibility:Show", this);
       mm.removeMessageListener("PageVisibility:Hide", this);
-      mm.removeMessageListener("Social:DOMWindowClose", this);
       this.iframe.removeEventListener("load", this);
       this.iframe.remove();
     }
   },
 
   _createFrame: function() {
     let panel = this.panel;
     if (this.iframe)
@@ -257,17 +504,16 @@ SocialShare = {
     iframe.setAttribute("messagemanagergroup", "social");
     panel.lastChild.appendChild(iframe);
     let mm = this.messageManager;
     mm.addMessageListener("PageVisibility:Show", this);
     mm.addMessageListener("PageVisibility:Hide", this);
     mm.sendAsyncMessage("Social:SetErrorURL",
                         { template: "about:socialerror?mode=compactInfo&origin=%{origin}&url=%{url}" });
     iframe.addEventListener("load", this, true);
-    mm.addMessageListener("Social:DOMWindowClose", this);
 
     this.populateProviderMenu();
   },
 
   get messageManager() {
     // The xbl bindings for the iframe may not exist yet, so we can't
     // access iframe.messageManager directly - but can get at it with this dance.
     return this.iframe.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader.messageManager;
@@ -277,19 +523,16 @@ SocialShare = {
     let iframe = this.iframe;
     switch(aMessage.name) {
       case "PageVisibility:Show":
         SocialShare._dynamicResizer.start(iframe.parentNode, iframe);
         break;
       case "PageVisibility:Hide":
         SocialShare._dynamicResizer.stop();
         break;
-      case "Social:DOMWindowClose":
-        this.panel.hidePopup();
-        break;
     }
   },
 
   handleEvent: function(event) {
     switch (event.type) {
       case "load": {
         let iframe = this.iframe;
         iframe.parentNode.removeAttribute("loading");
@@ -398,17 +641,17 @@ SocialShare = {
     // graphData is an optional param that either defines the full set of data
     // to be shared, or partial data about the current page. It is set by a call
     // in mozSocial API, or via nsContentMenu calls. If it is present, it MUST
     // define at least url. If it is undefined, we're sharing the current url in
     // the browser tab.
     let pageData = graphData ? graphData : this.currentShare;
     let sharedURI = pageData ? Services.io.newURI(pageData.url, null, null) :
                                 gBrowser.currentURI;
-    if (!SocialUI.canSharePage(sharedURI))
+    if (!SocialUI.canShareOrMarkPage(sharedURI))
       return;
 
     // the point of this action type is that we can use existing share
     // endpoints (e.g. oexchange) that do not support additional
     // socialapi functionality.  One tweak is that we shoot an event
     // containing the open graph data.
     let _dataFn;
     if (!pageData || sharedURI == gBrowser.currentURI) {
@@ -500,9 +743,671 @@ SocialShare = {
   _openPanel: function(anchor) {
     this._currentAnchor = anchor || this.anchor;
     anchor = document.getAnonymousElementByAttribute(this._currentAnchor, "class", "toolbarbutton-icon");
     this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
     Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(0);
   }
 };
 
+SocialSidebar = {
+  _openStartTime: 0,
+
+  get browser() {
+    return document.getElementById("social-sidebar-browser");
+  },
+
+  // Whether the sidebar can be shown for this window.
+  get canShow() {
+    if (!SocialUI.enabled || document.fullscreenElement)
+      return false;
+    return Social.providers.some(p => p.sidebarURL);
+  },
+
+  // Whether the user has toggled the sidebar on (for windows where it can appear)
+  get opened() {
+    let broadcaster = document.getElementById("socialSidebarBroadcaster");
+    return !broadcaster.hidden;
+  },
+
+  restoreWindowState: function() {
+    // Window state is used to allow different sidebar providers in each window.
+    // We also store the provider used in a pref as the default sidebar to
+    // maintain that state for users who do not restore window state. The
+    // existence of social.sidebar.provider means the sidebar is open with that
+    // provider.
+    this._initialized = true;
+    if (!this.canShow)
+      return;
+
+    if (Services.prefs.prefHasUserValue("social.provider.current")) {
+      // "upgrade" when the first window opens if we have old prefs.  We get the
+      // values from prefs this one time, window state will be saved when this
+      // window is closed.
+      let origin = Services.prefs.getCharPref("social.provider.current");
+      Services.prefs.clearUserPref("social.provider.current");
+      // social.sidebar.open default was true, but we only opened if there was
+      // a current provider
+      let opened = origin && true;
+      if (Services.prefs.prefHasUserValue("social.sidebar.open")) {
+        opened = origin && Services.prefs.getBoolPref("social.sidebar.open");
+        Services.prefs.clearUserPref("social.sidebar.open");
+      }
+      let data = {
+        "hidden": !opened,
+        "origin": origin
+      };
+      SessionStore.setWindowValue(window, "socialSidebar", JSON.stringify(data));
+    }
+
+    let data = SessionStore.getWindowValue(window, "socialSidebar");
+    // if this window doesn't have it's own state, use the state from the opener
+    if (!data && window.opener && !window.opener.closed) {
+      try {
+        data = SessionStore.getWindowValue(window.opener, "socialSidebar");
+      } catch(e) {
+        // Window is not tracked, which happens on osx if the window is opened
+        // from the hidden window. That happens when you close the last window
+        // without quiting firefox, then open a new window.
+      }
+    }
+    if (data) {
+      data = JSON.parse(data);
+      this.browser.setAttribute("origin", data.origin);
+      if (!data.hidden)
+        this.show(data.origin);
+    } else if (Services.prefs.prefHasUserValue("social.sidebar.provider")) {
+      // no window state, use the global state if it is available
+      this.show(Services.prefs.getCharPref("social.sidebar.provider"));
+    }
+  },
+
+  saveWindowState: function() {
+    let broadcaster = document.getElementById("socialSidebarBroadcaster");
+    let sidebarOrigin = this.browser.getAttribute("origin");
+    let data = {
+      "hidden": broadcaster.hidden,
+      "origin": sidebarOrigin
+    };
+    if (broadcaster.hidden) {
+      Services.telemetry.getHistogramById("SOCIAL_SIDEBAR_OPEN_DURATION").add(Date.now()  / 1000 - this._openStartTime);
+    } else {
+      this._openStartTime = Date.now() / 1000;
+    }
+
+    // Save a global state for users who do not restore state.
+    if (broadcaster.hidden)
+      Services.prefs.clearUserPref("social.sidebar.provider");
+    else
+      Services.prefs.setCharPref("social.sidebar.provider", sidebarOrigin);
+
+    try {
+      SessionStore.setWindowValue(window, "socialSidebar", JSON.stringify(data));
+    } catch(e) {
+      // window not tracked during uninit
+    }
+  },
+
+  setSidebarVisibilityState: function(aEnabled) {
+    let sbrowser = document.getElementById("social-sidebar-browser");
+    // it's possible we'll be called twice with aEnabled=false so let's
+    // just assume we may often be called with the same state.
+    if (aEnabled == sbrowser.docShellIsActive)
+      return;
+    sbrowser.docShellIsActive = aEnabled;
+  },
+
+  updateToggleNotifications: function() {
+    let command = document.getElementById("Social:ToggleNotifications");
+    command.setAttribute("checked", Services.prefs.getBoolPref("social.toast-notifications.enabled"));
+    command.setAttribute("hidden", !SocialUI.enabled);
+  },
+
+  update: function SocialSidebar_update() {
+    // ensure we never update before restoreWindowState
+    if (!this._initialized)
+      return;
+    this.ensureProvider();
+    this.updateToggleNotifications();
+    this._updateHeader();
+    clearTimeout(this._unloadTimeoutId);
+    // Hide the toggle menu item if the sidebar cannot appear
+    let command = document.getElementById("Social:ToggleSidebar");
+    command.setAttribute("hidden", this.canShow ? "false" : "true");
+
+    // Hide the sidebar if it cannot appear, or has been toggled off.
+    // Also set the command "checked" state accordingly.
+    let hideSidebar = !this.canShow || !this.opened;
+    let broadcaster = document.getElementById("socialSidebarBroadcaster");
+    broadcaster.hidden = hideSidebar;
+    command.setAttribute("checked", !hideSidebar);
+
+    let sbrowser = this.browser;
+
+    if (hideSidebar) {
+      sbrowser.messageManager.removeMessageListener("DOMContentLoaded", SocialSidebar._loadListener);
+      this.setSidebarVisibilityState(false);
+      // If we've been disabled, unload the sidebar content immediately;
+      // if the sidebar was just toggled to invisible, wait a timeout
+      // before unloading.
+      if (!this.canShow) {
+        this.unloadSidebar();
+      } else {
+        this._unloadTimeoutId = setTimeout(
+          this.unloadSidebar,
+          Services.prefs.getIntPref("social.sidebar.unload_timeout_ms")
+        );
+      }
+    } else {
+      sbrowser.setAttribute("origin", this.provider.origin);
+
+      // Make sure the right sidebar URL is loaded
+      if (sbrowser.getAttribute("src") != this.provider.sidebarURL) {
+        sbrowser.setAttribute("src", this.provider.sidebarURL);
+        PopupNotifications.locationChange(sbrowser);
+        document.getElementById("social-sidebar-button").setAttribute("loading", "true");
+        sbrowser.messageManager.addMessageListener("DOMContentLoaded", SocialSidebar._loadListener);
+      } else {
+        // if the document has not loaded, delay until it is
+        if (sbrowser.contentDocument.readyState != "complete") {
+          document.getElementById("social-sidebar-button").setAttribute("loading", "true");
+          sbrowser.messageManager.addMessageListener("DOMContentLoaded", SocialSidebar._loadListener);
+        } else {
+          this.setSidebarVisibilityState(true);
+        }
+      }
+    }
+    this._updateCheckedMenuItems(this.opened && this.provider ? this.provider.origin : null);
+  },
+
+  _onclick: function() {
+    Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(3);
+  },
+
+  _loadListener: function SocialSidebar_loadListener() {
+    let sbrowser = SocialSidebar.browser;
+    sbrowser.messageManager.removeMessageListener("DOMContentLoaded", SocialSidebar._loadListener);
+    document.getElementById("social-sidebar-button").removeAttribute("loading");
+    SocialSidebar.setSidebarVisibilityState(true);
+    sbrowser.addEventListener("click", SocialSidebar._onclick, true);
+  },
+
+  unloadSidebar: function SocialSidebar_unloadSidebar() {
+    let sbrowser = SocialSidebar.browser;
+    if (!sbrowser.hasAttribute("origin"))
+      return;
+
+    sbrowser.removeEventListener("click", SocialSidebar._onclick, true);
+    sbrowser.stop();
+    sbrowser.removeAttribute("origin");
+    sbrowser.setAttribute("src", "about:blank");
+    // We need to explicitly create a new content viewer because the old one
+    // doesn't get destroyed until about:blank has loaded (which does not happen
+    // as long as the element is hidden).
+    sbrowser.messageManager.sendAsyncMessage("Social:ClearFrame");
+    SocialFlyout.unload();
+  },
+
+  _unloadTimeoutId: 0,
+
+  _provider: null,
+  ensureProvider: function() {
+    if (this._provider)
+      return;
+    // origin for sidebar is persisted, so get the previously selected sidebar
+    // first, otherwise fallback to the first provider in the list
+    let origin = this.browser.getAttribute("origin");
+    let providers = Social.providers.filter(p => p.sidebarURL);
+    let provider;
+    if (origin)
+      provider = Social._getProviderFromOrigin(origin);
+    if (!provider && providers.length > 0)
+      provider = providers[0];
+    if (provider)
+      this.provider = provider;
+  },
+
+  get provider() {
+    return this._provider;
+  },
+
+  set provider(provider) {
+    if (!provider || provider.sidebarURL) {
+      this._provider = provider;
+      this._updateHeader();
+      this._updateCheckedMenuItems(provider && provider.origin);
+      this.update();
+    }
+  },
+
+  disableProvider: function(origin) {
+    if (this._provider && this._provider.origin == origin) {
+      this._provider = null;
+      // force a selection of the next provider if there is one
+      this.ensureProvider();
+    }
+  },
+
+  _updateHeader: function() {
+    let provider = this.provider;
+    let image, title;
+    if (provider) {
+      image = "url(" + (provider.icon32URL || provider.iconURL) + ")";
+      title = provider.name;
+    }
+    document.getElementById("social-sidebar-favico").style.listStyleImage = image;
+    document.getElementById("social-sidebar-title").value = title;
+  },
+
+  _updateCheckedMenuItems: function(origin) {
+    // update selected menuitems
+    let menuitems = document.getElementsByClassName("social-provider-menuitem");
+    for (let mi of menuitems) {
+      if (origin && mi.getAttribute("origin") == origin) {
+        mi.setAttribute("checked", "true");
+        mi.setAttribute("oncommand", "SocialSidebar.hide();");
+      } else if (mi.getAttribute("checked")) {
+        mi.removeAttribute("checked");
+        mi.setAttribute("oncommand", "SocialSidebar.show(this.getAttribute('origin'));");
+      }
+    }
+  },
+
+  show: function(origin) {
+    // always show the sidebar, and set the provider
+    let broadcaster = document.getElementById("socialSidebarBroadcaster");
+    broadcaster.hidden = false;
+    if (origin)
+      this.provider = Social._getProviderFromOrigin(origin);
+    else
+      SocialSidebar.update();
+    this.saveWindowState();
+    Services.telemetry.getHistogramById("SOCIAL_SIDEBAR_STATE").add(true);
+  },
+
+  hide: function() {
+    let broadcaster = document.getElementById("socialSidebarBroadcaster");
+    broadcaster.hidden = true;
+    this._updateCheckedMenuItems();
+    this.clearProviderMenus();
+    SocialSidebar.update();
+    this.saveWindowState();
+    Services.telemetry.getHistogramById("SOCIAL_SIDEBAR_STATE").add(false);
+  },
+
+  toggleSidebar: function SocialSidebar_toggle() {
+    let broadcaster = document.getElementById("socialSidebarBroadcaster");
+    if (broadcaster.hidden)
+      this.show();
+    else
+      this.hide();
+  },
+
+  populateSidebarMenu: function(event) {
+    // Providers are removed from the view->sidebar menu when there is a change
+    // in providers, so we only have to populate onshowing if there are no
+    // provider menus. We populate this menu so long as there are enabled
+    // providers with sidebars.
+    let popup = event.target;
+    let providerMenuSeps = popup.getElementsByClassName("social-provider-menu");
+    if (providerMenuSeps[0].previousSibling.nodeName == "menuseparator")
+      SocialSidebar.populateProviderMenu(providerMenuSeps[0]);
+  },
+
+  clearProviderMenus: function() {
+    // called when there is a change in the provider list we clear all menus,
+    // they will be repopulated when the menu is shown
+    let providerMenuSeps = document.getElementsByClassName("social-provider-menu");
+    for (let providerMenuSep of providerMenuSeps) {
+      while (providerMenuSep.previousSibling.nodeName == "menuitem") {
+        let menu = providerMenuSep.parentNode;
+        menu.removeChild(providerMenuSep.previousSibling);
+      }
+    }
+  },
+
+  populateProviderMenu: function(providerMenuSep) {
+    let menu = providerMenuSep.parentNode;
+    // selectable providers are inserted before the provider-menu seperator,
+    // remove any menuitems in that area
+    while (providerMenuSep.previousSibling.nodeName == "menuitem") {
+      menu.removeChild(providerMenuSep.previousSibling);
+    }
+    // only show a selection in the sidebar header menu if there is more than one
+    let providers = Social.providers.filter(p => p.sidebarURL);
+    if (providers.length < 2 && menu.id != "viewSidebarMenu") {
+      providerMenuSep.hidden = true;
+      return;
+    }
+    let topSep = providerMenuSep.previousSibling;
+    for (let provider of providers) {
+      let menuitem = document.createElement("menuitem");
+      menuitem.className = "menuitem-iconic social-provider-menuitem";
+      menuitem.setAttribute("image", provider.iconURL);
+      menuitem.setAttribute("label", provider.name);
+      menuitem.setAttribute("origin", provider.origin);
+      if (this.opened && provider == this.provider) {
+        menuitem.setAttribute("checked", "true");
+        menuitem.setAttribute("oncommand", "SocialSidebar.hide();");
+      } else {
+        menuitem.setAttribute("oncommand", "SocialSidebar.show(this.getAttribute('origin'));");
+      }
+      menu.insertBefore(menuitem, providerMenuSep);
+    }
+    topSep.hidden = topSep.nextSibling == providerMenuSep;
+    providerMenuSep.hidden = !providerMenuSep.nextSibling;
+  }
+}
+
+// this helper class is used by removable/customizable buttons to handle
+// widget creation/destruction
+
+// When a provider is installed we show all their UI so the user will see the
+// functionality of what they installed. The user can later customize the UI,
+// moving buttons around or off the toolbar.
+//
+// On startup, we create the button widgets of any enabled provider.
+// CustomizableUI handles placement and persistence of placement.
+function ToolbarHelper(type, createButtonFn, listener) {
+  this._createButton = createButtonFn;
+  this._type = type;
+
+  if (listener) {
+    CustomizableUI.addListener(listener);
+    // remove this listener on window close
+    window.addEventListener("unload", () => {
+      CustomizableUI.removeListener(listener);
+    });
+  }
+}
+
+ToolbarHelper.prototype = {
+  idFromOrigin: function(origin) {
+    // this id needs to pass the checks in CustomizableUI, so remove characters
+    // that wont pass.
+    return this._type + "-" + Services.io.newURI(origin, null, null).hostPort.replace(/[\.:]/g,'-');
+  },
+
+  // should be called on disable of a provider
+  removeProviderButton: function(origin) {
+    CustomizableUI.destroyWidget(this.idFromOrigin(origin));
+  },
+
+  clearPalette: function() {
+    for (let p of Social.providers) {
+      this.removeProviderButton(p.origin);
+    }
+  },
+
+  // should be called on enable of a provider
+  populatePalette: function() {
+    if (!Social.enabled) {
+      this.clearPalette();
+      return;
+    }
+
+    // create any buttons that do not exist yet if they have been persisted
+    // as a part of the UI (otherwise they belong in the palette).
+    for (let provider of Social.providers) {
+      let id = this.idFromOrigin(provider.origin);
+      this._createButton(id, provider);
+    }
+  }
+}
+
+var SocialStatusWidgetListener = {
+  _getNodeOrigin: function(aWidgetId) {
+    // we rely on the button id being the same as the widget.
+    let node = document.getElementById(aWidgetId);
+    if (!node)
+      return null
+    if (!node.classList.contains("social-status-button"))
+      return null
+    return node.getAttribute("origin");
+  },
+  onWidgetAdded: function(aWidgetId, aArea, aPosition) {
+    let origin = this._getNodeOrigin(aWidgetId);
+    if (origin)
+      SocialStatus.updateButton(origin);
+  },
+  onWidgetRemoved: function(aWidgetId, aPrevArea) {
+    let origin = this._getNodeOrigin(aWidgetId);
+    if (!origin)
+      return;
+    // When a widget is demoted to the palette ('removed'), it's visual
+    // style should change.
+    SocialStatus.updateButton(origin);
+    SocialStatus._removeFrame(origin);
+  }
+}
+
+SocialStatus = {
+  populateToolbarPalette: function() {
+    this._toolbarHelper.populatePalette();
+
+    for (let provider of Social.providers)
+      this.updateButton(provider.origin);
+  },
+
+  removeProvider: function(origin) {
+    this._removeFrame(origin);
+    this._toolbarHelper.removeProviderButton(origin);
+  },
+
+  reloadProvider: function(origin) {
+    let button = document.getElementById(this._toolbarHelper.idFromOrigin(origin));
+    if (button && button.getAttribute("open") == "true")
+      document.getElementById("social-notification-panel").hidePopup();
+    this._removeFrame(origin);
+  },
+
+  _removeFrame: function(origin) {
+    let notificationFrameId = "social-status-" + origin;
+    let frame = document.getElementById(notificationFrameId);
+    if (frame) {
+      frame.parentNode.removeChild(frame);
+    }
+  },
+
+  get _toolbarHelper() {
+    delete this._toolbarHelper;
+    this._toolbarHelper = new ToolbarHelper("social-status-button",
+                                            CreateSocialStatusWidget,
+                                            SocialStatusWidgetListener);
+    return this._toolbarHelper;
+  },
+
+  updateButton: function(origin) {
+    let id = this._toolbarHelper.idFromOrigin(origin);
+    let widget = CustomizableUI.getWidget(id);
+    if (!widget)
+      return;
+    let button = widget.forWindow(window).node;
+    if (button) {
+      // we only grab the first notification, ignore all others
+      let provider = Social._getProviderFromOrigin(origin);
+      let icons = provider.ambientNotificationIcons;
+      let iconNames = Object.keys(icons);
+      let notif = icons[iconNames[0]];
+
+      // The image and tooltip need to be updated for
+      // ambient notification changes.
+      let iconURL = provider.icon32URL || provider.iconURL;
+      let tooltiptext;
+      if (!notif || !widget.areaType) {
+        button.style.listStyleImage = "url(" + iconURL + ")";
+        button.setAttribute("badge", "");
+        button.setAttribute("aria-label", "");
+        button.setAttribute("tooltiptext", provider.name);
+        return;
+      }
+      button.style.listStyleImage = "url(" + (notif.iconURL || iconURL) + ")";
+      button.setAttribute("tooltiptext", notif.label || provider.name);
+
+      let badge = notif.counter || "";
+      button.setAttribute("badge", badge);
+      let ariaLabel = notif.label;
+      // if there is a badge value, we must use a localizable string to insert it.
+      if (badge)
+        ariaLabel = gNavigatorBundle.getFormattedString("social.aria.toolbarButtonBadgeText",
+                                                        [ariaLabel, badge]);
+      button.setAttribute("aria-label", ariaLabel);
+    }
+  },
+
+  _onclose: function(frame) {
+    frame.removeEventListener("close", this._onclose, true);
+    frame.removeEventListener("click", this._onclick, true);
+  },
+
+  _onclick: function() {
+    Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(1);
+  },
+
+  showPopup: function(aToolbarButton) {
+    // attach our notification panel if necessary
+    let origin = aToolbarButton.getAttribute("origin");
+    let provider = Social._getProviderFromOrigin(origin);
+
+    PanelFrame.showPopup(window, aToolbarButton, "social", origin,
+                         provider.statusURL, provider.getPageSize("status"),
+                         (frame) => {
+                          frame.addEventListener("close", () => { SocialStatus._onclose(frame) }, true);
+                          frame.addEventListener("click", this._onclick, true);
+                        });
+    Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(1);
+  }
+};
+
+
+var SocialMarksWidgetListener = {
+  onWidgetAdded: function(aWidgetId, aArea, aPosition) {
+    let node = document.getElementById(aWidgetId);
+    if (!node || !node.classList.contains("social-mark-button"))
+      return;
+    node._receiveMessage = node.receiveMessage.bind(node);
+    messageManager.addMessageListener("Social:ErrorPageNotify", node._receiveMessage);
+  },
+  onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer, isRemoval) {
+    if (!isRemoval || !aNode || !aNode.classList.contains("social-mark-button"))
+      return;
+    messageManager.removeMessageListener("Social:ErrorPageNotify", aNode._receiveMessage);
+    delete aNode._receiveMessage;
+  }
+}
+
+/**
+ * SocialMarks
+ *
+ * Handles updates to toolbox and signals all buttons to update when necessary.
+ */
+SocialMarks = {
+  get nodes() {
+    for (let p of Social.providers.filter(p => p.markURL)) {
+      let widgetId = SocialMarks._toolbarHelper.idFromOrigin(p.origin);
+      let widget = CustomizableUI.getWidget(widgetId);
+      if (!widget)
+        continue;
+      let node = widget.forWindow(window).node;
+      if (node)
+        yield node;
+    }
+  },
+  update: function() {
+    // querySelectorAll does not work on the menu panel, so we have to do this
+    // the hard way.
+    for (let node of this.nodes) {
+      // xbl binding is not complete on startup when buttons are not in toolbar,
+      // verify update is available
+      if (node.update) {
+        node.update();
+      }
+    }
+  },
+
+  getProviders: function() {
+    // only rely on providers that the user has placed in the UI somewhere. This
+    // also means that populateToolbarPalette must be called prior to using this
+    // method, otherwise you get a big fat zero. For our use case with context
+    // menu's, this is ok.
+    return Social.providers.filter(p => p.markURL &&
+                                        document.getElementById(this._toolbarHelper.idFromOrigin(p.origin)));
+  },
+
+  populateContextMenu: function() {
+    // only show a selection if enabled and there is more than one
+    let providers = this.getProviders();
+
+    // remove all previous entries by class
+    let menus = [...document.getElementsByClassName("context-socialmarks")];
+    for (let m of menus) {
+      m.parentNode.removeChild(m);
+    }
+
+    let contextMenus = [
+      {
+        type: "link",
+        id: "context-marklinkMenu",
+        label: "social.marklinkMenu.label"
+      },
+      {
+        type: "page",
+        id: "context-markpageMenu",
+        label: "social.markpageMenu.label"
+      }
+    ];
+    for (let cfg of contextMenus) {
+      this._populateContextPopup(cfg, providers);
+    }
+    this.update();
+  },
+
+  MENU_LIMIT: 3, // adjustable for testing
+  _populateContextPopup: function(menuInfo, providers) {
+    let menu = document.getElementById(menuInfo.id);
+    let popup = menu.firstChild;
+    for (let provider of providers) {
+      // We show up to MENU_LIMIT providers as single menuitems's at the top
+      // level of the context menu, if we have more than that, dump them *all*
+      // into the menu popup.
+      let mi = document.createElement("menuitem");
+      mi.setAttribute("oncommand", "gContextMenu.markLink(this.getAttribute('origin'));");
+      mi.setAttribute("origin", provider.origin);
+      mi.setAttribute("image", provider.iconURL);
+      if (providers.length <= this.MENU_LIMIT) {
+        // an extra class to make enable/disable easy
+        mi.setAttribute("class", "menuitem-iconic context-socialmarks context-mark"+menuInfo.type);
+        let menuLabel = gNavigatorBundle.getFormattedString(menuInfo.label, [provider.name]);
+        mi.setAttribute("label", menuLabel);
+        menu.parentNode.insertBefore(mi, menu);
+      } else {
+        mi.setAttribute("class", "menuitem-iconic context-socialmarks");
+        mi.setAttribute("label", provider.name);
+        popup.appendChild(mi);
+      }
+    }
+  },
+
+  populateToolbarPalette: function() {
+    this._toolbarHelper.populatePalette();
+    this.populateContextMenu();
+  },
+
+  removeProvider: function(origin) {
+    this._toolbarHelper.removeProviderButton(origin);
+  },
+
+  get _toolbarHelper() {
+    delete this._toolbarHelper;
+    this._toolbarHelper = new ToolbarHelper("social-mark-button",
+                                            CreateSocialMarkWidget,
+                                            SocialMarksWidgetListener);
+    return this._toolbarHelper;
+  },
+
+  markLink: function(aOrigin, aUrl, aTarget) {
+    // find the button for this provider, and open it
+    let id = this._toolbarHelper.idFromOrigin(aOrigin);
+    document.getElementById(id).markLink(aUrl, aTarget);
+  }
+};
+
 })();
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -14,16 +14,40 @@
 #main-window:not([chromehidden~="toolbar"]) {
 %ifdef XP_MACOSX
   min-width: 335px;
 %else
   min-width: 300px;
 %endif
 }
 
+/* These values are chosen to keep the Loop detached chat window from
+ * getting too small.  When it's too small, three bad things happen:
+ *
+ * - It looks terrible
+ * - It's not really usable
+ * - It's possible for the user to be transmitting video that's cropped by the
+ *   the edge of the window, so that they're not aware of it, which is a
+ *   privacy problem
+ *
+ * Note that if the chat window grows more users than Loop who want this
+ * ability, we'll need to generalize.  A partial patch for this is in
+ * bug 1112264.
+ */
+
+#chat-window {
+ /*
+  * In some ideal world, we'd have a simple way to express "block resizing
+  * along any dimension beyond the point at which an overflow event would
+  * occur".  But none of -moz-{fit,max,min}-content do what we want here. So..
+  */
+  min-width: 260px;
+  min-height: 315px;
+}
+
 #main-window[customize-entered] {
   min-width: -moz-fit-content;
 }
 
 searchbar {
   -moz-binding: url("chrome://browser/content/search/search.xml#searchbar");
 }
 
@@ -883,26 +907,101 @@ html|*#gcli-output-frame,
 .browserStack[responsivemode] {
   transition-property: min-width, max-width, min-height, max-height;
 }
 
 .browserStack[responsivemode][notransition] {
   transition: none;
 }
 
+toolbarbutton[type="socialmark"] {
+  -moz-binding: url("chrome://browser/content/socialmarks.xml#toolbarbutton-marks");
+}
+
 panelview > .social-panel-frame {
   width: auto;
   height: auto;
 }
 
 /* Translation */
 notification[value="translation"] {
   -moz-binding: url("chrome://browser/content/translation-infobar.xml#translationbar");
 }
 
+/* Social */
+/* Note the chatbox 'width' values are duplicated in socialchat.xml */
+chatbox {
+  -moz-binding: url("chrome://browser/content/socialchat.xml#chatbox");
+  transition: height 150ms ease-out, width 150ms ease-out;
+  height: 290px;
+  width: 300px; /* CHAT_WIDTH_OPEN in socialchat.xml */
+}
+
+chatbox[customSize] {
+  width: 350px; /* CHAT_WIDTH_OPEN_ALT in socialchat.xml */
+}
+
+#chat-window[customSize] {
+  min-width: 350px;
+}
+
+chatbox[customSize="loopChatEnabled"] {
+  /* 430px as defined per UX */
+  height: 430px;
+}
+
+#chat-window[customSize="loopChatEnabled"] {
+  /* 325px + 30px top bar height. */
+  min-height: calc(325px + 30px);
+}
+
+chatbox[customSize="loopChatMessageAppended"] {
+  /* 430px as defined per UX */
+  height: 430px;
+}
+
+chatbox[customSize="loopChatDisabledMessageAppended"] {
+  /* 388px as defined per UX */
+  height: 388px;
+}
+
+#chat-window[customSize="loopChatMessageAppended"] {
+  /* 445px + 30px top bar height. */
+  min-height: calc(400px + 30px);
+}
+
+chatbox[minimized="true"] {
+  width: 160px;
+  height: 20px; /* CHAT_WIDTH_MINIMIZED in socialchat.xml */
+}
+
+chatbar {
+  -moz-binding: url("chrome://browser/content/socialchat.xml#chatbar");
+  height: 0;
+  max-height: 0;
+}
+
+.chatbar-innerbox {
+  margin: -285px 0 0;
+}
+
+chatbar[customSize] > .chatbar-innerbox {
+  /* 450px to make room for the maximum custom-size chatbox; currently 'loopChatMessageAppended'. */
+  margin-top: -450px;
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+  #social-sidebar-favico,
+  .social-status-button,
+  .chat-status-icon {
+    image-rendering: -moz-crisp-edges;
+  }
+}
+
 /** See bug 872317 for why the following rule is necessary. */
 
 #downloads-button {
   -moz-binding: url("chrome://browser/content/downloads/download.xml#download-toolbarbutton");
 }
 
 /*** Visibility of downloads indicator controls ***/
 
@@ -924,16 +1023,28 @@ toolbarpaletteitem[place="palette"] > #d
 
 #downloads-button:-moz-any([progress], [counter], [paused]) #downloads-indicator-icon,
 #downloads-button:not(:-moz-any([progress], [counter], [paused]))
                                                    #downloads-indicator-progress-area
 {
   visibility: hidden;
 }
 
+/* hide chat chrome when chat is fullscreen */
+#chat-window[sizemode="fullscreen"] chatbox > .chat-titlebar {
+  display: none;
+}
+
+/* hide chatbar and sidebar if browser tab is fullscreen */
+#main-window[inFullscreen][inDOMFullscreen] chatbar,
+#main-window[inFullscreen][inDOMFullscreen] #social-sidebar-box,
+#main-window[inFullscreen][inDOMFullscreen] #social-sidebar-splitter {
+  display: none;
+}
+
 /* Combobox dropdown renderer */
 #ContentSelectDropdown > menupopup {
   /* The menupopup itself should always be rendered LTR to ensure the scrollbar aligns with
    * the dropdown arrow on the dropdown widget. If a menuitem is RTL, its style will be set accordingly */
   direction: ltr;
 }
 
 /* Indent options in optgroups */
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2289,16 +2289,19 @@ function BrowserViewSourceOfDocument(aAr
         // that of the original URL, so disable remoteness if necessary for this
         // URL.
         let contentProcess = Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT
         forceNotRemote =
           gMultiProcessBrowser &&
           !E10SUtils.canLoadURIInProcess(args.URL, contentProcess)
       }
 
+      // In the case of sidebars and chat windows, gBrowser is defined but null,
+      // because no #content element exists.  For these cases, we need to find
+      // the most recent browser window.
       // In the case of popups, we need to find a non-popup browser window.
       if (!tabBrowser || !window.toolbar.visible) {
         // This returns only non-popup browser windows by default.
         let browserWindow = RecentWindow.getMostRecentBrowserWindow();
         tabBrowser = browserWindow.gBrowser;
       }
 
       // `viewSourceInBrowser` will load the source content from the page
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -266,16 +266,33 @@
           <toolbarbutton id="add-share-provider" class="toolbarbutton share-provider-button" type="radio"
                          group="share-providers" tooltiptext="&findShareServices.label;"
                          oncommand="SocialShare.showDirectory()"/>
         </arrowscrollbox>
       </hbox>
       <hbox id="share-container" flex="1"/>
     </panel>
 
+    <panel id="social-notification-panel"
+           class="social-panel"
+           type="arrow"
+           hidden="true"
+           noautofocus="true"/>
+    <panel id="social-flyout-panel"
+           class="social-panel"
+           onpopupshown="SocialFlyout.onShown()"
+           onpopuphidden="SocialFlyout.onHidden()"
+           side="right"
+           type="arrow"
+           hidden="true"
+           flip="slide"
+           rolluponmousewheel="true"
+           noautofocus="true"
+           position="topcenter topright"/>
+
     <menupopup id="toolbar-context-menu"
                onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('viewToolbarsMenuSeparator'));">
       <menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)"
                 accesskey="&customizeMenu.moveToPanel.accesskey;"
                 label="&customizeMenu.moveToPanel.label;"
                 contexttype="toolbaritem"
                 class="customize-context-moveToPanel"/>
       <menuitem oncommand="gCustomizeMode.removeFromArea(document.popupNode)"
@@ -1059,16 +1076,66 @@
       <vbox id="appcontent" flex="1">
         <notificationbox id="high-priority-global-notificationbox" notificationside="top"/>
         <tabbrowser id="content"
                     flex="1" contenttooltip="aHTMLTooltip"
                     tabcontainer="tabbrowser-tabs"
                     contentcontextmenu="contentAreaContextMenu"
                     autocompletepopup="PopupAutoComplete"
                     selectmenulist="ContentSelectDropdown"/>
+        <chatbar id="pinnedchats" layer="true" mousethrough="always" hidden="true"/>
+      </vbox>
+      <splitter id="social-sidebar-splitter"
+                class="chromeclass-extrachrome sidebar-splitter"
+                observes="socialSidebarBroadcaster"/>
+      <vbox id="social-sidebar-box"
+            class="chromeclass-extrachrome"
+            observes="socialSidebarBroadcaster"
+            persist="width">
+
+        <sidebarheader id="social-sidebar-header" class="sidebar-header" align="center">
+          <image id="social-sidebar-favico"/>
+          <label id="social-sidebar-title" class="sidebar-title" persist="value" flex="1" crop="end" control="sidebar"/>
+          <toolbarbutton id="social-sidebar-button"
+                         class="toolbarbutton-1"
+                         type="menu">
+            <menupopup id="social-statusarea-popup" position="after_end">
+              <menuitem class="social-toggle-sidebar-menuitem"
+                        type="checkbox"
+                        autocheck="false"
+                        command="Social:ToggleSidebar"
+                        label="&social.toggleSidebar.label;"
+                        accesskey="&social.toggleSidebar.accesskey;"/>
+              <menuitem class="social-toggle-notifications-menuitem"
+                        type="checkbox"
+                        autocheck="false"
+                        command="Social:ToggleNotifications"
+                        label="&social.toggleNotifications.label;"
+                        accesskey="&social.toggleNotifications.accesskey;"/>
+              <menuseparator/>
+              <menuseparator class="social-provider-menu" hidden="true"/>
+              <menuitem class="social-addons-menuitem" command="Social:Addons"
+                        label="&social.addons.label;"/>
+              <menuitem label="&social.learnMore.label;"
+                        accesskey="&social.learnMore.accesskey;"
+                        oncommand="SocialUI.showLearnMore();"/>
+            </menupopup>
+          </toolbarbutton>
+        </sidebarheader>
+
+        <browser id="social-sidebar-browser"
+                 type="content"
+                 context="contentAreaContextMenu"
+                 message="true"
+                 messagemanagergroup="social"
+                 disableglobalhistory="true"
+                 tooltip="aHTMLTooltip"
+                 popupnotificationanchor="social-sidebar-favico"
+                 flex="1"
+                 style="min-width: 14em; width: 18em; max-width: 36em;"/>
       </vbox>
       <vbox id="browser-border-end" hidden="true" layer="true"/>
     </hbox>
 #include ../../components/customizableui/content/customizeMode.inc.xul
   </deck>
 
   <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden="true">
     <html:div class="pointerlockfswarning-domain-text">
new file mode 100644
--- /dev/null
+++ b/browser/base/content/chatWindow.xul
@@ -0,0 +1,170 @@
+#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;"
+        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:OpenFile',
+                         'Browser:SendLink', 'cmd_pageSetup', 'cmd_print',
+                         'cmd_find', 'cmd_findAgain', 'cmd_findPrevious',
+                         'cmd_fullZoomReduce', 'cmd_fullZoomEnlarge', 'cmd_fullZoomReset',
+#ifdef XP_MACOSX
+                         'viewToolbarsMenu', 'viewSidebarMenuMenu',
+                         'viewFullZoomMenu', 'pageStyleMenu', 'charsetMenu',
+#else
+                         'Browser:OpenLocation', 'Tools:Search',
+#endif
+                         'Tools:Sanitize', 'Tools:DevToolbox',
+                         'key_selectTab1', 'key_selectTab2', 'key_selectTab3',
+                         'key_selectTab4', 'key_selectTab5', 'key_selectTab6',
+                         'key_selectTab7', 'key_selectTab8', 'key_selectLastTab',
+                         'viewHistorySidebar', 'viewBookmarksSidebar',
+                         'Browser:AddBookmarkAs', 'Browser:BookmarkAllTabs'];
+
+    for (let disabledItem of disabledItems) {
+      document.getElementById(disabledItem).setAttribute("disabled", "true");
+    }
+
+    window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
+      new chatBrowserAccess();
+
+    // initialise the offline listener
+    BrowserOffline.init();
+  },
+
+  onUnload: function() {
+    BrowserOffline.uninit();
+  }
+}
+
+// define a popupnotifications handler for this window.  we don't use
+// an iconbox here, and only support the browser frame for chat.
+XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function () {
+  let tmp = {};
+  Cu.import("resource://gre/modules/PopupNotifications.jsm", tmp);
+  try {
+    return new tmp.PopupNotifications(document.getElementById("chatter").content,
+                                      document.getElementById("notification-popup"),
+                                      null);
+  } catch (ex) {
+    console.error(ex);
+    return null;
+  }
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
+                                  "resource:///modules/RecentWindow.jsm");
+
+function chatBrowserAccess() { }
+
+chatBrowserAccess.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow, Ci.nsISupports]),
+
+  _openURIInNewTab: function(aURI, aWhere) {
+    if (aWhere != Ci.nsIBrowserDOMWindow.OPEN_NEWTAB)
+      return null;
+
+    let win = RecentWindow.getMostRecentBrowserWindow();
+    if (!win) {
+      // We couldn't find a suitable window, a new one needs to be opened.
+      return null;
+    }
+
+    let loadInBackground =
+      Services.prefs.getBoolPref("browser.tabs.loadDivertedInBackground");
+    let tab = win.gBrowser.loadOneTab(aURI ? aURI.spec : "about:blank",
+                                      {inBackground: loadInBackground});
+    let browser = win.gBrowser.getBrowserForTab(tab);
+    win.focus();
+
+    return browser;
+  },
+
+  openURI: function (aURI, aOpener, aWhere, aContext) {
+    let browser = this._openURIInNewTab(aURI, aWhere);
+    return browser ? browser.contentWindow : null;
+  },
+
+  openURIInFrame: function browser_openURIInFrame(aURI, aParams, aWhere, aContext) {
+    let browser = this._openURIInNewTab(aURI, aWhere);
+    return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
+  },
+
+  isTabContentWindow: function (aWindow) {
+    return this.contentWindow == aWindow;
+  },
+
+  canClose() {
+    let {BrowserUtils} = Cu.import("resource://gre/modules/BrowserUtils.jsm", {});
+    return BrowserUtils.canCloseWindow(window);
+  },
+};
+
+</script>
+
+#include browser-sets.inc
+
+#ifdef XP_MACOSX
+#include browser-menubar.inc
+#endif
+
+  <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>
+
+#include popup-notifications.inc
+
+  </popupset>
+
+  <commandset id="editMenuCommands"/>
+  <chatbox id="chatter" flex="1"/>
+</window>
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -708,16 +708,20 @@ var PageMetadataMessenger = {
       }
     }
   }
 }
 PageMetadataMessenger.init();
 
 addEventListener("ActivateSocialFeature", function (aEvent) {
   let document = content.document;
+  if (PrivateBrowsingUtils.isContentWindowPrivate(content)) {
+    Cu.reportError("cannot use social providers in private windows");
+    return;
+  }
   let dwu = content.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIDOMWindowUtils);
   if (!dwu.isHandlingUserInput) {
     Cu.reportError("attempt to activate provider without user input from " + document.nodePrincipal.origin);
     return;
   }
 
   let node = aEvent.target;
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -345,16 +345,38 @@ nsContextMenu.prototype = {
 
     // BiDi UI
     this.showItem("context-sep-bidi", !this.onNumeric && top.gBidiUI);
     this.showItem("context-bidi-text-direction-toggle",
                   this.onTextInput && !this.onNumeric && top.gBidiUI);
     this.showItem("context-bidi-page-direction-toggle",
                   !this.onTextInput && top.gBidiUI);
 
+    // SocialMarks. Marks does not work with text selections, only links. If
+    // there is more than MENU_LIMIT providers, we show a submenu for them,
+    // otherwise we have a menuitem per provider (added in SocialMarks class).
+    let markProviders = SocialMarks.getProviders();
+    let enablePageMarks = markProviders.length > 0 && !(this.onLink || this.onImage
+                            || this.onVideo || this.onAudio);
+    this.showItem("context-markpageMenu", enablePageMarks && markProviders.length > SocialMarks.MENU_LIMIT);
+    let enablePageMarkItems = enablePageMarks && markProviders.length <= SocialMarks.MENU_LIMIT;
+    let linkmenus = document.getElementsByClassName("context-markpage");
+    for (let m of linkmenus) {
+      m.hidden = !enablePageMarkItems;
+    }
+
+    let enableLinkMarks = markProviders.length > 0 &&
+                            ((this.onLink && !this.onMailtoLink) || this.onPlainTextLink);
+    this.showItem("context-marklinkMenu", enableLinkMarks && markProviders.length > SocialMarks.MENU_LIMIT);
+    let enableLinkMarkItems = enableLinkMarks && markProviders.length <= SocialMarks.MENU_LIMIT;
+    linkmenus = document.getElementsByClassName("context-marklink");
+    for (let m of linkmenus) {
+      m.hidden = !enableLinkMarkItems;
+    }
+
     // SocialShare
     let shareButton = SocialShare.shareButton;
     let shareEnabled = shareButton && !shareButton.disabled && !this.onSocial;
     let pageShare = shareEnabled && !(this.isContentSelected ||
                             this.onTextInput || this.onLink || this.onImage ||
                             this.onVideo || this.onAudio || this.onCanvas);
     this.showItem("context-sharepage", pageShare);
     this.showItem("context-shareselect", shareEnabled && this.isContentSelected);
@@ -1041,24 +1063,32 @@ nsContextMenu.prototype = {
                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     let referrer = gContextMenuContentData.referrer;
     openUILinkIn(gContextMenuContentData.docLocation, "current",
                  { disallowInheritPrincipal: true,
                    referrerURI: referrer ? makeURI(referrer) : null });
   },
 
   reload: function(event) {
-    BrowserReloadOrDuplicate(event);
+    if (this.onSocial) {
+      // full reload of social provider
+      Social._getProviderFromOrigin(this.browser.getAttribute("origin")).reload();
+    } else {
+      BrowserReloadOrDuplicate(event);
+    }
   },
 
   // View Partial Source
   viewPartialSource: function(aContext) {
     let inWindow = !Services.prefs.getBoolPref("view_source.tab");
     let openSelectionFn = inWindow ? null : function() {
       let tabBrowser = gBrowser;
+      // In the case of sidebars and chat windows, gBrowser is defined but null,
+      // because no #content element exists.  For these cases, we need to find
+      // the most recent browser window.
       // In the case of popups, we need to find a non-popup browser window.
       if (!tabBrowser || !window.toolbar.visible) {
         // This returns only non-popup browser windows by default.
         let browserWindow = RecentWindow.getMostRecentBrowserWindow();
         tabBrowser = browserWindow.gBrowser;
       }
       let tab = tabBrowser.loadOneTab("about:blank", {
         relatedToCurrent: true,
@@ -1697,16 +1727,20 @@ nsContextMenu.prototype = {
                                                 message.data.description)
                                   .catch(Components.utils.reportError);
     };
     mm.addMessageListener("ContextMenu:BookmarkFrame:Result", onMessage);
 
     mm.sendAsyncMessage("ContextMenu:BookmarkFrame", null, { target: this.target });
   },
 
+  markLink: function CM_markLink(origin) {
+    // send link to social, if it is the page url linkURI will be null
+    SocialMarks.markLink(origin, this.linkURI ? this.linkURI.spec : null, this.target);
+  },
   shareLink: function CM_shareLink() {
     SocialShare.sharePage(null, { url: this.linkURI.spec }, this.target);
   },
 
   shareImage: function CM_shareImage() {
     SocialShare.sharePage(null, { url: this.imageURL, previews: [ this.mediaURL ] }, this.target);
   },
 
--- a/browser/base/content/social-content.js
+++ b/browser/base/content/social-content.js
@@ -39,16 +39,25 @@ addEventListener("DOMTitleChanged", func
   if (!gDOMTitleChangedByUs) {
     sendAsyncMessage("Social:DOMTitleChanged", {
       title: e.target.title
     });
   }
   gDOMTitleChangedByUs = false;
 });
 
+addEventListener("Social:Notification", function(event) {
+  let frame = docShell.chromeEventHandler;
+  let origin = frame.getAttribute("origin");
+  sendAsyncMessage("Social:Notification", {
+    "origin": origin,
+    "detail": JSON.parse(event.detail)
+  });
+});
+
 addMessageListener("Social:OpenGraphData", (message) => {
   let ev = new content.CustomEvent("OpenGraphData", { detail: JSON.stringify(message.data) });
   content.dispatchEvent(ev);
 });
 
 addMessageListener("Social:ClearFrame", (message) => {
   docShell.createAboutBlankContentViewer(null);
 });
new file mode 100644
--- /dev/null
+++ b/browser/base/content/socialchat.xml
@@ -0,0 +1,913 @@
+<?xml version="1.0"?>
+
+<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" 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="crop=titlecrop,value=label" crop="end"/>
+        </xul:hbox>
+        <xul:toolbarbutton anonid="webRTC-shareScreen-icon"
+                           class="notification-anchor-icon chat-toolbarbutton screen-icon"
+                           oncommand="document.getBindingParent(this).showNotifications(this); event.stopPropagation();"/>
+        <xul:toolbarbutton anonid="webRTC-sharingScreen-icon"
+                           class="notification-anchor-icon chat-toolbarbutton screen-icon in-use"
+                           oncommand="document.getBindingParent(this).showNotifications(this); event.stopPropagation();"/>
+        <xul:toolbarbutton anonid="notification-icon" class="notification-anchor-icon chat-toolbarbutton"
+                           oncommand="document.getBindingParent(this).showNotifications(this); event.stopPropagation();"/>
+        <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:browser anonid="remote-content" class="chat-frame" flex="1"
+                   context="contentAreaContextMenu"
+                   disableglobalhistory="true"
+                   frameType="social"
+                   message="true"
+                   messagemanagergroup="social"
+                   tooltip="aHTMLTooltip"
+                   remote="true"
+                   xbl:inherits="src,origin"
+                   type="content"/>
+
+      <xul:browser anonid="content" class="chat-frame" flex="1"
+                   context="contentAreaContextMenu"
+                   disableglobalhistory="true"
+                   message="true"
+                   messagemanagergroup="social"
+                   tooltip="aHTMLTooltip"
+                   xbl:inherits="src,origin"
+                   type="content"/>
+    </content>
+
+    <implementation implements="nsIDOMEventListener, nsIMessageListener">
+      <constructor><![CDATA[
+        const kAnchorMap = new Map([
+          ["", "notification-"],
+          ["webRTC-shareScreen-", ""],
+          ["webRTC-sharingScreen-", ""]
+        ]);
+        const kBrowsers = [
+          document.getAnonymousElementByAttribute(this, "anonid", "content"),
+          document.getAnonymousElementByAttribute(this, "anonid", "remote-content")
+        ];
+        for (let content of kBrowsers) {
+          for (let [getterPrefix, idPrefix] of kAnchorMap) {
+            let getter = getterPrefix + "popupnotificationanchor";
+            let anonid = (idPrefix || getterPrefix) + "icon";
+            content.__defineGetter__(getter, () => {
+              delete content[getter];
+              return content[getter] = document.getAnonymousElementByAttribute(
+                this, "anonid", anonid);
+            });
+          }
+        }
+
+        let mm = this.content.messageManager;
+        // 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;
+        }
+
+        mm.addMessageListener("Social:DOMTitleChanged", this);
+
+        mm.sendAsyncMessage("WaitForDOMContentLoaded");
+        mm.addMessageListener("DOMContentLoaded", function DOMContentLoaded(event) {
+          mm.removeMessageListener("DOMContentLoaded", DOMContentLoaded);
+          this.isActive = !this.minimized;
+          this._chat.loadButtonSet(this, this.getAttribute("buttonSet"));
+          this._deferredChatLoaded.resolve(this);
+        }.bind(this));
+
+        this.setActiveBrowser();
+      ]]></constructor>
+
+      <field name="_deferredChatLoaded" readonly="true">
+        Promise.defer();
+      </field>
+
+      <property name="promiseChatLoaded">
+        <getter>
+          return this._deferredChatLoaded.promise;
+        </getter>
+      </property>
+
+      <property name="content">
+        <getter>
+          return document.getAnonymousElementByAttribute(this, "anonid",
+            (this.remote ? "remote-" : "") + "content");
+        </getter>
+      </property>
+
+      <field name="_chat" readonly="true">
+        Cu.import("resource:///modules/Chat.jsm", {}).Chat;
+      </field>
+
+      <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.chatbar;
+          if (val) {
+            this.setAttribute("minimized", "true");
+            // If this chat is the selected one a new one needs to be selected.
+            if (parent && parent.selectedChat == this)
+              parent._selectAnotherChat();
+          } else {
+            this.removeAttribute("minimized");
+            // this chat gets selected.
+            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.content.docShellIsActive;
+        </getter>
+        <setter>
+          this.content.docShellIsActive = !!val;
+
+          // Bug 1256431 to remove socialFrameShow/Hide from hello, keep this
+          // until that is complete.
+          // let the chat frame know if it is being shown or hidden
+          this.content.messageManager.sendAsyncMessage("Social:CustomEvent", {
+            name: val ? "socialFrameShow" : "socialFrameHide"
+          });
+        </setter>
+      </property>
+
+      <field name="_remote">false</field>
+      <property name="remote" onget="return this._remote;">
+        <setter><![CDATA[
+          this._remote = !!val;
+
+          this.setActiveBrowser();
+        ]]></setter>
+      </property>
+
+      <method name="setActiveBrowser">
+        <body><![CDATA[
+          // Make sure we only show one browser element at a time.
+          let content = document.getAnonymousElementByAttribute(this, "anonid", "content");
+          let remoteContent = document.getAnonymousElementByAttribute(this, "anonid", "remote-content");
+          remoteContent.setAttribute("hidden", !this.remote);
+          content.setAttribute("hidden", this.remote);
+          remoteContent.removeAttribute("src");
+          content.removeAttribute("src");
+
+          if (this.src) {
+            this.setAttribute("src", this.src);
+
+            // Stop loading of the document - that is set before this method was
+            // called - in the now hidden browser.
+            (this.remote ? content : remoteContent).setAttribute("src", "about:blank");
+          }
+        ]]></body>
+      </method>
+
+      <method name="showNotifications">
+        <parameter name="aAnchor"/>
+        <body><![CDATA[
+        PopupNotifications._reshowNotifications(aAnchor,
+                                                this.content);
+        ]]></body>
+      </method>
+
+      <method name="swapDocShells">
+        <parameter name="aTarget"/>
+        <body><![CDATA[
+          aTarget.setAttribute("label", this.content.contentTitle);
+
+          aTarget.remote = this.remote;
+          aTarget.src = this.src;
+          let content = aTarget.content;
+          content.setAttribute("origin", this.content.getAttribute("origin"));
+          content.popupnotificationanchor.className = this.content.popupnotificationanchor.className;
+          content.swapDocShells(this.content);
+
+          // When a chat window is attached or detached, the docShell hosting
+          // the chat document is swapped to the newly created chat window.
+          // (Be it inside a popup or back inside a chatbox element attached to
+          // the chatbar.)
+          // Since a swapDocShells call does not swap the messageManager instances
+          // attached to a browser, we'll need to add the message listeners to
+          // the new messageManager. This is not a bug in swapDocShells, merely
+          // a design decision.
+          content.messageManager.addMessageListener("Social:DOMTitleChanged", content);
+        ]]></body>
+      </method>
+
+      <method name="setDecorationAttributes">
+        <parameter name="aTarget"/>
+        <body><![CDATA[
+          if (this.hasAttribute("customSize"))
+            aTarget.setAttribute("customSize", this.getAttribute("customSize"));
+          this._chat.loadButtonSet(aTarget, this.getAttribute("buttonSet"));
+        ]]></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.chatbar.focus();
+          } else if (aEvent.button == 1) // middle-click: close chat
+            this.close();
+        ]]></body>
+      </method>
+
+      <method name="close">
+        <body><![CDATA[
+        if (this.chatbar)
+          this.chatbar.remove(this);
+        else
+          window.close();
+
+        if (!this.swappingWindows)
+          this.dispatchEvent(new CustomEvent("ChatboxClosed"));
+        ]]></body>
+      </method>
+
+      <method name="swapWindows">
+        <body><![CDATA[
+        let deferred = Promise.defer();
+        let title = this.getAttribute("label");
+        if (this.chatbar) {
+          this.chatbar.detachChatbox(this, { "centerscreen": "yes" }).then(
+            chatbox => {
+              chatbox.content.messageManager.sendAsyncMessage("Social:SetDocumentTitle", {
+                title: title
+              });
+              deferred.resolve(chatbox);
+            }
+          );
+        } else {
+          // attach this chatbox to the topmost browser window
+          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: origin,
+            title: title,
+            url: "about:blank"
+          });
+
+          cb.promiseChatLoaded.then(
+            () => {
+              this.setDecorationAttributes(cb);
+
+              this.swapDocShells(cb);
+
+              chatbar.focus();
+              this.swappingWindows = true;
+              this.close();
+
+              // 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));
+
+              cb.content.messageManager.sendAsyncMessage("Social:CustomEvent", {
+                name: "socialFrameAttached"
+              });
+
+              deferred.resolve(cb);
+            }
+          );
+        }
+        return deferred.promise;
+        ]]></body>
+      </method>
+
+      <method name="toggle">
+        <body><![CDATA[
+          this.minimized = !this.minimized;
+        ]]></body>
+      </method>
+
+      <method name="setTitle">
+        <body><![CDATA[
+          try {
+            this.setAttribute("label", this.content.contentTitle);
+          } catch (ex) {}
+          if (this.chatbar)
+            this.chatbar.updateTitlebar(this);
+        ]]></body>
+      </method>
+
+      <method name="receiveMessage">
+        <parameter name="aMessage" />
+        <body><![CDATA[
+          switch (aMessage.name) {
+            case "Social:DOMTitleChanged":
+              this.setTitle();
+              break;
+          }
+        ]]></body>
+      </method>
+    </implementation>
+
+    <handlers>
+      <handler event="focus" phase="capturing">
+        if (this.chatbar)
+          this.chatbar.selectedChat = this;
+      </handler>
+      <handler event="DOMTitleChanged">
+        this.setTitle();
+      </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;
+        if (link.rel.indexOf("icon") < 0)
+          return;
+
+        let ContentLinkHandler = Cu.import("resource:///modules/ContentLinkHandler.jsm", {})
+          .ContentLinkHandler;
+        let uri = ContentLinkHandler.getLinkIconURI(link);
+        if (!uri)
+          return;
+
+        // We made it this far, use it.
+        this.setAttribute("image", uri.spec);
+        if (this.chatbar)
+          this.chatbar.updateTitlebar(this);
+      ]]></handler>
+      <handler event="transitionend">
+        if (this.isActive == this.minimized)
+          this.isActive = !this.minimized;
+      </handler>
+    </handlers>
+  </binding>
+
+  <binding id="chatbar">
+    <content>
+      <xul:hbox align="end" pack="end" anonid="innerbox" class="chatbar-innerbox" mousethrough="always" flex="1">
+        <xul:spacer flex="1" anonid="spacer" class="chatbar-overflow-spacer"/>
+        <xul:toolbarbutton anonid="nub" class="chatbar-button" type="menu" collapsed="true" mousethrough="never">
+          <xul:menupopup anonid="nubMenu" oncommand="document.getBindingParent(this).showChat(event.target.chat)"/>
+        </xul:toolbarbutton>
+        <children/>
+      </xul:hbox>
+    </content>
+
+    <implementation implements="nsIDOMEventListener">
+      <constructor>
+        // to avoid reflows we cache the width of the nub.
+        this.cachedWidthNub = 0;
+        this._selectedChat = null;
+      </constructor>
+
+      <field name="innerbox" readonly="true">
+        document.getAnonymousElementByAttribute(this, "anonid", "innerbox");
+      </field>
+
+      <field name="menupopup" readonly="true">
+        document.getAnonymousElementByAttribute(this, "anonid", "nubMenu");
+      </field>
+
+      <field name="nub" readonly="true">
+        document.getAnonymousElementByAttribute(this, "anonid", "nub");
+      </field>
+
+      <method name="focus">
+        <body><![CDATA[
+          if (!this.selectedChat)
+            return;
+          this.selectedChat.content.messageManager.sendAsyncMessage("Social:EnsureFocus");
+        ]]></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.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 browser...
+          let containingBrowser = fw.QueryInterface(Ci.nsIInterfaceRequestor)
+                                    .getInterface(Ci.nsIWebNavigation)
+                                    .QueryInterface(Ci.nsIDocShell)
+                                    .chromeEventHandler;
+          return containingBrowser == aChatbox.content;
+        ]]></body>
+      </method>
+
+      <property name="selectedChat">
+        <getter><![CDATA[
+          return this._selectedChat;
+        ]]></getter>
+        <setter><![CDATA[
+          // this is pretty horrible, but we:
+          // * want to avoid doing touching 'selected' attribute when the
+          //   specified chat is already selected.
+          // * remove 'activity' attribute on newly selected tab *even if*
+          //   newly selected is already selected.
+          // * need to handle either current or new being null.
+          if (this._selectedChat != val) {
+            if (this._selectedChat) {
+              this._selectedChat.removeAttribute("selected");
+            }
+            this._selectedChat = val;
+            if (val) {
+              this._selectedChat.setAttribute("selected", "true");
+            }
+          }
+          if (val) {
+            this._selectedChat.removeAttribute("activity");
+          }
+        ]]></setter>
+      </property>
+
+      <field name="menuitemMap">new WeakMap()</field>
+      <field name="chatboxForURL">new Map();</field>
+
+      <property name="hasCollapsedChildren">
+        <getter><![CDATA[
+          return !!this.querySelector("[collapsed]");
+        ]]></getter>
+      </property>
+
+      <property name="collapsedChildren">
+        <getter><![CDATA[
+          // A generator yielding all collapsed chatboxes, in the order in
+          // which they should be restored.
+          return function*() {
+            let child = this.lastElementChild;
+            while (child) {
+              if (child.collapsed)
+                yield child;
+              child = child.previousElementSibling;
+            }
+          }
+        ]]></getter>
+      </property>
+
+      <property name="visibleChildren">
+        <getter><![CDATA[
+          // A generator yielding all non-collapsed chatboxes.
+          return function*() {
+            let child = this.firstElementChild;
+            while (child) {
+              if (!child.collapsed)
+                yield child;
+              child = child.nextElementSibling;
+            }
+          }
+        ]]></getter>
+      </property>
+
+      <property name="collapsibleChildren">
+        <getter><![CDATA[
+          // A generator yielding all children which are able to be collapsed
+          // in the order in which they should be collapsed.
+          // (currently this is all visible ones other than the selected one.)
+          return function*() {
+            for (let child of this.visibleChildren())
+              if (child != this.selectedChat)
+                yield child;
+          }
+        ]]></getter>
+      </property>
+
+      <method name="_selectAnotherChat">
+        <body><![CDATA[
+          // Select a different chat (as the currently selected one is no
+          // longer suitable as the selection - maybe it is being minimized or
+          // closed.)  We only select non-minimized and non-collapsed chats,
+          // and if none are found, set the selectedChat to null.
+          // It's possible in the future we will track most-recently-selected
+          // chats or similar to find the "best" candidate - for now though
+          // the choice is somewhat arbitrary.
+          let moveFocus = this.selectedChat && this._isChatFocused(this.selectedChat);
+          for (let other of this.children) {
+            if (other != this.selectedChat && !other.minimized && !other.collapsed) {
+              this.selectedChat = other;
+              if (moveFocus)
+                this.focus();
+              return;
+            }
+          }
+          // can't find another - so set no chat as selected.
+          this.selectedChat = null;
+        ]]></body>
+      </method>
+
+      <method name="updateTitlebar">
+        <parameter name="aChatbox"/>
+        <body><![CDATA[
+          if (aChatbox.collapsed) {
+            let menuitem = this.menuitemMap.get(aChatbox);
+            if (aChatbox.getAttribute("activity")) {
+              menuitem.setAttribute("activity", true);
+              this.nub.setAttribute("activity", true);
+            }
+            menuitem.setAttribute("label", aChatbox.getAttribute("label"));
+            menuitem.setAttribute("image", aChatbox.getAttribute("image"));
+          }
+        ]]></body>
+      </method>
+
+      <method name="calcTotalWidthOf">
+        <parameter name="aElement"/>
+        <body><![CDATA[
+          let cs = document.defaultView.getComputedStyle(aElement);
+          let margins = parseInt(cs.marginLeft) + parseInt(cs.marginRight);
+          return aElement.getBoundingClientRect().width + margins;
+        ]]></body>
+      </method>
+
+      <method name="getTotalChildWidth">
+        <parameter name="aChatbox"/>
+        <body><![CDATA[
+          // These are from the CSS for the chatbox and must be kept in sync.
+          // We can't use calcTotalWidthOf due to the transitions...
+          const CHAT_WIDTH_OPEN = 300;
+          const CHAT_WIDTH_OPEN_ALT = 350;
+          const CHAT_WIDTH_MINIMIZED = 160;
+          let openWidth = aChatbox.hasAttribute("customSize") ?
+            CHAT_WIDTH_OPEN_ALT : CHAT_WIDTH_OPEN;
+
+          return aChatbox.minimized ? CHAT_WIDTH_MINIMIZED : openWidth;
+        ]]></body>
+      </method>
+
+      <method name="collapseChat">
+        <parameter name="aChatbox"/>
+        <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.content.contentTitle);
+          menu.setAttribute("image", aChatbox.getAttribute("image"));
+          menu.chat = aChatbox;
+          this.menuitemMap.set(aChatbox, menu);
+          this.menupopup.appendChild(menu);
+          this.nub.collapsed = false;
+        ]]></body>
+      </method>
+
+      <method name="showChat">
+        <parameter name="aChatbox"/>
+        <parameter name="aMode"/>
+        <body><![CDATA[
+          if ((aMode != "minimized") && aChatbox.minimized)
+            aChatbox.minimized = false;
+          if (this.selectedChat != aChatbox)
+            this.selectedChat = aChatbox;
+          if (!aChatbox.collapsed)
+            return; // already showing - no more to do.
+          this._showChat(aChatbox);
+          // showing a collapsed chat might mean another needs to be collapsed
+          // to make room...
+          this.resize();
+        ]]></body>
+      </method>
+
+      <method name="_showChat">
+        <parameter name="aChatbox"/>
+        <body><![CDATA[
+          // the actual implementation - doesn't check for overflow, assumes
+          // collapsed, etc.
+          let menuitem = this.menuitemMap.get(aChatbox);
+          this.menuitemMap.delete(aChatbox);
+          this.menupopup.removeChild(menuitem);
+          aChatbox.collapsed = false;
+          aChatbox.isActive = !aChatbox.minimized;
+        ]]></body>
+      </method>
+
+      <method name="remove">
+        <parameter name="aChatbox"/>
+        <body><![CDATA[
+          this._remove(aChatbox);
+          // The removal of a chat may mean a collapsed one can spring up,
+          // or that the popup should be hidden.  We also defer the selection
+          // of another chat until after a resize, as a new candidate may
+          // become uncollapsed after the resize.
+          this.resize();
+          if (this.selectedChat == aChatbox) {
+            this._selectAnotherChat();
+          }
+        ]]></body>
+      </method>
+
+      <method name="_remove">
+        <parameter name="aChatbox"/>
+        <body><![CDATA[
+          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>
+
+      <method name="openChat">
+        <parameter name="aOptions"/>
+        <parameter name="aCallback"/>
+        <body><![CDATA[
+          let {origin, title, url, mode} = aOptions;
+          let cb = this.chatboxForURL.get(url);
+          if (cb && (cb = cb.get())) {
+            // A chatbox is still alive to us when it's parented and still has
+            // content.
+            if (cb.parentNode) {
+              this.showChat(cb, mode);
+              if (aCallback) {
+                if (cb._callbacks == null) {
+                  // Chatbox has already been created, so callback now.
+                  aCallback(cb);
+                } else {
+                  // Chatbox is yet to have bindings created...
+                  cb._callbacks.push(aCallback);
+                }
+              }
+              return cb;
+            }
+            this.chatboxForURL.delete(url);
+          }
+          cb = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "chatbox");
+          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);
+          }
+
+          cb.remote = !!aOptions.remote;
+          // src also a javascript property; the src attribute is set in the ctor.
+          cb.src = url;
+          if (mode == "minimized")
+            cb.setAttribute("minimized", "true");
+          cb.setAttribute("origin", origin);
+          cb.setAttribute("label", title);
+          this.insertBefore(cb, this.firstChild);
+          this.selectedChat = cb;
+          this.chatboxForURL.set(url, 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.
+        // So 2 basic strategies:
+        // * Collapse/Expand one at a time until we can't collapse/expand any
+        //   more - but this is one reflow per change.
+        // * Calculate the dimensions ourself and choose how many to collapse
+        //   or expand based on this, then do them all in one go.  This is one
+        //   reflow regardless of how many we change.
+        // So we go the more complicated but more efficient second option...
+        let availWidth = this.getBoundingClientRect().width;
+        let currentWidth = 0;
+        if (!this.nub.collapsed) { // the nub is visible.
+          if (!this.cachedWidthNub)
+            this.cachedWidthNub = this.calcTotalWidthOf(this.nub);
+          currentWidth += this.cachedWidthNub;
+        }
+        for (let child of this.visibleChildren()) {
+          currentWidth += this.getTotalChildWidth(child);
+        }
+
+        if (currentWidth > availWidth) {
+          // we need to collapse some.
+          let toCollapse = [];
+          for (let child of this.collapsibleChildren()) {
+            if (currentWidth <= availWidth)
+              break;
+            toCollapse.push(child);
+            currentWidth -= this.getTotalChildWidth(child);
+          }
+          if (toCollapse.length) {
+            for (let child of toCollapse)
+              this.collapseChat(child);
+          }
+        } else if (currentWidth < availWidth) {
+          // we *might* be able to expand some - see how many.
+          // XXX - if this was clever, it could know when removing the nub
+          // leaves enough space to show all collapsed
+          let toShow = [];
+          for (let child of this.collapsedChildren()) {
+            currentWidth += this.getTotalChildWidth(child);
+            if (currentWidth > availWidth)
+              break;
+            toShow.push(child);
+          }
+          for (let child of toShow)
+            this._showChat(child);
+
+          // If none remain collapsed remove the nub.
+          if (!this.hasCollapsedChildren) {
+            this.nub.collapsed = true;
+          }
+        }
+        // else: achievement unlocked - we are pixel-perfect!
+        ]]></body>
+      </method>
+
+      <method name="handleEvent">
+        <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. Returns a promise that is resolved
+           once the move to the other window is complete.
+      -->
+      <method name="detachChatbox">
+        <parameter name="aChatbox"/>
+        <parameter name="aOptions"/>
+        <body><![CDATA[
+          let deferred = Promise.defer();
+          let chatbar = this;
+          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;
+
+            if (aChatbox.hasAttribute("customSize")) {
+              otherWin.document.getElementById("chat-window").
+                setAttribute("customSize", aChatbox.getAttribute("customSize"));
+            }
+
+            otherWin.removeEventListener("load", _chatLoad, true);
+            let otherChatbox = otherWin.document.getElementById("chatter");
+            aChatbox.setDecorationAttributes(otherChatbox);
+            aChatbox.swapDocShells(otherChatbox);
+
+            aChatbox.swappingWindows = true;
+            aChatbox.close();
+            let url = aChatbox.src;
+            chatbar.chatboxForURL.set(url, Cu.getWeakReference(otherChatbox));
+
+            // All processing is done, now we can fire the event.
+            otherChatbox.content.messageManager.sendAsyncMessage("Social:CustomEvent", {
+              name: "socialFrameDetached"
+            });
+
+            Services.obs.addObserver(function onDOMWindowClosed(subject) {
+              if (subject !== otherWin)
+                return;
+
+              Services.obs.removeObserver(onDOMWindowClosed, "domwindowclosed");
+              chatbar.chatboxForURL.delete(url);
+
+              if (!otherChatbox.swappingWindows)
+                otherChatbox.dispatchEvent(new CustomEvent("ChatboxClosed"));
+            }, "domwindowclosed", false);
+
+            deferred.resolve(otherChatbox);
+          }, true);
+          return deferred.promise;
+        ]]></body>
+      </method>
+
+    </implementation>
+
+    <handlers>
+      <handler event="popupshown"><![CDATA[
+        this.nub.removeAttribute("activity");
+      ]]></handler>
+      <handler event="load"><![CDATA[
+        window.addEventListener("resize", this, true);
+      ]]></handler>
+      <handler event="unload"><![CDATA[
+        window.removeEventListener("resize", this, true);
+      ]]></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, 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);
+
+        this.detachChatbox(draggedChat, { screenX: left, screenY: top });
+        event.stopPropagation();
+      ]]></handler>
+    </handlers>
+  </binding>
+
+</bindings>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/socialmarks.xml
@@ -0,0 +1,366 @@
+<?xml version="1.0"?>
+
+<bindings id="socialMarkBindings"
+    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="toolbarbutton-marks" display="xul:button"
+           extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
+    <content>
+      <xul:panel anonid="panel" hidden="true" type="arrow" class="social-panel"/>
+      <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label"/>
+      <xul:label class="toolbarbutton-text" crop="right" flex="1"
+                 xbl:inherits="value=label,accesskey,crop,wrap"/>
+      <xul:label class="toolbarbutton-multiline-text" flex="1"
+                 xbl:inherits="xbl:text=label,accesskey,wrap"/>
+    </content>
+    <implementation implements="nsIDOMEventListener, nsIObserver">
+      <constructor>
+        // if we overflow, we have to reset the button. unfortunately we cannot
+        // use a widget listener because we need to do this *after* the node is
+        // moved, and the event happens before the node is moved.
+        this.update();
+      </constructor>
+      <property name="_anchor">
+        <getter>
+          let widgetGroup = CustomizableUI.getWidget(this.getAttribute("id"));
+          return widgetGroup.forWindow(window).anchor;
+        </getter>
+      </property>
+      <property name="_useDynamicResizer">
+        <getter>
+          let provider = Social._getProviderFromOrigin(this.getAttribute("origin"));
+          return !provider.getPageSize("marks");
+        </getter>
+      </property>
+
+      <property name="panel">
+        <getter>
+          return document.getAnonymousElementByAttribute(this, "anonid", "panel");
+        </getter>
+      </property>
+
+      <property name="content">
+        <getter><![CDATA[
+          if (this._frame)
+            return this._frame;
+          let provider = Social._getProviderFromOrigin(this.getAttribute("origin"));
+          let size = provider.getPageSize("marks");
+          let {width, height} = size ? size : {width: 330, height: 100};
+
+          let iframe = this._frame = document.createElement("iframe");
+          iframe.setAttribute("type", "content");
+          iframe.setAttribute("class", "social-panel-frame");
+          iframe.setAttribute("flex", "1");
+          iframe.setAttribute("message", "true");
+          iframe.setAttribute("messagemanagergroup", "social");
+          iframe.setAttribute("tooltip", "aHTMLTooltip");
+          iframe.setAttribute("context", "contentAreaContextMenu");
+          iframe.setAttribute("origin", provider.origin);
+          iframe.setAttribute("style", "width: " + width + "px; height: " + height + "px;");
+          this.panel.appendChild(iframe);
+
+          this._frame.addEventListener("DOMLinkAdded", this);
+          return this._frame;
+        ]]></getter>
+      </property>
+
+      <property name="messageManager">
+        <getter>
+          return this.content.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader.messageManager;
+        </getter>
+      </property>
+
+      <property name="contentWindow">
+        <getter>
+          return this.content.contentWindow;
+        </getter>
+      </property>
+
+      <property name="contentDocument">
+        <getter>
+          return this.content.contentDocument;
+        </getter>
+      </property>
+
+      <property name="provider">
+        <getter>
+          return Social._getProviderFromOrigin(this.getAttribute("origin"));
+        </getter>
+      </property>
+
+      <property name="isMarked">
+        <setter><![CDATA[
+          this._isMarked = val;
+          let provider = this.provider;
+          // we cannot size the image when we apply it via listStyleImage, so
+          // use the toolbar image
+          let widgetGroup = CustomizableUI.getWidget(this.getAttribute("id"));
+          val = val && !!widgetGroup.areaType;
+          let icon = val ? provider.markedIcon : provider.unmarkedIcon;
+          let iconURL = icon || provider.icon32URL || provider.iconURL;
+          this.setAttribute("image", iconURL);
+        ]]></setter>
+        <getter>
+          return this._isMarked;
+        </getter>
+      </property>
+
+      <method name="update">
+        <body><![CDATA[
+        // update the button for use with the current tab
+        let provider = this.provider;
+        if (this._dynamicResizer) {
+          this._dynamicResizer.stop();
+          this._dynamicResizer = null;
+        }
+        this.content.setAttribute("src", "about:blank");
+        // called during onhidden, make sure the docshell is updated
+        if (this._frame.docShell)
+          this._frame.docShell.createAboutBlankContentViewer(null);
+
+        // disabled attr is set by Social:PageShareOrMark command
+        if (this.hasAttribute("disabled")) {
+          this.isMarked = false;
+        } else {
+          Social.isURIMarked(provider.origin, gBrowser.currentURI, (isMarked) => {
+            this.isMarked = isMarked;
+          });
+        }
+
+        this.content.setAttribute("origin", provider.origin);
+
+        let panel = this.panel;
+        // if customization is currently happening, we may not have a panel
+        // that we can hide
+        if (panel.hidePopup) {
+          panel.hidePopup();
+        }
+        this.pageData = null;
+        ]]></body>
+      </method>
+
+      <method name="receiveMessage">
+        <parameter name="message"/>
+        <body><![CDATA[
+        if (message.name != "Social:ErrorPageNotify" || message.target != this.content)
+          return;
+        this.openPanel();
+        ]]></body>
+      </method>
+
+      <method name="loadPanel">
+        <parameter name="pageData"/>
+        <parameter name="target"/>
+        <body><![CDATA[
+        let provider = this.provider;
+        let panel = this.panel;
+        panel.hidden = false;
+
+        // reparent the iframe if we've been customized to a new location
+        if (this.content.parentNode != panel)
+          panel.appendChild(this.content);
+
+        let URLTemplate = provider.markURL;
+        let _dataFn;
+        if (!pageData) {
+          messageManager.addMessageListener("PageMetadata:PageDataResult", _dataFn = (msg) => {
+            messageManager.removeMessageListener("PageMetadata:PageDataResult", _dataFn);
+            this.loadPanel(msg.json, target);
+          });
+          gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetPageData", null, { target });
+          return;
+        }
+        // if this is a share of a selected item, get any microformats
+        if (!pageData.microformats && target) {
+          messageManager.addMessageListener("PageMetadata:MicroformatsResult", _dataFn = (msg) => {
+            messageManager.removeMessageListener("PageMetadata:MicroformatsResult", _dataFn);
+            pageData.microformats = msg.data;
+            this.loadPanel(pageData, target);
+          });
+          gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetMicroformats", null, { target });
+          return;
+        }
+        this.pageData = pageData;
+
+        let endpoint = OpenGraphBuilder.generateEndpointURL(URLTemplate, this.pageData);
+        // setup listeners
+        let DOMContentLoaded = (event) => {
+          this._loading = false;
+          this.messageManager.removeMessageListener("DOMContentLoaded", DOMContentLoaded);
+          // add our resizer after the dom is ready
+          if (this._useDynamicResizer) {
+            let DynamicResizeWatcher = Cu.import("resource:///modules/Social.jsm", {}).DynamicResizeWatcher;
+            this._dynamicResizer = new DynamicResizeWatcher();
+            this._dynamicResizer.start(this.panel, this.content);
+          } else if (this._dynamicResizer) {
+            this._dynamicResizer.stop();
+            this._dynamicResizer = null;
+          }
+
+          let contentWindow = this.contentWindow;
+          let markUpdate = function(event) {
+            // update the annotation based on this event, then update the
+            // icon as well
+            this.isMarked = JSON.parse(event.detail).marked;
+            if (this.isMarked) {
+              Social.markURI(provider.origin, gBrowser.currentURI);
+            } else {
+              Social.unmarkURI(provider.origin, gBrowser.currentURI, () => {
+                this.update();
+              });
+            }
+          }.bind(this);
+          let unload = () => {
+            contentWindow.removeEventListener("unload", unload);
+            contentWindow.removeEventListener("socialMarkUpdate", markUpdate);
+          }
+          contentWindow.addEventListener("socialMarkUpdate", markUpdate);
+          contentWindow.addEventListener("unload", unload);
+
+          // send the opengraph data
+          this.messageManager.sendAsyncMessage("Social:OpenGraphData", pageData);
+        }
+        this.messageManager.addMessageListener("DOMContentLoaded", DOMContentLoaded);
+        this._loading = true;
+        this.content.setAttribute("src", endpoint);
+        ]]></body>
+      </method>
+
+      <method name="openPanel">
+        <parameter name="aResetOnClose"/>
+        <body><![CDATA[
+        let panel = this.panel;
+        let anchor = document.getAnonymousElementByAttribute(this._anchor, "class", "toolbarbutton-icon");
+        // Bug 849216 - open the popup in a setTimeout so we avoid the auto-rollup
+        // handling from preventing it being opened in some cases.
+        setTimeout(() => {
+          panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
+        }, 0);
+        Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(2);
+        ]]></body>
+       </method>
+
+      <method name="markCurrentPage">
+        <parameter name="aOpenPanel"/>
+        <body><![CDATA[
+        // we always set the src on click if it has not been set for this tab,
+        // but we only want to open the panel if it was previously annotated.
+        let openPanel = this.isMarked || aOpenPanel;
+        let src = this.content.getAttribute("src");
+        if (!src || src == "about:blank") {
+          this.loadPanel();
+        }
+        if (openPanel)
+          this.openPanel();
+        ]]></body>
+      </method>
+
+      <method name="markLink">
+        <parameter name="aUrl"/>
+        <parameter name="aTarget"/>
+        <body><![CDATA[
+        if (!aUrl) {
+          this.markCurrentPage(true);
+          return;
+        }
+        // initiated form an external source, such as gContextMenu, where
+        // pageData is passed into us. In this case, we always load the iframe
+        // and show it since the url may not be the browser tab, but an image,
+        // link, etc. inside the page. We also "update" the iframe to the
+        // previous url when it is closed.
+        this.content.setAttribute("src", "about:blank");
+        this.loadPanel({ url: aUrl }, aTarget);
+        this.openPanel(true);
+        ]]></body>
+      </method>
+
+      <method name="dispatchPanelEvent">
+        <parameter name="name"/>
+        <body><![CDATA[
+        let evt = this.contentDocument.createEvent("CustomEvent");
+        evt.initCustomEvent(name, true, true, {});
+        this.contentDocument.documentElement.dispatchEvent(evt);
+        ]]></body>
+      </method>
+
+      <method name="onShown">
+        <body><![CDATA[
+        // because the panel may be preloaded, we need to size the panel when
+        // showing as well as after load
+        if (!this._useDynamicResizer) {
+          return;
+        }
+        let sizeSocialPanelToContent = Cu.import("resource:///modules/Social.jsm", {}).sizeSocialPanelToContent;
+        if (!this._loading && this.contentDocument &&
+            this.contentDocument.readyState == "complete") {
+          sizeSocialPanelToContent(this.panel, this.content);
+        } else {
+          let panelBrowserOnload = (message) => {
+            if (message.target != this.content)
+              return;
+            this.messageManager.removeMessageListener("PageVisibility:Show", panelBrowserOnload, true);
+            sizeSocialPanelToContent(this.panel, this.content);
+          };
+          this.messageManager.addMessageListener("PageVisibility:Show", panelBrowserOnload);
+        }
+        ]]></body>
+      </method>
+
+      <method name="handleEvent">
+        <parameter name="aEvent"/>
+        <body><![CDATA[
+        if (aEvent.eventPhase != aEvent.BUBBLING_PHASE)
+          return;
+        switch(aEvent.type) {
+          case "DOMLinkAdded": {
+            // 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 = aEvent.originalTarget;
+            let rel = link.rel && link.rel.toLowerCase();
+            if (!link || !link.ownerDocument || !rel || !link.href)
+              return;
+            if (link.rel.indexOf("icon") < 0)
+              return;
+
+            let ContentLinkHandler = Cu.import("resource:///modules/ContentLinkHandler.jsm", {}).ContentLinkHandler;
+            let uri = ContentLinkHandler.getLinkIconURI(link);
+            if (!uri)
+              return;
+
+            // we cannot size the image when we apply it via listStyleImage, so
+            // use the toolbar image
+            this.setAttribute("image", uri.spec);
+            }
+            break;
+          case "click":
+            Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(2);
+            break;
+        }
+        ]]></body>
+      </method>
+
+    </implementation>
+    <handlers>
+      <handler event="popupshowing"><![CDATA[
+        this._anchor.setAttribute("open", "true");
+        this.content.addEventListener("click", this);
+      ]]></handler>
+      <handler event="popupshown"><![CDATA[
+        this.onShown();
+      ]]></handler>
+      <handler event="popuphidden"><![CDATA[
+        this._anchor.removeAttribute("open");
+        this.update();
+        this.content.removeEventListener("click", this);
+      ]]></handler>
+      <handler event="command"><![CDATA[
+        this.markCurrentPage();
+      ]]></handler>
+    </handlers>
+  </binding>
+
+</bindings>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/chat/.eslintrc
@@ -0,0 +1,5 @@
+{
+  "extends": [
+    "../../../../../testing/mochitest/browser.eslintrc"
+  ]
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/chat/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+skip-if = buildapp == 'mulet'
+support-files =
+  head.js
+  chat.html
+
+[browser_chatwindow.js]
+[browser_focus.js]
+[browser_tearoff.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/chat/browser_chatwindow.js
@@ -0,0 +1,197 @@
+/* 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/. */
+
+requestLongerTimeout(2);
+
+var chatbar = document.getElementById("pinnedchats");
+
+add_chat_task(function* testOpenCloseChat() {
+  let chatbox = yield promiseOpenChat("http://example.com");
+  Assert.strictEqual(chatbox, chatbar.selectedChat);
+  // we requested a "normal" chat, so shouldn't be minimized
+  Assert.ok(!chatbox.minimized, "chat is not minimized");
+  Assert.equal(chatbar.childNodes.length, 1, "should be 1 chat open");
+
+
+  // now request the same URL again - we should get the same chat.
+  let chatbox2 = yield promiseOpenChat("http://example.com");
+  Assert.strictEqual(chatbox2, chatbox, "got the same chat");
+  Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open");
+
+  chatbox.toggle();
+  is(chatbox.minimized, true, "chat is now minimized");
+  // was no other chat to select, so selected becomes null.
+  is(chatbar.selectedChat, null);
+
+  // We check the content gets an unload event as we close it.
+  chatbox.close();
+});
+
+// In this case we open a chat minimized, then request the same chat again
+// without specifying minimized.  On that second call the chat should open,
+// selected, and no longer minimized.
+add_chat_task(function* testMinimized() {
+  let chatbox = yield promiseOpenChat("http://example.com", "minimized");
+  Assert.strictEqual(chatbox, chatbar.selectedChat);
+  Assert.ok(chatbox.minimized, "chat is minimized");
+  Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open");
+  yield promiseOpenChat("http://example.com");
+  Assert.ok(!chatbox.minimized, false, "chat is no longer minimized");
+});
+
+// open enough chats to overflow the window, then check
+// if the menupopup is visible
+add_chat_task(function* testManyChats() {
+  Assert.ok(chatbar.menupopup.parentNode.collapsed, "popup nub collapsed at start");
+  // we should *never* find a test box that needs more than this to cause
+  // an overflow!
+  let maxToOpen = 20;
+  let numOpened = 0;
+  for (let i = 0; i < maxToOpen; i++) {
+    yield promiseOpenChat("http://example.com#" + i);
+    if (!chatbar.menupopup.parentNode.collapsed) {
+      info("the menu popup appeared");
+      return;
+    }
+  }
+  Assert.ok(false, "We didn't find a collapsed chat after " + maxToOpen + "chats!");
+});
+
+// Check that closeAll works as expected.
+add_chat_task(function* testOpenTwiceCallbacks() {
+  yield promiseOpenChat("http://example.com#1");
+  yield promiseOpenChat("http://example.com#2");
+  yield promiseOpenChat("http://test2.example.com");
+  Assert.equal(numChatsInWindow(window), 3, "should be 3 chats open");
+  Chat.closeAll("http://example.com");
+  Assert.equal(numChatsInWindow(window), 1, "should have closed 2 chats");
+  Chat.closeAll("http://test2.example.com");
+  Assert.equal(numChatsInWindow(window), 0, "should have closed last chat");
+});
+
+// Check that when we open the same chat twice, the callbacks are called back
+// twice.
+add_chat_task(function* testOpenTwiceCallbacks() {
+  yield promiseOpenChatCallback("http://example.com");
+  yield promiseOpenChatCallback("http://example.com");
+});
+
+// Bug 817782 - check chats work in new top-level windows.
+add_chat_task(function* testSecondTopLevelWindow() {
+  const chatUrl = "http://example.com";
+  let winPromise = BrowserTestUtils.waitForNewWindow();
+  OpenBrowserWindow();
+  let secondWindow = yield winPromise;
+  yield promiseOpenChat(chatUrl);
+  // the chat was created - let's make sure it was created in the second window.
+  Assert.equal(numChatsInWindow(window), 0, "main window has no chats");
+  Assert.equal(numChatsInWindow(secondWindow), 1, "second window has 1 chat");
+  secondWindow.close();
+});
+
+// Test that findChromeWindowForChats() returns the expected window.
+add_chat_task(function* testChatWindowChooser() {
+  let chat = yield promiseOpenChat("http://example.com");
+  Assert.equal(numChatsInWindow(window), 1, "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();
+  yield promiseOneEvent(secondWindow, "load");
+  Assert.equal(secondWindow, Chat.findChromeWindowForChats(null), "Second window is the preferred chat window");
+
+  // focus the first window, and check it will be selected for future chats.
+  // Bug 1090633 - there are lots of issues around focus, especially when the
+  // browser itself doesn't have focus. We can end up with
+  // Services.wm.getMostRecentWindow("navigator:browser") returning a different
+  // window than, say, Services.focus.activeWindow. But the focus manager isn't
+  // the point of this test.
+  // So we simply keep focusing the first window until it is reported as the
+  // most recent.
+  do {
+    dump("trying to force window to become the most recent.\n");
+    secondWindow.focus();
+    window.focus();
+    yield promiseWaitForFocus();
+  } while (Services.wm.getMostRecentWindow("navigator:browser") != window)
+
+  Assert.equal(window, Chat.findChromeWindowForChats(null), "First window now the preferred chat window");
+
+  let privateWindow = OpenBrowserWindow({private: true});
+  yield promiseOneEvent(privateWindow, "load")
+
+  // 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 will be
+  // either window or secondWindow (linux may choose a different one) but the
+  // point is that the window is *not* the private one.
+  Assert.ok(Chat.findChromeWindowForChats(null) == window ||
+            Chat.findChromeWindowForChats(null) == secondWindow,
+            "Private window isn't selected for new chats.");
+  privateWindow.close();
+  secondWindow.close();
+});
+
+add_chat_task(function* testButtonSet() {
+  let chatbox = yield promiseOpenChat("http://example.com#1");
+  let document = chatbox.ownerDocument;
+
+  // Expect all default buttons to be visible.
+  for (let buttonId of kDefaultButtonSet) {
+    let button = document.getAnonymousElementByAttribute(chatbox, "anonid", buttonId);
+    Assert.ok(!button.hidden, "Button '" + buttonId + "' should be visible");
+  }
+
+  let visible = new Set(["minimize", "close"]);
+  chatbox = yield promiseOpenChat("http://example.com#2", null, null, [...visible].join(","));
+
+  for (let buttonId of kDefaultButtonSet) {
+    let button = document.getAnonymousElementByAttribute(chatbox, "anonid", buttonId);
+    if (visible.has(buttonId)) {
+      Assert.ok(!button.hidden, "Button '" + buttonId + "' should be visible");
+    } else {
+      Assert.ok(button.hidden, "Button '" + buttonId + "' should NOT be visible");
+    }
+  }
+});
+
+add_chat_task(function* testCustomButton() {
+  let commanded = 0;
+  let customButton = {
+    id: "custom",
+    onCommand: function() {
+      ++commanded;
+    }
+  };
+
+  Chat.registerButton(customButton);
+
+  let chatbox = yield promiseOpenChat("http://example.com#1");
+  let document = chatbox.ownerDocument;
+  let titlebarNode = document.getAnonymousElementByAttribute(chatbox, "class",
+    "chat-titlebar");
+
+  Assert.equal(titlebarNode.getElementsByClassName("chat-custom")[0], null,
+    "Custom chat button should not be in the toolbar yet.");
+
+  let visible = new Set(["minimize", "close", "custom"]);
+  Chat.loadButtonSet(chatbox, [...visible].join(","));
+
+  for (let buttonId of kDefaultButtonSet) {
+    let button = document.getAnonymousElementByAttribute(chatbox, "anonid", buttonId);
+    if (visible.has(buttonId)) {
+      Assert.ok(!button.hidden, "Button '" + buttonId + "' should be visible");
+    } else {
+      Assert.ok(button.hidden, "Button '" + buttonId + "' should NOT be visible");
+    }
+  }
+
+  let customButtonNode = titlebarNode.getElementsByClassName("chat-custom")[0];
+  Assert.ok(!customButtonNode.hidden, "Custom button should be visible");
+
+  let ev = document.createEvent("XULCommandEvent");
+  ev.initCommandEvent("command", true, true, document.defaultView, 0, false,
+    false, false, false, null);
+  customButtonNode.dispatchEvent(ev);
+
+  Assert.equal(commanded, 1, "Button should have been commanded once");
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/chat/browser_focus.js
@@ -0,0 +1,262 @@
+/* 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/. */
+
+// Tests the focus functionality.
+
+Cu.import("resource://testing-common/ContentTask.jsm", this);
+const CHAT_URL = "https://example.com/browser/browser/base/content/test/chat/chat.html";
+
+requestLongerTimeout(2);
+
+// Is the currently opened tab focused?
+function isTabFocused() {
+  let tabb = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+  // focus sucks in tests - our window may have lost focus.
+  let elt = Services.focus.getFocusedElementForWindow(window, false, {});
+  return elt == tabb;
+}
+
+// Is the specified chat focused?
+function isChatFocused(chat) {
+  // focus sucks in tests - our window may have lost focus.
+  let elt = Services.focus.getFocusedElementForWindow(window, false, {});
+  return elt == chat.content;
+}
+
+var chatbar = document.getElementById("pinnedchats");
+
+function* setUp() {
+  // Note that (probably) due to bug 604289, if a tab is focused but the
+  // focused element is null, our chat windows can "steal" focus.  This is
+  // avoided if we explicitly focus an element in the tab.
+  // So we load a page with an <input> field and focus that before testing.
+  let html = '<input id="theinput"><button id="chat-opener"></button>';
+  let url = "data:text/html;charset=utf-8," + encodeURI(html);
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+  let browser = tab.linkedBrowser;
+  yield ContentTask.spawn(browser, null, function* () {
+    content.document.getElementById("theinput").focus();
+  });
+
+  registerCleanupFunction(function() {
+    gBrowser.removeTab(tab);
+  });
+}
+
+// Test default focus - not user input.
+add_chat_task(function* testDefaultFocus() {
+  yield setUp();
+  let chat = yield promiseOpenChat("http://example.com");
+  // we used the default focus behaviour, which means that because this was
+  // not the direct result of user action the chat should not be focused.
+  Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open");
+  Assert.ok(isTabFocused(), "the tab should remain focused.");
+  Assert.ok(!isChatFocused(chat), "the chat should not be focused.");
+});
+
+// Test default focus via user input.
+add_chat_task(function* testDefaultFocusUserInput() {
+  todo(false, "BrowserTestUtils.synthesizeMouseAtCenter doesn't move the user " +
+    "focus to the chat window, even though we're recording a click correctly.");
+  return;
+
+  yield setUp();
+  let browser = gBrowser.selectedTab.linkedBrowser;
+  let mm = browser.messageManager;
+
+  let promise = new Promise(resolve => {
+    mm.addMessageListener("ChatOpenerClicked", function handler() {
+      mm.removeMessageListener("ChatOpenerClicked", handler);
+      promiseOpenChat("http://example.com").then(resolve);
+    });
+  });
+
+  yield ContentTask.spawn(browser, null, function* () {
+    let button = content.document.getElementById("chat-opener");
+    button.addEventListener("click", function onclick() {
+      button.removeEventListener("click", onclick);
+      sendAsyncMessage("ChatOpenerClicked");
+    });
+  });
+  // Note we must use synthesizeMouseAtCenter() rather than calling
+  // .click() directly as this causes nsIDOMWindowUtils.isHandlingUserInput
+  // to be true.
+  yield BrowserTestUtils.synthesizeMouseAtCenter("#chat-opener", {}, browser);
+  let chat = yield promise;
+
+  // we use the default focus behaviour but the chat was opened via user input,
+  // so the chat should be focused.
+  Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open");
+  yield promiseWaitForCondition(() => !isTabFocused());
+  Assert.ok(!isTabFocused(), "the tab should have lost focus.");
+  Assert.ok(isChatFocused(chat), "the chat should have got focus.");
+});
+
+// We explicitly ask for the chat to be focused.
+add_chat_task(function* testExplicitFocus() {
+  yield setUp();
+  let chat = yield promiseOpenChat("http://example.com", undefined, true);
+  // we use the default focus behaviour, which means that because this was
+  // not the direct result of user action the chat should not be focused.
+  Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open");
+  yield promiseWaitForCondition(() => !isTabFocused());
+  Assert.ok(!isTabFocused(), "the tab should have lost focus.");
+  Assert.ok(isChatFocused(chat), "the chat should have got focus.");
+});
+
+// Open a minimized chat via default focus behaviour - it will open and not
+// have focus.  Then open the same chat without 'minimized' - it will be
+// restored but should still not have grabbed focus.
+add_chat_task(function* testNoFocusOnAutoRestore() {
+  yield setUp();
+  let chat = yield promiseOpenChat("http://example.com", "minimized");
+  Assert.ok(chat.minimized, "chat is minimized");
+  Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open");
+  Assert.ok(isTabFocused(), "the tab should remain focused.");
+  Assert.ok(!isChatFocused(chat), "the chat should not be focused.");
+  yield promiseOpenChat("http://example.com");
+  Assert.ok(!chat.minimized, "chat should be restored");
+  Assert.ok(isTabFocused(), "the tab should remain focused.");
+  Assert.ok(!isChatFocused(chat), "the chat should not be focused.");
+});
+
+// Here we open a chat, which will not be focused.  Then we minimize it and
+// restore it via a titlebar clock - it should get focus at that point.
+add_chat_task(function* testFocusOnExplicitRestore() {
+  yield setUp();
+  let chat = yield promiseOpenChat("http://example.com");
+  Assert.ok(!chat.minimized, "chat should have been opened restored");
+  Assert.ok(isTabFocused(), "the tab should remain focused.");
+  Assert.ok(!isChatFocused(chat), "the chat should not be focused.");
+  chat.minimized = true;
+  Assert.ok(isTabFocused(), "tab should still be focused");
+  Assert.ok(!isChatFocused(chat), "the chat should not be focused.");
+
+  let promise = promiseOneMessage(chat.content, "Social:FocusEnsured");
+  // pretend we clicked on the titlebar
+  chat.onTitlebarClick({button: 0});
+  yield promise; // wait for focus event.
+  Assert.ok(!chat.minimized, "chat should have been restored");
+  Assert.ok(isChatFocused(chat), "chat should be focused");
+  Assert.strictEqual(chat, chatbar.selectedChat, "chat is marked selected");
+});
+
+// Open 2 chats and give 1 focus.  Minimize the focused one - the second
+// should get focus.
+add_chat_task(function* testMinimizeFocused() {
+  yield setUp();
+  let chat1 = yield promiseOpenChat("http://example.com#1");
+  let chat2 = yield promiseOpenChat("http://example.com#2");
+  Assert.equal(numChatsInWindow(window), 2, "2 chats open");
+  Assert.strictEqual(chatbar.selectedChat, chat2, "chat2 is selected");
+  let promise = promiseOneMessage(chat1.content, "Social:FocusEnsured");
+  chatbar.selectedChat = chat1;
+  chatbar.focus();
+  yield promise; // wait for chat1 to get focus.
+  Assert.strictEqual(chat1, chatbar.selectedChat, "chat1 is marked selected");
+  Assert.notStrictEqual(chat2, chatbar.selectedChat, "chat2 is not marked selected");
+
+  todo(false, "Bug 1245803 should re-enable the test below to have a chat window " +
+    "re-gain focus when another chat window is minimized.");
+  return;
+
+  promise = promiseOneMessage(chat2.content, "Social:FocusEnsured");
+  chat1.minimized = true;
+  yield promise; // wait for chat2 to get focus.
+  Assert.notStrictEqual(chat1, chatbar.selectedChat, "chat1 is not marked selected");
+  Assert.strictEqual(chat2, chatbar.selectedChat, "chat2 is marked selected");
+});
+
+// Open 2 chats, select and focus the second.  Pressing the TAB key should
+// cause focus to move between all elements in our chat window before moving
+// to the next chat window.
+add_chat_task(function* testTab() {
+  yield setUp();
+
+  function sendTabAndWaitForFocus(chat, eltid) {
+    EventUtils.sendKey("tab");
+
+    return ContentTask.spawn(chat.content, { eltid: eltid }, function* (args) {
+      let doc = content.document;
+
+      // 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...
+      yield new Promise(function(resolve, reject) {
+        let tries = 0;
+        let interval = content.setInterval(function() {
+          if (tries >= 30) {
+            clearInterval(interval);
+            reject("never got focus");
+            return;
+          }
+          tries++;
+          let elt = args.eltid ? doc.getElementById(args.eltid) : doc.documentElement;
+          if (doc.activeElement == elt) {
+            content.clearInterval(interval);
+            resolve();
+          }
+          info("retrying wait for focus: " + tries);
+          info("(the active element is " + doc.activeElement + "/" +
+            doc.activeElement.getAttribute("id") + ")");
+        }, 100);
+        info("waiting for element " + args.eltid + " to get focus");
+      });
+    });
+  }
+
+  let chat1 = yield promiseOpenChat(CHAT_URL + "#1");
+  let chat2 = yield promiseOpenChat(CHAT_URL + "#2");
+  chatbar.selectedChat = chat2;
+  let promise = promiseOneMessage(chat2.content, "Social:FocusEnsured");
+  chatbar.focus();
+  info("waiting for second chat to get focus");
+  yield promise;
+
+  // Our chats have 3 focusable elements, so it takes 4 TABs to move
+  // to the new chat.
+  yield sendTabAndWaitForFocus(chat2, "input1");
+  Assert.ok(isChatFocused(chat2), "new chat still focused after first tab");
+
+  yield sendTabAndWaitForFocus(chat2, "input2");
+  Assert.ok(isChatFocused(chat2), "new chat still focused after tab");
+
+  yield sendTabAndWaitForFocus(chat2, "iframe");
+  Assert.ok(isChatFocused(chat2), "new chat still focused after tab");
+
+  // this tab now should move to the next chat, but focus the
+  // document element itself (hence the null eltid)
+  yield sendTabAndWaitForFocus(chat1, null);
+  Assert.ok(isChatFocused(chat1), "first chat is focused");
+});
+
+// Open a chat and focus an element other than the first. Move focus to some
+// other item (the tab itself in this case), then focus the chatbar - the
+// same element that was previously focused should still have focus.
+add_chat_task(function* testFocusedElement() {
+  yield setUp();
+
+  // open a chat with focus requested.
+  let chat = yield promiseOpenChat(CHAT_URL, undefined, true);
+
+  yield ContentTask.spawn(chat.content, null, function* () {
+    content.document.getElementById("input2").focus();
+  });
+
+  // set focus to the main window.
+  let tabb = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+  let promise = promiseOneEvent(window, "focus");
+  Services.focus.moveFocus(window, null, Services.focus.MOVEFOCUS_ROOT, 0);
+  yield promise;
+
+  promise = promiseOneMessage(chat.content, "Social:FocusEnsured");
+  chatbar.focus();
+  yield promise;
+
+  yield ContentTask.spawn(chat.content, null, function* () {
+    Assert.equal(content.document.activeElement.getAttribute("id"), "input2",
+      "correct input field still has focus");
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/chat/browser_tearoff.js
@@ -0,0 +1,135 @@
+/* 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/. */
+
+var chatbar = document.getElementById("pinnedchats");
+
+function promiseNewWindowLoaded() {
+  return new Promise(resolve => {
+    Services.wm.addListener({
+      onWindowTitleChange: function() {},
+      onCloseWindow: function(xulwindow) {},
+      onOpenWindow: function(xulwindow) {
+        var domwindow = xulwindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+                              .getInterface(Components.interfaces.nsIDOMWindow);
+        Services.wm.removeListener(this);
+        // wait for load to ensure the window is ready for us to test
+        domwindow.addEventListener("load", function _load(event) {
+          let doc = domwindow.document;
+          if (event.target != doc)
+            return;
+          domwindow.removeEventListener("load", _load);
+          resolve(domwindow);
+        });
+      },
+    });
+  });
+}
+
+add_chat_task(function* testTearoffChat() {
+  let chatbox = yield promiseOpenChat("http://example.com");
+  Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open");
+
+  let chatTitle = yield ContentTask.spawn(chatbox.content, null, function* () {
+    let chatDoc = content.document;
+
+    // Mutate the chat document a bit before we tear it off.
+    let div = chatDoc.createElement("div");
+    div.setAttribute("id", "testdiv");
+    div.setAttribute("test", "1");
+    chatDoc.body.appendChild(div);
+
+    return chatDoc.title;
+  });
+
+  Assert.equal(chatbox.getAttribute("label"), chatTitle,
+               "the new chatbox should show the title of the chat window");
+
+  // chatbox is open, lets detach. The new chat window will be caught in
+  // the window watcher below
+  let promise = promiseNewWindowLoaded();
+
+  let swap = document.getAnonymousElementByAttribute(chatbox, "anonid", "swap");
+  swap.click();
+
+  // and wait for the new window.
+  let domwindow = yield promise;
+
+  Assert.equal(domwindow.document.documentElement.getAttribute("windowtype"), "Social:Chat", "Social:Chat window opened");
+  Assert.equal(numChatsInWindow(window), 0, "should be no chats in the chat bar");
+
+  // get the chatbox from the new window.
+  chatbox = domwindow.document.getElementById("chatter")
+  Assert.equal(chatbox.getAttribute("label"), chatTitle, "window should have same title as chat");
+
+  yield ContentTask.spawn(chatbox.content, null, function* () {
+    let div = content.document.getElementById("testdiv");
+    Assert.equal(div.getAttribute("test"), "1", "docshell should have been swapped");
+    div.setAttribute("test", "2");
+  });
+
+  // swap the window back to the chatbar
+  promise = promiseOneEvent(domwindow, "unload");
+  swap = domwindow.document.getAnonymousElementByAttribute(chatbox, "anonid", "swap");
+  swap.click();
+
+  yield promise;
+
+  Assert.equal(numChatsInWindow(window), 1, "chat should be docked back in the window");
+  chatbox = chatbar.selectedChat;
+  Assert.equal(chatbox.getAttribute("label"), chatTitle,
+               "the new chatbox should show the title of the chat window again");
+
+  yield ContentTask.spawn(chatbox.content, null, function* () {
+    let div = content.document.getElementById("testdiv");
+    Assert.equal(div.getAttribute("test"), "2", "docshell should have been swapped");
+  });
+});
+
+// Similar test but with 2 chats.
+add_chat_task(function* testReattachTwice() {
+  let chatbox1 = yield promiseOpenChat("http://example.com#1");
+  let chatbox2 = yield promiseOpenChat("http://example.com#2");
+  Assert.equal(numChatsInWindow(window), 2, "both chats should be docked in the window");
+
+  info("chatboxes are open, detach from window");
+  let promise = promiseNewWindowLoaded();
+  document.getAnonymousElementByAttribute(chatbox1, "anonid", "swap").click();
+  let domwindow1 = yield promise;
+  chatbox1 = domwindow1.document.getElementById("chatter");
+  Assert.equal(numChatsInWindow(window), 1, "only second chat should be docked in the window");
+
+  promise = promiseNewWindowLoaded();
+  document.getAnonymousElementByAttribute(chatbox2, "anonid", "swap").click();
+  let domwindow2 = yield promise;
+  chatbox2 = domwindow2.document.getElementById("chatter");
+  Assert.equal(numChatsInWindow(window), 0, "should be no docked chats");
+
+  promise = promiseOneEvent(domwindow2, "unload");
+  domwindow2.document.getAnonymousElementByAttribute(chatbox2, "anonid", "swap").click();
+  yield promise;
+  Assert.equal(numChatsInWindow(window), 1, "one chat should be docked back in the window");
+
+  promise = promiseOneEvent(domwindow1, "unload");
+  domwindow1.document.getAnonymousElementByAttribute(chatbox1, "anonid", "swap").click();
+  yield promise;
+  Assert.equal(numChatsInWindow(window), 2, "both chats should be docked back in the window");
+});
+
+// Check that Chat.closeAll() also closes detached windows.
+add_chat_task(function* testCloseAll() {
+  let chatbox1 = yield promiseOpenChat("http://example.com#1");
+  let chatbox2 = yield promiseOpenChat("http://example.com#2");
+
+  let promise = promiseNewWindowLoaded();
+  document.getAnonymousElementByAttribute(chatbox1, "anonid", "swap").click();
+  let domwindow = yield promise;
+  chatbox1 = domwindow.document.getElementById("chatter");
+
+  let promiseWindowUnload = promiseOneEvent(domwindow, "unload");
+
+  Assert.equal(numChatsInWindow(window), 1, "second chat should still be docked");
+  Chat.closeAll("http://example.com");
+  yield promiseWindowUnload;
+  Assert.equal(numChatsInWindow(window), 0, "should be no chats left");
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/chat/chat.html
@@ -0,0 +1,14 @@
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>test chat window</title>
+  </head>
+  <body>
+    <p>This is a test chat window.</p>
+    <!-- a couple of input fields to help with focus testing -->
+    <input id="input1"/>
+    <input id="input2"/>
+    <!-- an iframe here so this one page generates multiple load events -->
+    <iframe id="iframe" src="data:text/plain:this is an iframe"></iframe>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/chat/head.js
@@ -0,0 +1,130 @@
+/* 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/. */
+
+// Utility functions for Chat tests.
+
+var Chat = Cu.import("resource:///modules/Chat.jsm", {}).Chat;
+const kDefaultButtonSet = new Set(["minimize", "swap", "close"]);
+
+function promiseOpenChat(url, mode, focus, buttonSet = null) {
+  let uri = Services.io.newURI(url, null, null);
+  let origin = uri.prePath;
+  let title = origin;
+  return new Promise(resolve => {
+    // we just through a few hoops to ensure the content document is fully
+    // loaded, otherwise tests that rely on that content may intermittently fail.
+    let callback = function(chatbox) {
+      let mm = chatbox.content.messageManager;
+      mm.sendAsyncMessage("WaitForDOMContentLoaded");
+      mm.addMessageListener("DOMContentLoaded", function cb() {
+        mm.removeMessageListener("DOMContentLoaded", cb);
+        resolve(chatbox);
+      });
+    }
+    let chatbox = Chat.open(null, {
+      origin: origin,
+      title: title,
+      url: url,
+      mode: mode,
+      focus: focus
+    }, callback);
+    if (buttonSet) {
+      chatbox.setAttribute("buttonSet", buttonSet);
+    }
+  });
+}
+
+// Opens a chat, returns a promise resolved when the chat callback fired.
+function promiseOpenChatCallback(url, mode) {
+  let uri = Services.io.newURI(url, null, null);
+  let origin = uri.prePath;
+  let title = origin;
+  return new Promise(resolve => {
+    Chat.open(null, { origin, title, url, mode }, resolve);
+  });
+}
+
+// Opens a chat, returns the chat window's promise which fires when the chat
+// starts loading.
+function promiseOneEvent(target, eventName, capture) {
+  return new Promise(resolve => {
+    target.addEventListener(eventName, function handler(event) {
+      target.removeEventListener(eventName, handler, capture);
+      resolve();
+    }, capture);
+  });
+}
+
+function promiseOneMessage(target, messageName) {
+  return new Promise(resolve => {
+    let mm = target.messageManager;
+    mm.addMessageListener(messageName, function handler() {
+      mm.removeMessageListener(messageName, handler);
+      resolve();
+    });
+  });
+}
+
+// Return the number of chats in a browser window.
+function numChatsInWindow(win) {
+  let chatbar = win.document.getElementById("pinnedchats");
+  return chatbar.childElementCount;
+}
+
+function promiseWaitForFocus() {
+  return new Promise(resolve => waitForFocus(resolve));
+}
+
+// A simple way to clean up after each test.
+function add_chat_task(genFunction) {
+  add_task(function* () {
+    info("Starting chat test " + genFunction.name);
+    try {
+      yield genFunction();
+    } finally {
+      info("Finished chat test " + genFunction.name + " - cleaning up.");
+      // close all docked chats.
+      while (chatbar.childNodes.length) {
+        chatbar.childNodes[0].close();
+      }
+      // and non-docked chats.
+      let winEnum = Services.wm.getEnumerator("Social:Chat");
+      while (winEnum.hasMoreElements()) {
+        let win = winEnum.getNext();
+        if (win.closed) {
+          continue;
+        }
+        win.close();
+      }
+    }
+  });
+}
+
+function waitForCondition(condition, nextTest, errorMsg) {
+  var tries = 0;
+  var interval = setInterval(function() {
+    if (tries >= 100) {
+      ok(false, errorMsg);
+      moveOn();
+    }
+    var conditionPassed;
+    try {
+      conditionPassed = condition();
+    } catch (e) {
+      ok(false, e + "\n" + e.stack);
+      conditionPassed = false;
+    }
+    if (conditionPassed) {
+      moveOn();
+    }
+    tries++;
+  }, 100);
+  var moveOn = function() { clearInterval(interval); nextTest(); };
+}
+
+function promiseWaitForCondition(aConditionFn) {
+  return new Promise((resolve, reject) => {
+    waitForCondition(aConditionFn, resolve, "Condition didn't pass.");
+  });
+}
--- a/browser/base/content/test/social/browser.ini
+++ b/browser/base/content/test/social/browser.ini
@@ -1,24 +1,48 @@
 [DEFAULT]
 skip-if = buildapp == "mulet"
 support-files =
   blocklist.xml
+  checked.jpg
   head.js
   opengraph/og_invalid_url.html
   opengraph/opengraph.html
   opengraph/shortlink_linkrel.html
   opengraph/shorturl_link.html
   opengraph/shorturl_linkrel.html
   microformats.html
   share.html
   share_activate.html
   social_activate.html
   social_activate_basic.html
   social_activate_iframe.html
+  social_chat.html
+  social_crash_content_helper.js
+  social_flyout.html
+  social_mark.html
+  social_panel.html
   social_postActivation.html
+  social_sidebar.html
+  social_sidebar_empty.html
+  unchecked.jpg
   !/browser/base/content/test/plugins/blockNoPlugins.xml
 
 [browser_aboutHome_activation.js]
 [browser_addons.js]
 [browser_blocklist.js]
 [browser_share.js]
 [browser_social_activation.js]
+[browser_social_chatwindow.js]
+[browser_social_chatwindow_resize.js]
+[browser_social_chatwindowfocus.js]
+skip-if = asan # Bug 1260177
+[browser_social_contextmenu.js]
+skip-if = (os == 'linux' && e10s) # Bug 1072669 context menu relies on target element
+[browser_social_errorPage.js]
+[browser_social_flyout.js]
+[browser_social_isVisible.js]
+[browser_social_marks.js]
+[browser_social_marks_context.js]
+[browser_social_multiprovider.js]
+[browser_social_sidebar.js]
+[browser_social_status.js]
+[browser_social_window.js]
--- a/browser/base/content/test/social/browser_aboutHome_activation.js
+++ b/browser/base/content/test/social/browser_aboutHome_activation.js
@@ -1,28 +1,28 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
-var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
 
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils",
   "resource:///modules/AboutHome.jsm");
 
 var snippet =
 '     <script>' +
 '       var manifest = {' +
 '         "name": "Demo Social Service",' +
 '         "origin": "https://example.com",' +
 '         "iconURL": "chrome://branding/content/icon16.png",' +
 '         "icon32URL": "chrome://branding/content/icon32.png",' +
 '         "icon64URL": "chrome://branding/content/icon64.png",' +
-'         "shareURL": "https://example.com/browser/browser/base/content/test/social/social_share.html",' +
+'         "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' +
 '         "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
 '       };' +
 '       function activateProvider(node) {' +
 '         node.setAttribute("data-service", JSON.stringify(manifest));' +
 '         var event = new CustomEvent("ActivateSocialFeature");' +
 '         node.dispatchEvent(event);' +
 '       }' +
 '     </script>' +
@@ -34,17 +34,17 @@ var snippet =
 var snippet2 =
 '     <script>' +
 '       var manifest = {' +
 '         "name": "Demo Social Service",' +
 '         "origin": "https://example.com",' +
 '         "iconURL": "chrome://branding/content/icon16.png",' +
 '         "icon32URL": "chrome://branding/content/icon32.png",' +
 '         "icon64URL": "chrome://branding/content/icon64.png",' +
-'         "shareURL": "https://example.com/browser/browser/base/content/test/social/social_share.html",' +
+'         "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' +
 '         "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
 '         "oneclick": true' +
 '       };' +
 '       function activateProvider(node) {' +
 '         node.setAttribute("data-service", JSON.stringify(manifest));' +
 '         var event = new CustomEvent("ActivateSocialFeature");' +
 '         node.dispatchEvent(event);' +
 '       }' +
@@ -96,18 +96,20 @@ function test()
       // ensure our activation snippet is indeed available
       yield ContentTask.spawn(tab.linkedBrowser, {}, function*(arg) {
         ok(!!content.document.getElementById("snippets"), "Found snippets element");
         ok(!!content.document.getElementById("activationSnippet"), "The snippet is present.");
       });
 
       yield new Promise(resolve => {
         activateProvider(tab, test.panel).then(() => {
+          ok(SocialSidebar.provider, "provider activated");
           checkSocialUI();
-          SocialService.uninstallProvider("https://example.com", function () {
+          is(gBrowser.currentURI.spec, SocialSidebar.provider.manifest.postActivationURL, "postActivationURL was loaded");
+          SocialService.uninstallProvider(SocialSidebar.provider.origin, function () {
             info("provider uninstalled");
             resolve();
           });
         });
       });
 
       // activation opened a post-activation info tab, close it.
       yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
@@ -203,27 +205,30 @@ function sendActivationEvent(tab) {
     doc = doc.defaultView.frames[0].document;
   let button = doc.getElementById("activationSnippet");
   BrowserTestUtils.synthesizeMouseAtCenter(button, {}, tab.linkedBrowser);
 }
 
 function activateProvider(tab, expectPanel, aCallback) {
   return new Promise(resolve => {
     if (expectPanel) {
-      BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => {
+      ensureEventFired(PopupNotifications.panel, "popupshown").then(() => {
         let panel = document.getElementById("servicesInstall-notification");
         panel.button.click();
       });
     }
     waitForProviderLoad().then(() => {
+      ok(SocialSidebar.provider, "new provider is active");
+      ok(SocialSidebar.opened, "sidebar is open");
       checkSocialUI();
       resolve();
     });
     sendActivationEvent(tab);
   });
 }
 
 function waitForProviderLoad(cb) {
   return Promise.all([
     promiseObserverNotified("social:provider-enabled"),
     ensureFrameLoaded(gBrowser, "https://example.com/browser/browser/base/content/test/social/social_postActivation.html"),
+    ensureFrameLoaded(SocialSidebar.browser)
   ]);
 }
--- a/browser/base/content/test/social/browser_addons.js
+++ b/browser/base/content/test/social/browser_addons.js
@@ -1,28 +1,28 @@
 var AddonManager = Cu.import("resource://gre/modules/AddonManager.jsm", {}).AddonManager;
-var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
 
 var manifest = {
   name: "provider 1",
   origin: "https://example.com",
-  shareURL: "https://example.com/browser/browser/base/content/test/social/social_share.html",
+  sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",
   iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
 };
 var manifest2 = { // used for testing install
   name: "provider 2",
   origin: "https://test1.example.com",
-  shareURL: "https://test1.example.com/browser/browser/base/content/test/social/social_share.html",
+  sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",
   iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png",
   version: "1.0"
 };
 var manifestUpgrade = { // used for testing install
   name: "provider 3",
   origin: "https://test2.example.com",
-  shareURL: "https://test2.example.com/browser/browser/base/content/test/social/social_share.html",
+  sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html",
   iconURL: "https://test2.example.com/browser/browser/base/content/test/general/moz.png",
   version: "1.0"
 };
 
 function test() {
   waitForExplicitFinish();
   PopupNotifications.panel.setAttribute("animate", "false");
   registerCleanupFunction(function () {
--- a/browser/base/content/test/social/browser_blocklist.js
+++ b/browser/base/content/test/social/browser_blocklist.js
@@ -1,29 +1,29 @@
 /* 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/. */
 
 // a place for miscellaneous social tests
 
-var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
 
 const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
 var blocklistURL = "http://example.com/browser/browser/base/content/test/social/blocklist.xml";
 
 var manifest = { // normal provider
   name: "provider ok",
   origin: "https://example.com",
-  shareURL: "https://example.com/browser/browser/base/content/test/social/social_share.html",
+  sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
   iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
 };
 var manifest_bad = { // normal provider
   name: "provider blocked",
   origin: "https://test1.example.com",
-  shareURL: "https://test1.example.com/browser/browser/base/content/test/social/social_share.html",
+  sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html",
   iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png"
 };
 
 // blocklist testing
 function updateBlocklist() {
   var blocklistNotifier = Cc["@mozilla.org/extensions/blocklist;1"]
                           .getService(Ci.nsITimerCallback);
   let promise = promiseObserverNotified("blocklist-updated");
--- a/browser/base/content/test/social/browser_share.js
+++ b/browser/base/content/test/social/browser_share.js
@@ -1,10 +1,10 @@
 
-var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
 
 var baseURL = "https://example.com/browser/browser/base/content/test/social/";
 
 var manifest = { // normal provider
   name: "provider 1",
   origin: "https://example.com",
   iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png",
   shareURL: "https://example.com/browser/browser/base/content/test/social/share.html"
--- a/browser/base/content/test/social/browser_social_activation.js
+++ b/browser/base/content/test/social/browser_social_activation.js
@@ -5,17 +5,17 @@
 ///////////////////
 //
 // Whitelisting this test.
 // As part of bug 1077403, the leaking uncaught rejection should be fixed.
 //
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: Assert is null");
 
 
-var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
 
 var tabsToRemove = [];
 
 function removeProvider(provider) {
   return new Promise(resolve => {
     // a full install sets the manifest into a pref, addProvider alone doesn't,
     // make sure we uninstall if the manifest was added.
     if (provider.manifest) {
@@ -68,16 +68,17 @@ function activateIFrameProvider(domain, 
   newTab(activationURL).then(tab => {
     sendActivationEvent(tab, callback, false);
   });
 }
 
 function waitForProviderLoad(origin) {
   return Promise.all([
     ensureFrameLoaded(gBrowser, origin + "/browser/browser/base/content/test/social/social_postActivation.html"),
+    ensureFrameLoaded(SocialSidebar.browser)
   ]);
 }
 
 function getAddonItemInList(aId, aList) {
   var item = aList.firstChild;
   while (item) {
     if ("mAddon" in item && item.mAddon.id == aId) {
       aList.ensureElementIsVisible(item);
@@ -107,32 +108,33 @@ function clickAddonRemoveButton(tab, aCa
       executeSoon(function() { aCallback(addon); });
     });
 
     BrowserTestUtils.synthesizeMouseAtCenter(button, {}, tab.linkedBrowser);
   });
 }
 
 function activateOneProvider(manifest, finishActivation, aCallback) {
-  info("activating provider "+manifest.name);
   let panel = document.getElementById("servicesInstall-notification");
   BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => {
     ok(!panel.hidden, "servicesInstall-notification panel opened");
     if (finishActivation)
       panel.button.click();
     else
       panel.closebutton.click();
   });
   BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden").then(() => {
     ok(panel.hidden, "servicesInstall-notification panel hidden");
     if (!finishActivation) {
       ok(panel.hidden, "activation panel is not showing");
       executeSoon(aCallback);
     } else {
       waitForProviderLoad(manifest.origin).then(() => {
+        is(SocialSidebar.provider.origin, manifest.origin, "new provider is active");
+        ok(SocialSidebar.opened, "sidebar is open");
         checkSocialUI();
         executeSoon(aCallback);
       });
     }
   });
 
   // the test will continue as the popup events fire...
   activateProvider(manifest.origin, function() {
@@ -140,29 +142,29 @@ function activateOneProvider(manifest, f
   });
 }
 
 var gTestDomains = ["https://example.com", "https://test1.example.com", "https://test2.example.com"];
 var gProviders = [
   {
     name: "provider 1",
     origin: "https://example.com",
-    shareURL: "https://example.com/browser/browser/base/content/test/social/social_share.html?provider1",
+    sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html?provider1",
     iconURL: "chrome://branding/content/icon48.png"
   },
   {
     name: "provider 2",
     origin: "https://test1.example.com",
-    shareURL: "https://test1.example.com/browser/browser/base/content/test/social/social_share.html?provider2",
+    sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar_empty.html?provider2",
     iconURL: "chrome://branding/content/icon64.png"
   },
   {
     name: "provider 3",
     origin: "https://test2.example.com",
-    shareURL: "https://test2.example.com/browser/browser/base/content/test/social/social_share.html?provider2",
+    sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar_empty.html?provider2",
     iconURL: "chrome://branding/content/about-logo.png"
   }
 ];
 
 
 function test() {
   PopupNotifications.panel.setAttribute("animate", "false");
   registerCleanupFunction(function () {
@@ -184,27 +186,29 @@ var tests = {
       Services.prefs.clearUserPref("social.remote-install.enabled");
       next();
     });
   },
   
   testIFrameActivation: function(next) {
     activateIFrameProvider(gTestDomains[0], function() {
       is(SocialUI.enabled, false, "SocialUI is not enabled");
+      ok(!SocialSidebar.provider, "provider is not installed");
       let panel = document.getElementById("servicesInstall-notification");
       ok(panel.hidden, "activation panel still hidden");
       checkSocialUI();
       next();
     });
   },
   
   testActivationFirstProvider: function(next) {
     // first up we add a manifest entry for a single provider.
     activateOneProvider(gProviders[0], false, function() {
       // we deactivated leaving no providers left, so Social is disabled.
+      ok(!SocialSidebar.provider, "should be no provider left after disabling");
       checkSocialUI();
       next();
     });
   },
   
   testActivationMultipleProvider: function(next) {
     // The trick with this test is to make sure that Social.providers[1] is
     // the current provider when doing the undo - this makes sure that the
@@ -212,16 +216,17 @@ var tests = {
     // do in some cases (but those cases do not include what this test does)
     // first enable the 2 providers
     SocialService.addProvider(gProviders[0], function() {
       SocialService.addProvider(gProviders[1], function() {
         checkSocialUI();
         // activate the last provider.
         activateOneProvider(gProviders[2], false, function() {
           // we deactivated - the first provider should be enabled.
+          is(SocialSidebar.provider.origin, Social.providers[1].origin, "original provider should have been reactivated");
           checkSocialUI();
           next();
         });
       });
     });
   },
 
   testAddonManagerDoubleInstall: function(next) {
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_chatwindow.js
@@ -0,0 +1,141 @@
+/* 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/. */
+
+var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
+
+var manifests = [
+  {
+    name: "provider@example.com",
+    origin: "https://example.com",
+    sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html?example.com",
+    iconURL: "chrome://branding/content/icon48.png"
+  },
+  {
+    name: "provider@test1",
+    origin: "https://test1.example.com",
+    sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html?test1",
+    iconURL: "chrome://branding/content/icon48.png"
+  },
+  {
+    name: "provider@test2",
+    origin: "https://test2.example.com",
+    sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html?test2",
+    iconURL: "chrome://branding/content/icon48.png"
+  }
+];
+
+var chatId = 0;
+function openChat(provider) {
+  return new Promise(resolve => {
+    SocialSidebar.provider = provider;
+    let chatUrl = provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
+    let url = chatUrl + "?id=" + (chatId++);
+    makeChat("normal", "chat " + chatId, (cb) => { resolve(cb); });
+  });
+}
+
+function windowHasChats(win) {
+  return !!getChatBar().firstElementChild;
+}
+
+function test() {
+  requestLongerTimeout(2); // only debug builds seem to need more time...
+  waitForExplicitFinish();
+
+  let frameScript = "data:,(" + function frame_script() {
+    addMessageListener("socialTest-CloseSelf", function(e) {
+      content.close();
+    }, true);
+  }.toString() + ")();";
+  let mm = getGroupMessageManager("social");
+  mm.loadFrameScript(frameScript, true);
+
+  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) {
+    let chats = document.getElementById("pinnedchats");
+    ok(chats.children.length == 0, "no chatty children left behind");
+    cb();
+  };
+  runSocialTestWithProvider(manifests, function (finishcb) {
+    ok(Social.enabled, "Social is enabled");
+    SocialSidebar.show();
+    runSocialTests(tests, undefined, postSubTest, function() {
+      window.moveTo(oldleft, window.screenY)
+      window.resizeTo(oldwidth, window.outerHeight);
+      mm.removeDelayedFrameScript(frameScript);
+      finishcb();
+    });
+  });
+}
+
+var tests = {
+  testOpenCloseChat: function(next) {
+    openChat(SocialSidebar.provider).then((cb) => {
+      BrowserTestUtils.waitForCondition(() => { return cb.minimized; },
+                                        "chatbox is minimized").then(() => {
+        ok(cb.minimized, "chat is minimized after toggle");
+        BrowserTestUtils.waitForCondition(() => { return !cb.minimized; },
+                                          "chatbox is not minimized").then(() => {
+          ok(!cb.minimized, "chat is not minimized after toggle");
+          promiseNodeRemoved(cb).then(next);
+          let mm = cb.content.messageManager;
+          mm.sendAsyncMessage("socialTest-CloseSelf", {});
+          info("close chat window requested");
+        });
+        cb.toggle();
+      });
+
+      ok(!cb.minimized, "chat is not minimized on open");
+      // toggle to minimize chat
+      cb.toggle();
+    });
+  },
+
+  // Check what happens when you close the only visible chat.
+  testCloseOnlyVisible: function(next) {
+    let chatbar = getChatBar();
+    let chatWidth = undefined;
+    let num = 0;
+    is(chatbar.childNodes.length, 0, "chatbar starting empty");
+    is(chatbar.menupopup.childNodes.length, 0, "popup starting empty");
+
+    makeChat("normal", "first chat", function() {
+      // got the first one.
+      checkPopup();
+      ok(chatbar.menupopup.parentNode.collapsed, "menu selection isn't visible");
+      // we kinda cheat here and get the width of the first chat, assuming
+      // that all future chats will have the same width when open.
+      chatWidth = chatbar.calcTotalWidthOf(chatbar.selectedChat);
+      let desired = chatWidth * 1.5;
+      resizeWindowToChatAreaWidth(desired, function(sizedOk) {
+        ok(sizedOk, "can't do any tests without this width");
+        checkPopup();
+        makeChat("normal", "second chat", function() {
+          is(chatbar.childNodes.length, 2, "now have 2 chats");
+          let first = chatbar.childNodes[0];
+          let second = chatbar.childNodes[1];
+          is(chatbar.selectedChat, first, "first chat is selected");
+          ok(second.collapsed, "second chat is currently collapsed");
+          // closing the first chat will leave enough room for the second
+          // chat to appear, and thus become selected.
+          chatbar.selectedChat.close();
+          is(chatbar.selectedChat, second, "second chat is selected");
+          Task.spawn(closeAllChats).then(next);
+        });
+      });
+    });
+  },
+
+  testShowWhenCollapsed: function(next) {
+    get3ChatsForCollapsing("normal", function(first, second, third) {
+      let chatbar = getChatBar();
+      chatbar.showChat(first);
+      ok(!first.collapsed, "first should no longer be collapsed");
+      is(second.collapsed ||  third.collapsed, true, "one of the others should be collapsed");
+      Task.spawn(closeAllChats).then(next);
+    });
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_chatwindow_resize.js
@@ -0,0 +1,82 @@
+/* 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_empty.html",
+    iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png",
+    // added for test purposes
+    chatURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html"
+  };
+  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) {
+    let chats = document.getElementById("pinnedchats");
+    ok(chats.children.length == 0, "no chatty children left behind");
+    cb();
+  };
+
+  runSocialTestWithProvider(manifest, function (finishcb) {
+    let sbrowser = document.getElementById("social-sidebar-browser");
+    ensureFrameLoaded(sbrowser).then(() => {
+      let provider = SocialSidebar.provider;
+      provider.chatURL = manifest.chatURL;
+      ok(provider, "provider is set");
+      ok(provider.chatURL, "provider has chatURL");
+      // executeSoon to let the browser UI observers run first
+      runSocialTests(tests, undefined, postSubTest, function() {
+        window.moveTo(oldleft, window.screenY)
+        window.resizeTo(oldwidth, window.outerHeight);
+        finishcb();
+      });
+    });
+    SocialSidebar.show();
+  });
+}
+
+var tests = {
+
+  // resize and collapse testing.
+  testBrowserResize: function(next, mode) {
+    let chats = document.getElementById("pinnedchats");
+    get3ChatsForCollapsing(mode || "normal", function(first, second, third) {
+      let chatWidth = chats.getTotalChildWidth(first);
+      ok(chatWidth, "have a chatwidth");
+      let popupWidth = getPopupWidth();
+      ok(popupWidth, "have a popupwidth");
+      info("starting resize tests - each chat's width is " + chatWidth +
+           " and the popup width is " + popupWidth);
+      // Note that due to a difference between "device", "app" and "css" pixels
+      // we allow use 2 pixels as the minimum size difference.
+      resizeAndCheckWidths(first, second, third, [
+        [chatWidth-2, 1, "to < 1 chat width - only last should be visible."],
+        [chatWidth+2, 1, "2 pixels more then one fully exposed (not counting popup) - still only 1."],
+        [chatWidth+popupWidth+2, 1, "2 pixels more than one fully exposed (including popup) - still only 1."],
+        [chatWidth*2-2, 1, "second not showing by 2 pixels (not counting popup) - only 1 exposed."],
+        [chatWidth*2+popupWidth-2, 1, "second not showing by 2 pixelx (including popup) - only 1 exposed."],
+        [chatWidth*2+popupWidth+2, 2, "big enough to fit 2 - nub remains visible as first is still hidden"],
+        [chatWidth*3+popupWidth-2, 2, "one smaller than the size necessary to display all three - first still hidden"],
+        [chatWidth*3+popupWidth+2, 3, "big enough to fit all - all exposed (which removes the nub)"],
+        [chatWidth*3+2, 3, "now the nub is hidden we can resize back down to chatWidth*3 before overflow."],
+        [chatWidth*3-2, 2, "2 pixels less and the first is again collapsed (and the nub re-appears)"],
+        [chatWidth*2+popupWidth+2, 2, "back down to just big enough to fit 2"],
+        [chatWidth*2+popupWidth-2, 1, "back down to just not enough to fit 2"],
+        [chatWidth*3+popupWidth+2, 3, "now a large jump to make all 3 visible (ie, affects 2)"],
+        [chatWidth*1.5, 1, "and a large jump back down to 1 visible (ie, affects 2)"],
+      ], function() {
+        Task.spawn(closeAllChats).then(next);
+      });
+    });
+  },
+
+  testBrowserResizeMinimized: function(next) {
+    this.testBrowserResize(next);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_chatwindowfocus.js
@@ -0,0 +1,66 @@
+/* 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 isChatFocused(chat) {
+  return getChatBar()._isChatFocused(chat);
+}
+
+var manifest = { // normal provider
+  name: "provider 1",
+  origin: "https://example.com",
+  sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
+  iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
+};
+
+function test() {
+  waitForExplicitFinish();
+
+  // Note that (probably) due to bug 604289, if a tab is focused but the
+  // focused element is null, our chat windows can "steal" focus.  This is
+  // avoided if we explicitly focus an element in the tab.
+  // So we load a page with an <input> field and focus that before testing.
+  let url = "data:text/html;charset=utf-8," + encodeURI('<input id="theinput">');
+  let tab = gBrowser.selectedTab = gBrowser.addTab(url, {skipAnimation: true});
+  let browser = tab.linkedBrowser;
+  browser.addEventListener("load", function tabLoad(event) {
+    browser.removeEventListener("load", tabLoad, true);
+    // before every test we focus the input field.
+    let preSubTest = function(cb) {
+      ContentTask.spawn(browser, null, function* () {
+        content.focus();
+        content.document.getElementById("theinput").focus();
+
+        yield ContentTaskUtils.waitForCondition(
+          () => Services.focus.focusedWindow == content, "tab should have focus");
+      }).then(cb);
+    }
+    let postSubTest = function(cb) {
+      Task.spawn(closeAllChats).then(cb);
+    }
+    // and run the tests.
+    runSocialTestWithProvider(manifest, function (finishcb) {
+      SocialSidebar.show();
+      runSocialTests(tests, preSubTest, postSubTest, function () {
+        BrowserTestUtils.removeTab(tab).then(finishcb);
+      });
+    });
+  }, true);
+}
+
+var tests = {
+  // In this test we arrange for the sidebar to open the chat via a simulated
+  // click.  This should cause the new chat to be opened and focused.
+  testFocusWhenViaUser: function(next) {
+    ensureFrameLoaded(document.getElementById("social-sidebar-browser")).then(() => {
+      let chatbar = getChatBar();
+      openChatViaUser();
+      ok(chatbar.firstElementChild, "chat opened");
+      BrowserTestUtils.waitForCondition(() => isChatFocused(chatbar.selectedChat),
+                                        "chat should be focused").then(() => {
+        is(chatbar.selectedChat, chatbar.firstElementChild, "chat is selected");
+        next();
+      });
+    });
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_contextmenu.js
@@ -0,0 +1,74 @@
+/* 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/. */
+
+var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
+
+var manifest = { // used for testing install
+  name: "provider test1",
+  origin: "https://test1.example.com",
+  markURL: "https://test1.example.com/browser/browser/base/content/test/social/social_mark.html?url=%{url}",
+  markedIcon: "https://test1.example.com/browser/browser/base/content/test/social/unchecked.jpg",
+  unmarkedIcon: "https://test1.example.com/browser/browser/base/content/test/social/checked.jpg",
+
+  iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png",
+  version: "1.0"
+};
+
+function test() {
+  waitForExplicitFinish();
+  let frameScript = "data:,(" + function frame_script() {
+    addEventListener("OpenGraphData", function (aEvent) {
+      sendAsyncMessage("sharedata", aEvent.detail);
+    }, true, true);
+  }.toString() + ")();";
+  let mm = getGroupMessageManager("social");
+  mm.loadFrameScript(frameScript, true);
+
+  runSocialTestWithProvider(manifest, function (finishcb) {
+    runSocialTests(tests, undefined, undefined, function () {
+      mm.removeDelayedFrameScript(frameScript);
+      finishcb();
+    });
+  });
+}
+
+var tests = {
+  testMarkMicroformats: function(next) {
+    // emulates context menu action using target element, calling SocialMarks.markLink
+    let provider = Social._getProviderFromOrigin(manifest.origin);
+    let target, testTab;
+
+    // browser_share tests microformats on the full page, this is testing a
+    // specific target element.
+    let expecting = JSON.stringify({
+      "url": "https://example.com/browser/browser/base/content/test/social/microformats.html",
+      "microformats": {
+        "items": [{
+            "type": ["h-review"],
+            "properties": {
+              "rating": ["4.5"]
+            }
+          }
+        ],
+        "rels": {},
+        "rel-urls": {}
+      }
+    });
+
+    let mm = getGroupMessageManager("social");
+    mm.addMessageListener("sharedata", function handler(msg) {
+      is(msg.data, expecting, "microformats data ok");
+      mm.removeMessageListener("sharedata", handler);
+      BrowserTestUtils.removeTab(testTab).then(next);
+    });
+
+    let url = "https://example.com/browser/browser/base/content/test/social/microformats.html"
+    BrowserTestUtils.openNewForegroundTab(gBrowser, url).then(tab => {
+      testTab = tab;
+      let doc = tab.linkedBrowser.contentDocument;
+      target = doc.getElementById("test-review");
+      SocialMarks.markLink(manifest.origin, url, target);
+    });
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_errorPage.js
@@ -0,0 +1,187 @@
+/* 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 gc() {
+  Cu.forceGC();
+  let wu =  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+                  .getInterface(Components.interfaces.nsIDOMWindowUtils);
+  wu.garbageCollect();
+}
+
+var openChatWindow = Cu.import("resource://gre/modules/MozSocialAPI.jsm", {}).openChatWindow;
+
+function openPanel(url, panelCallback, loadCallback) {
+  // open a flyout
+  SocialFlyout.open(url, 0, panelCallback);
+  // wait for both open and loaded before callback. Since the test doesn't close
+  // the panel between opens, we cannot rely on events here. We need to ensure
+  // popupshown happens before we finish out the tests.
+  BrowserTestUtils.waitForCondition(function() {
+                    return SocialFlyout.panel.state == "open" &&
+                           SocialFlyout.iframe.contentDocument.readyState == "complete";
+                   },"flyout is open and loaded").then(() => { executeSoon(loadCallback) });
+}
+
+function openChat(url, panelCallback, loadCallback) {
+  // open a chat window
+  let chatbar = getChatBar();
+  openChatWindow(null, SocialSidebar.provider, url, panelCallback);
+  chatbar.firstChild.addEventListener("DOMContentLoaded", function panelLoad() {
+    chatbar.firstChild.removeEventListener("DOMContentLoaded", panelLoad, true);
+    executeSoon(loadCallback);
+  }, true);
+}
+
+function onSidebarLoad(callback) {
+  let sbrowser = document.getElementById("social-sidebar-browser");
+  sbrowser.addEventListener("load", function load() {
+    sbrowser.removeEventListener("load", load, true);
+    executeSoon(callback);
+  }, true);
+}
+
+var manifest = { // normal provider
+  name: "provider 1",
+  origin: "https://example.com",
+  sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",
+  iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
+};
+
+function test() {
+  waitForExplicitFinish();
+
+  runSocialTestWithProvider(manifest, function (finishcb) {
+    runSocialTests(tests, undefined, function(next) { goOnline().then(next) }, finishcb);
+  });
+}
+
+var tests = {
+  testSidebar: function(next) {
+    let sbrowser = document.getElementById("social-sidebar-browser");
+    onSidebarLoad(function() {
+      ok(sbrowser.contentDocument.documentURI.indexOf("about:socialerror?mode=tryAgainOnly")==0, "sidebar is on social error page");
+      gc();
+      // Add a new load listener, then find and click the "try again" button.
+      onSidebarLoad(function() {
+        // should still be on the error page.
+        ok(sbrowser.contentDocument.documentURI.indexOf("about:socialerror?mode=tryAgainOnly")==0, "sidebar is still on social error page");
+        // go online and try again - this should work.
+        goOnline().then(function () {
+          onSidebarLoad(function() {
+            // should now be on the correct page.
+            is(sbrowser.contentDocument.documentURI, manifest.sidebarURL, "sidebar is now on social sidebar page");
+            next();
+          });
+          sbrowser.contentDocument.getElementById("btnTryAgain").click();
+        });
+      });
+      sbrowser.contentDocument.getElementById("btnTryAgain").click();
+    });
+    // go offline then attempt to load the sidebar - it should fail.
+    goOffline().then(function() {
+      SocialSidebar.show();
+    });
+  },
+
+  testFlyout: function(next) {
+    let panelCallbackCount = 0;
+    let panel = document.getElementById("social-flyout-panel");
+    goOffline().then(function() {
+      openPanel(
+        manifest.sidebarURL, /* empty html page */
+        function() { // the panel api callback
+          panelCallbackCount++;
+        },
+        function() { // the "load" callback.
+          todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads.");
+          let href = panel.firstChild.contentDocument.documentURI;
+          ok(href.indexOf("about:socialerror?mode=compactInfo")==0, "flyout is on social error page");
+          // Bug 832943 - the listeners previously stopped working after a GC, so
+          // force a GC now and try again.
+          gc();
+          openPanel(
+            manifest.sidebarURL, /* empty html page */
+            function() { // the panel api callback
+              panelCallbackCount++;
+            },
+            function() { // the "load" callback.
+              todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads.");
+              let href = panel.firstChild.contentDocument.documentURI;
+              ok(href.indexOf("about:socialerror?mode=compactInfo")==0, "flyout is on social error page");
+              gc();
+              SocialFlyout.unload();
+              next();
+            }
+          );
+        }
+      );
+    });
+  },
+
+  testChatWindow: function(next) {
+    todo(false, "Bug 1245799 is needed to make error pages work again for chat windows.");
+    next();
+    return;
+
+    let panelCallbackCount = 0;
+    // chatwindow tests throw errors, which muddy test output, if the worker
+    // doesn't get test-init
+    goOffline().then(function() {
+      openChat(
+        manifest.sidebarURL, /* empty html page */
+        function() { // the panel api callback
+          panelCallbackCount++;
+        },
+        function() { // the "load" callback.
+          todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads.");
+          let chat = getChatBar().selectedChat;
+          BrowserTestUtils.waitForCondition(() => chat.content != null && chat.contentDocument.documentURI.indexOf("about:socialerror?mode=tryAgainOnly")==0,
+                           "error page didn't appear").then(() => {
+                              chat.close();
+                              next();
+                            });
+        }
+      );
+    });
+  },
+
+  testChatWindowAfterTearOff: function(next) {
+    todo(false, "Bug 1245799 is needed to make error pages work again for chat windows.");
+    next();
+    return;
+
+    // Ensure that the error listener survives the chat window being detached.
+    let url = manifest.sidebarURL; /* empty html page */
+    let panelCallbackCount = 0;
+    // chatwindow tests throw errors, which muddy test output, if the worker
+    // doesn't get test-init
+    // open a chat while we are still online.
+    openChat(
+      url,
+      null,
+      function() { // the "load" callback.
+        let chat = getChatBar().selectedChat;
+        is(chat.contentDocument.documentURI, url, "correct url loaded");
+        // toggle to a detached window.
+        chat.swapWindows().then(chat => {
+          ok(!!chat.content, "we have chat content 1");
+          BrowserTestUtils.waitForCondition(() => chat.content != null && chat.contentDocument.readyState == "complete",
+                                            "swapped window loaded").then(() => {
+            // now go offline and reload the chat - about:socialerror should be loaded.
+            goOffline().then(() => {
+              ok(!!chat.content, "we have chat content 2");
+              chat.contentDocument.location.reload();
+              info("chat reload called");
+              BrowserTestUtils.waitForCondition(() => chat.contentDocument.documentURI.indexOf("about:socialerror?mode=tryAgainOnly")==0,
+                                                "error page didn't appear").then(() => {
+                chat.close();
+                next();
+              });
+            });
+          });
+        });
+      }
+    );
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_flyout.js
@@ -0,0 +1,102 @@
+/* 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() {
+  waitForExplicitFinish();
+  let frameScript = "data:,(" + function frame_script() {
+    addMessageListener("socialTest-CloseSelf", function(e) {
+      content.close();
+    });
+    addMessageListener("socialTest-sendEvent", function(msg) {
+      let data = msg.data;
+      let evt = content.document.createEvent("CustomEvent");
+      evt.initCustomEvent(data.name, true, true, JSON.stringify(data.data));
+      content.document.documentElement.dispatchEvent(evt);
+    });
+
+  }.toString() + ")();";
+  let mm = getGroupMessageManager("social");
+  mm.loadFrameScript(frameScript, true);
+
+  let manifest = { // normal provider
+    name: "provider 1",
+    origin: "https://example.com",
+    sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
+    iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
+  };
+  runSocialTestWithProvider(manifest, function (finishcb) {
+    SocialSidebar.show();
+    ensureFrameLoaded(SocialSidebar.browser, manifest.sidebarURL).then(() => {
+      // disable transitions for the test
+      registerCleanupFunction(function () {
+        SocialFlyout.panel.removeAttribute("animate");
+      });
+      SocialFlyout.panel.setAttribute("animate", "false");
+      runSocialTests(tests, undefined, undefined, finishcb);
+    });
+  });
+}
+
+var tests = {
+  testResizeFlyout: function(next) {
+    let panel = document.getElementById("social-flyout-panel");
+
+    BrowserTestUtils.waitForEvent(panel, "popupshown").then(() => {
+      is(panel.firstChild.contentDocument.readyState, "complete", "panel is loaded prior to showing");
+      // The width of the flyout should be 400px initially
+      let iframe = panel.firstChild;
+      let body = iframe.contentDocument.body;
+      let cs = iframe.contentWindow.getComputedStyle(body);
+
+      is(cs.width, "400px", "should be 400px wide");
+      is(iframe.boxObject.width, 400, "iframe should now be 400px wide");
+      is(cs.height, "400px", "should be 400px high");
+      is(iframe.boxObject.height, 400, "iframe should now be 400px high");
+
+      BrowserTestUtils.waitForEvent(iframe.contentWindow, "resize").then(() => {
+        cs = iframe.contentWindow.getComputedStyle(body);
+
+        is(cs.width, "500px", "should now be 500px wide");
+        is(iframe.boxObject.width, 500, "iframe should now be 500px wide");
+        is(cs.height, "500px", "should now be 500px high");
+        is(iframe.boxObject.height, 500, "iframe should now be 500px high");
+        BrowserTestUtils.waitForEvent(panel, "popuphidden").then(next);
+        panel.hidePopup();
+      });
+      SocialFlyout.dispatchPanelEvent("socialTest-MakeWider");
+    });
+
+    SocialSidebar.browser.messageManager.sendAsyncMessage("socialTest-sendEvent", { name: "test-flyout-open", data: {} });
+  },
+
+  testCloseSelf: function(next) {
+    let panel = document.getElementById("social-flyout-panel");
+    BrowserTestUtils.waitForEvent(panel, "popupshown").then(() => {
+      is(panel.firstChild.contentDocument.readyState, "complete", "panel is loaded prior to showing");
+      BrowserTestUtils.waitForEvent(panel, "popuphidden").then(next);
+      let mm = panel.firstChild.messageManager;
+      mm.sendAsyncMessage("socialTest-CloseSelf", {});
+    });
+    SocialSidebar.browser.messageManager.sendAsyncMessage("socialTest-sendEvent", { name: "test-flyout-open", data: {} });
+  },
+
+  testCloseOnLinkTraversal: function(next) {
+
+    BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen", true).then(event => {
+      BrowserTestUtils.waitForCondition(function() { return panel.state == "closed" },
+                                        "panel should close after tab open").then(() => {
+        BrowserTestUtils.removeTab(event.target).then(next);
+      });
+    });
+
+    let panel = document.getElementById("social-flyout-panel");
+    BrowserTestUtils.waitForEvent(panel, "popupshown").then(() => {
+      is(panel.firstChild.contentDocument.readyState, "complete", "panel is loaded prior to showing");
+      is(panel.state, "open", "flyout should be open");
+      let iframe = panel.firstChild;
+      iframe.contentDocument.getElementById('traversal').click();
+    });
+    SocialSidebar.browser.messageManager.sendAsyncMessage("socialTest-sendEvent", { name: "test-flyout-open", data: {} });
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_isVisible.js
@@ -0,0 +1,51 @@
+/* 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() {
+  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",
+    iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
+  };
+
+  let frameScript = "data:,(" + function frame_script() {
+    addEventListener("visibilitychange", function() {
+      sendAsyncMessage("visibility", content.document.hidden ? "hidden" : "shown");
+    });
+  }.toString() + ")();";
+  let mm = getGroupMessageManager("social");
+  mm.loadFrameScript(frameScript, true);
+
+  registerCleanupFunction(function () {
+    mm.removeDelayedFrameScript(frameScript);
+  });
+
+  runSocialTestWithProvider(manifest, function (finishcb) {
+    runSocialTests(tests, undefined, undefined, finishcb);
+  });
+}
+
+var tests = {
+  testIsVisible: function(next) {
+    let mm = getGroupMessageManager("social");
+    mm.addMessageListener("visibility", function handler(msg) {
+      mm.removeMessageListener("visibility", handler);
+      is(msg.data, "shown", "sidebar is visible");
+      next();
+    });
+    SocialSidebar.show();
+  },
+  testIsNotVisible: function(next) {
+    let mm = getGroupMessageManager("social");
+    mm.addMessageListener("visibility", function handler(msg) {
+      mm.removeMessageListener("visibility", handler);
+      is(msg.data, "hidden", "sidebar is hidden");
+      next();
+    });
+    SocialSidebar.hide();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_marks.js
@@ -0,0 +1,232 @@
+/* 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/. */
+
+var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
+
+var manifest2 = { // used for testing install
+  name: "provider test1",
+  origin: "https://test1.example.com",
+  markURL: "https://test1.example.com/browser/browser/base/content/test/social/social_mark.html?url=%{url}",
+  markedIcon: "https://test1.example.com/browser/browser/base/content/test/social/unchecked.jpg",
+  unmarkedIcon: "https://test1.example.com/browser/browser/base/content/test/social/checked.jpg",
+
+  iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png",
+  version: "1.0"
+};
+var manifest3 = { // used for testing install
+  name: "provider test2",
+  origin: "https://test2.example.com",
+  sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html",
+  iconURL: "https://test2.example.com/browser/browser/base/content/test/general/moz.png",
+  version: "1.0"
+};
+
+function test() {
+  waitForExplicitFinish();
+
+  let frameScript = "data:,(" + function frame_script() {
+    addEventListener("visibilitychange", function() {
+      sendAsyncMessage("visibility", content.document.hidden ? "hidden" : "shown");
+    });
+  }.toString() + ")();";
+  let mm = getGroupMessageManager("social");
+  mm.loadFrameScript(frameScript, true);
+
+  PopupNotifications.panel.setAttribute("animate", "false");
+  registerCleanupFunction(function () {
+    PopupNotifications.panel.removeAttribute("animate");
+    mm.removeDelayedFrameScript(frameScript);
+  });
+
+  runSocialTests(tests, undefined, undefined, finish);
+}
+
+var tests = {
+  testButtonDisabledOnActivate: function(next) {
+    // starting on about:blank page, share should be visible but disabled when
+    // adding provider
+    is(gBrowser.selectedBrowser.currentURI.spec, "about:blank");
+    SocialService.addProvider(manifest2, function(provider) {
+      is(provider.origin, manifest2.origin, "provider is installed");
+      let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin);
+      let widget = CustomizableUI.getWidget(id).forWindow(window)
+      ok(widget.node, "button added to widget set");
+
+      // bypass widget go directly to dom, check attribute states
+      let button = document.getElementById(id);
+      is(button.disabled, true, "mark button is disabled");
+      // verify the attribute for proper css
+      is(button.getAttribute("disabled"), "true", "mark button attribute is disabled");
+      // button should be visible
+      is(button.hidden, false, "mark button is visible");
+
+      checkSocialUI(window);
+      SocialService.disableProvider(manifest2.origin, next);
+    });
+  },
+  testNoButtonOnEnable: function(next) {
+    // we expect the addon install dialog to appear, we need to accept the
+    // install from the dialog.
+    let panel = document.getElementById("servicesInstall-notification");
+    BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => {
+      info("servicesInstall-notification panel opened");
+      panel.button.click();
+    });
+
+    let activationURL = manifest3.origin + "/browser/browser/base/content/test/social/social_activate.html"
+    BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => {
+      let doc = tab.linkedBrowser.contentDocument;
+      let data = {
+        origin: doc.nodePrincipal.origin,
+        url: doc.location.href,
+        manifest: manifest3,
+        window: window
+      }
+
+      Social.installProvider(data, function(addonManifest) {
+        // enable the provider so we know the button would have appeared
+        SocialService.enableProvider(manifest3.origin, function(provider) {
+          is(provider.origin, manifest3.origin, "provider is installed");
+          let id = SocialMarks._toolbarHelper.idFromOrigin(provider.origin);
+          let widget = CustomizableUI.getWidget(id);
+          ok(!widget || !widget.forWindow(window).node, "no button added to widget set");
+          Social.uninstallProvider(manifest3.origin, function() {
+            BrowserTestUtils.removeTab(tab).then(next);
+          });
+        });
+      });
+    });
+  },
+
+  testButtonOnEnable: function(next) {
+    let panel = document.getElementById("servicesInstall-notification");
+    BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => {
+      info("servicesInstall-notification panel opened");
+      panel.button.click();
+    });
+
+    // enable the provider now
+    let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html"
+    BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => {
+      let doc = tab.linkedBrowser.contentDocument;
+      let data = {
+        origin: doc.nodePrincipal.origin,
+        url: doc.location.href,
+        manifest: manifest2,
+        window: window
+      }
+
+      Social.installProvider(data, function(addonManifest) {
+        SocialService.enableProvider(manifest2.origin, function(provider) {
+          is(provider.origin, manifest2.origin, "provider is installed");
+          let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin);
+          let widget = CustomizableUI.getWidget(id).forWindow(window)
+          ok(widget.node, "button added to widget set");
+
+          // bypass widget go directly to dom, check attribute states
+          let button = document.getElementById(id);
+          is(button.disabled, false, "mark button is disabled");
+          // verify the attribute for proper css
+          ok(!button.hasAttribute("disabled"), "mark button attribute is disabled");
+          // button should be visible
+          is(button.hidden, false, "mark button is visible");
+
+          checkSocialUI(window);
+          BrowserTestUtils.removeTab(tab).then(next);
+        });
+      });
+    });
+  },
+
+  testMarkPanel: function(next) {
+    // click on panel to open and wait for visibility
+    let provider = Social._getProviderFromOrigin(manifest2.origin);
+    ok(provider.enabled, "provider is enabled");
+    let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin);
+    let widget = CustomizableUI.getWidget(id);
+    let btn = widget.forWindow(window).node;
+    ok(btn, "got a mark button");
+    let ourTab;
+
+    BrowserTestUtils.waitForEvent(btn.panel, "popupshown").then(() => {
+      info("marks panel shown");
+      let doc = btn.contentDocument;
+      let unmarkBtn = doc.getElementById("unmark");
+      ok(unmarkBtn, "testMarkPanel - got the panel unmark button");
+      EventUtils.sendMouseEvent({type: "click"}, unmarkBtn, btn.contentWindow);
+    });
+
+    BrowserTestUtils.waitForEvent(btn.panel, "popuphidden").then(() => {
+      BrowserTestUtils.removeTab(ourTab).then(() => {
+        ok(btn.disabled, "button is disabled");
+        next();
+      });
+    });
+
+    // verify markbutton is disabled when there is no browser url
+    ok(btn.disabled, "button is disabled");
+    let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html"
+    BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => {
+      ourTab = tab;
+      ok(!btn.disabled, "button is enabled");
+      // first click marks the page, second click opens the page. We have to
+      // synthesize so the command event happens
+      EventUtils.synthesizeMouseAtCenter(btn, {});
+      // wait for the button to be marked, click to open panel
+      is(btn.panel.state, "closed", "panel should not be visible yet");
+      BrowserTestUtils.waitForCondition(() => btn.isMarked, "button is marked").then(() => {
+        EventUtils.synthesizeMouseAtCenter(btn, {});
+      });
+    });
+  },
+
+  testMarkPanelOffline: function(next) {
+    // click on panel to open and wait for visibility
+    let provider = Social._getProviderFromOrigin(manifest2.origin);
+    ok(provider.enabled, "provider is enabled");
+    let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin);
+    let widget = CustomizableUI.getWidget(id);
+    let btn = widget.forWindow(window).node;
+    ok(btn, "got a mark button");
+
+    // verify markbutton is disabled when there is no browser url
+    ok(btn.disabled, "button is disabled");
+    let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html";
+    BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => {
+      ok(!btn.disabled, "button is enabled");
+      goOffline().then(function() {
+        info("testing offline error page");
+        // wait for popupshown
+        BrowserTestUtils.waitForEvent(btn.panel, "popupshown").then(() => {
+          info("marks panel is open");
+          ensureFrameLoaded(btn.content).then(() => {
+            is(btn.contentDocument.documentURI.indexOf("about:socialerror?mode=tryAgainOnly"), 0, "social error page is showing "+btn.contentDocument.documentURI);
+            // cleanup after the page has been unmarked
+            BrowserTestUtils.removeTab(tab).then(() => {
+              ok(btn.disabled, "button is disabled");
+              goOnline().then(next);
+            });
+          });
+        });
+        btn.markCurrentPage();
+      });
+    });
+  },
+
+  testButtonOnDisable: function(next) {
+    // enable the provider now
+    let provider = Social._getProviderFromOrigin(manifest2.origin);
+    ok(provider, "provider is installed");
+    SocialService.disableProvider(manifest2.origin, function() {
+      let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin);
+      BrowserTestUtils.waitForCondition(() => {
+                        // getWidget now returns null since we've destroyed the widget
+                        return !CustomizableUI.getWidget(id)
+                       }, "button does not exist after disabling the provider").then(() => {
+                         checkSocialUI(window);
+                         Social.uninstallProvider(manifest2.origin, next);
+                       });
+    });
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_marks_context.js
@@ -0,0 +1,106 @@
+var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
+
+function makeMarkProvider(origin) {
+  return { // used for testing install
+    name: "mark provider " + origin,
+    origin: "https://" + origin + ".example.com",
+    markURL: "https://" + origin + ".example.com/browser/browser/base/content/test/social/social_mark.html?url=%{url}",
+    markedIcon: "https://" + origin + ".example.com/browser/browser/base/content/test/social/unchecked.jpg",
+    unmarkedIcon: "https://" + origin + ".example.com/browser/browser/base/content/test/social/checked.jpg",
+    iconURL: "https://" + origin + ".example.com/browser/browser/base/content/test/general/moz.png",
+    version: "1.0"
+  }
+}
+
+function test() {
+  waitForExplicitFinish();
+  PopupNotifications.panel.setAttribute("animate", "false");
+  registerCleanupFunction(function () {
+    PopupNotifications.panel.removeAttribute("animate");
+  });
+
+  runSocialTests(tests, undefined, undefined, finish);
+}
+
+var tests = {
+  testContextSubmenu: function(next) {
+    // install 4 providers to test that the menu's are added as submenus
+    let manifests = [
+      makeMarkProvider("sub1.test1"),
+      makeMarkProvider("sub2.test1"),
+      makeMarkProvider("sub1.test2"),
+      makeMarkProvider("sub2.test2")
+    ];
+    let installed = [];
+    let markLinkMenu = document.getElementById("context-marklinkMenu").firstChild;
+    let markPageMenu = document.getElementById("context-markpageMenu").firstChild;
+
+    function addProviders(callback) {
+      let manifest = manifests.pop();
+      if (!manifest) {
+        info("INSTALLATION FINISHED");
+        executeSoon(callback);
+        return;
+      }
+      info("INSTALLING " + manifest.origin);
+      let panel = document.getElementById("servicesInstall-notification");
+      BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => {
+        info("servicesInstall-notification panel opened");
+        panel.button.click();
+      });
+
+      let activationURL = manifest.origin + "/browser/browser/base/content/test/social/social_activate.html"
+      let id = SocialMarks._toolbarHelper.idFromOrigin(manifest.origin);
+      let toolbar = document.getElementById("nav-bar");
+      BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => {
+        let doc = tab.linkedBrowser.contentDocument;
+        let data = {
+          origin: doc.nodePrincipal.origin,
+          url: doc.location.href,
+          manifest: manifest,
+          window: window
+        }
+
+        Social.installProvider(data, function(addonManifest) {
+          // enable the provider so we know the button would have appeared
+          SocialService.enableProvider(manifest.origin, function(provider) {
+            BrowserTestUtils.waitForCondition(() => { return CustomizableUI.getWidget(id) },
+                             "button exists after enabling social").then(() => {
+              BrowserTestUtils.removeTab(tab).then(() => {
+                installed.push(manifest.origin);
+                // checkSocialUI will properly check where the menus are located
+                checkSocialUI(window);
+                executeSoon(function() {
+                  addProviders(callback);
+                });
+              });
+            });
+          });
+        });
+      });
+    }
+
+    function removeProviders(callback) {
+      let origin = installed.pop();
+      if (!origin) {
+        executeSoon(callback);
+        return;
+      }
+      Social.uninstallProvider(origin, function(provider) {
+        executeSoon(function() {
+          removeProviders(callback);
+        });
+      });
+    }
+
+    addProviders(function() {
+      removeProviders(function() {
+        is(SocialMarks.getProviders().length, 0, "mark providers removed");
+        is(markLinkMenu.childNodes.length, 0, "marklink menu ok");
+        is(markPageMenu.childNodes.length, 0, "markpage menu ok");
+        checkSocialUI(window);
+        next();
+      });
+    });
+  }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_multiprovider.js
@@ -0,0 +1,78 @@
+/* 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() {
+  waitForExplicitFinish();
+  runSocialTestWithProvider(gProviders, function (finishcb) {
+    SocialSidebar.provider = Social.providers[0];
+    SocialSidebar.show();
+    is(Social.providers[0].origin, SocialSidebar.provider.origin, "selected provider in sidebar");
+    runSocialTests(tests, undefined, undefined, finishcb);
+  });
+}
+
+var gProviders = [
+  {
+    name: "provider 1",
+    origin: "https://test1.example.com",
+    sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html?provider1",
+    iconURL: "chrome://branding/content/icon48.png"
+  },
+  {
+    name: "provider 2",
+    origin: "https://test2.example.com",
+    sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html?provider2",
+    iconURL: "chrome://branding/content/icon48.png"
+  }
+];
+
+var tests = {
+  testProviderSwitch: function(next) {
+    let sbrowser = document.getElementById("social-sidebar-browser");
+    let menu = document.getElementById("social-statusarea-popup");
+    let button = document.getElementById("social-sidebar-button");
+    function checkProviderMenu(selectedProvider) {
+      let menuProviders = menu.querySelectorAll(".social-provider-menuitem");
+      is(menuProviders.length, gProviders.length, "correct number of providers listed in the menu");
+      // Find the selectedProvider's menu item
+      let el = menu.getElementsByAttribute("origin", selectedProvider.origin);
+      is(el.length, 1, "selected provider menu item exists");
+      is(el[0].getAttribute("checked"), "true", "selected provider menu item is checked");
+    }
+
+    // the menu is not populated until onpopupshowing, so wait for popupshown
+    BrowserTestUtils.waitForEvent(menu, "popupshown", true).then(()=>{
+      menu.hidePopup(); // doesn't need visibility
+      // first provider should already be visible in the sidebar
+      is(Social.providers[0].origin, SocialSidebar.provider.origin, "selected provider in sidebar");
+      checkProviderMenu(Social.providers[0]);
+
+      // Now activate "provider 2"
+      BrowserTestUtils.waitForEvent(sbrowser, "load", true).then(()=>{
+        checkUIStateMatchesProvider(Social.providers[1]);
+
+        BrowserTestUtils.waitForEvent(sbrowser, "load", true).then(()=>{
+          checkUIStateMatchesProvider(Social.providers[0]);
+          next();
+        });
+
+        // show the menu again so the menu is updated with the correct commands
+        BrowserTestUtils.waitForEvent(menu, "popupshown", true).then(()=>{
+          // click on the provider menuitem to switch providers
+          let el = menu.getElementsByAttribute("origin", Social.providers[0].origin);
+          is(el.length, 1, "selected provider menu item exists");
+          EventUtils.synthesizeMouseAtCenter(el[0], {});
+        });
+        EventUtils.synthesizeMouseAtCenter(button, {});
+      });
+      SocialSidebar.provider = Social.providers[1];
+    });
+    EventUtils.synthesizeMouseAtCenter(button, {});
+  }
+}
+
+function checkUIStateMatchesProvider(provider) {
+  // Sidebar
+  is(document.getElementById("social-sidebar-browser").getAttribute("src"), provider.sidebarURL, "side bar URL is set");
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_sidebar.js
@@ -0,0 +1,98 @@
+/* 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/. */
+
+var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
+
+var manifest = { // normal provider
+  name: "provider 1",
+  origin: "https://example.com",
+  sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
+  iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
+};
+
+function test() {
+  waitForExplicitFinish();
+
+  let frameScript = "data:,(" + function frame_script() {
+    addEventListener("visibilitychange", function() {
+      sendAsyncMessage("visibility", content.document.hidden ? "hidden" : "shown");
+    });
+  }.toString() + ")();";
+  let mm = getGroupMessageManager("social");
+  mm.loadFrameScript(frameScript, true);
+
+  registerCleanupFunction(function () {
+    mm.removeDelayedFrameScript(frameScript);
+  });
+
+  SocialService.addProvider(manifest, function() {
+    // the test will remove the provider
+    doTest();
+  });
+}
+
+function doTest() {
+  ok(SocialSidebar.canShow, "social sidebar should be able to be shown");
+  ok(!SocialSidebar.opened, "social sidebar should not be open by default");
+
+  let command = document.getElementById("Social:ToggleSidebar");
+  let sidebar = document.getElementById("social-sidebar-box");
+  let browser = sidebar.lastChild;
+  ok(!browser.docShellIsActive, "sidebar is not active");
+  is(sidebar.hidden, true, "sidebar should be hidden");
+  is(command.getAttribute("checked"), "false", "toggle command should be unchecked");
+
+  function checkShown(shouldBeShown) {
+    is(command.getAttribute("checked"), shouldBeShown ? "true" : "false",
+       "toggle command should be " + (shouldBeShown ? "checked" : "unchecked"));
+    is(sidebar.hidden, !shouldBeShown,
+       "sidebar should be " + (shouldBeShown ? "visible" : "hidden"));
+    is(browser.docShellIsActive, shouldBeShown, "sidebar isActive in correct state");
+    if (shouldBeShown) {
+      is(browser.getAttribute('src'), SocialSidebar.provider.sidebarURL, "sidebar url should be set");
+      // We don't currently check docShellIsActive as this is only set
+      // after load event fires, and the tests below explicitly wait for this
+      // anyway.
+    }
+    else {
+      ok(!browser.docShellIsActive, "sidebar should have an inactive docshell");
+      // sidebar will only be immediately unloaded (and thus set to
+      // about:blank) when canShow is false.
+      if (SocialSidebar.canShow) {
+        // should not have unloaded so will still be the provider URL.
+        is(browser.getAttribute('src'), SocialSidebar.provider.sidebarURL, "sidebar url should be set");
+      } else {
+        // should have been an immediate unload.
+        is(browser.getAttribute('src'), "about:blank", "sidebar url should be blank");
+      }
+    }
+  }
+  ensureFrameLoaded(browser).then(() => {
+    // First check the the sidebar is initially visible, and loaded
+    ok(!command.hidden, "toggle command should be visible");
+    let mm = getGroupMessageManager("social");
+    mm.addMessageListener("visibility", function shown(msg) {
+      if (msg.data == "shown") {
+        mm.removeMessageListener("visibility", shown);
+        checkShown(true);
+        info("Toggling sidebar to closed");
+        SocialSidebar.toggleSidebar();
+      }
+    });
+    mm.addMessageListener("visibility", function handler(msg) {
+      if (msg.data == "hidden") {
+        mm.removeMessageListener("visibility", handler);
+        // disable social.
+        SocialService.disableProvider(SocialSidebar.provider.origin, function() {
+          checkShown(false);
+          is(Social.providers.length, 0, "no providers left");
+          defaultFinishChecks();
+          // Finish the test
+          executeSoon(finish);
+        });
+      }
+    });
+  });
+  SocialSidebar.show();
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_status.js
@@ -0,0 +1,216 @@
+/* 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/. */
+
+var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
+
+var manifest = { // builtin provider
+  name: "provider example.com",
+  origin: "https://example.com",
+  sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
+  iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
+};
+var manifest2 = { // used for testing install
+  name: "provider test1",
+  origin: "https://test1.example.com",
+  statusURL: "https://test1.example.com/browser/browser/base/content/test/social/social_panel.html",
+  iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png",
+  version: "1.0"
+};
+var manifest3 = { // used for testing install
+  name: "provider test2",
+  origin: "https://test2.example.com",
+  sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html",
+  iconURL: "https://test2.example.com/browser/browser/base/content/test/general/moz.png",
+  version: "1.0"
+};
+
+
+function openWindowAndWaitForInit(callback) {
+  let topic = "browser-delayed-startup-finished";
+  let w = OpenBrowserWindow();
+  Services.obs.addObserver(function providerSet(subject, topic, data) {
+    Services.obs.removeObserver(providerSet, topic);
+    executeSoon(() => callback(w));
+  }, topic, false);
+}
+
+function test() {
+  waitForExplicitFinish();
+
+  let frameScript = "data:,(" + function frame_script() {
+    addMessageListener("socialTest-sendEvent", function(msg) {
+      let data = msg.data;
+      let evt = content.document.createEvent("CustomEvent");
+      evt.initCustomEvent(data.name, true, true, JSON.stringify(data.data));
+      content.document.documentElement.dispatchEvent(evt);
+    });
+  }.toString() + ")();";
+  let mm = getGroupMessageManager("social");
+  mm.loadFrameScript(frameScript, true);
+
+  PopupNotifications.panel.setAttribute("animate", "false");
+  registerCleanupFunction(function () {
+    PopupNotifications.panel.removeAttribute("animate");
+    mm.removeDelayedFrameScript(frameScript);
+  });
+
+  runSocialTestWithProvider(manifest, function (finishcb) {
+    runSocialTests(tests, undefined, undefined, function () {
+      Services.prefs.clearUserPref("social.remote-install.enabled");
+      // just in case the tests failed, clear these here as well
+      Services.prefs.clearUserPref("social.whitelist");
+      CustomizableUI.reset();
+      finishcb();
+    });
+  });
+}
+
+var tests = {
+  testNoButtonOnEnable: function(next) {
+    // we expect the addon install dialog to appear, we need to accept the
+    // install from the dialog.
+    let panel = document.getElementById("servicesInstall-notification");
+    BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => {
+      info("servicesInstall-notification panel opened");
+      panel.button.click();
+    })
+
+    let activationURL = manifest3.origin + "/browser/browser/base/content/test/social/social_activate.html"
+    BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => {
+      let doc = tab.linkedBrowser.contentDocument;
+      let data = {
+        origin: doc.nodePrincipal.origin,
+        url: doc.location.href,
+        manifest: manifest3,
+        window: window
+      }
+      Social.installProvider(data, function(addonManifest) {
+        // enable the provider so we know the button would have appeared
+        SocialService.enableProvider(manifest3.origin, function(provider) {
+          is(provider.origin, manifest3.origin, "provider is installed");
+          let id = SocialStatus._toolbarHelper.idFromOrigin(provider.origin);
+          let widget = CustomizableUI.getWidget(id);
+          ok(!widget || !widget.forWindow(window).node, "no button added to widget set");
+          Social.uninstallProvider(manifest3.origin, function() {
+            BrowserTestUtils.removeTab(tab).then(next);
+          });
+        });
+      });
+    });
+  },
+  testButtonOnEnable: function(next) {
+    let panel = document.getElementById("servicesInstall-notification");
+    BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => {
+      info("servicesInstall-notification panel opened");
+      panel.button.click();
+    });
+
+    // enable the provider now
+    let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html"
+    BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => {
+      let doc = tab.linkedBrowser.contentDocument;
+      let data = {
+        origin: doc.nodePrincipal.origin,
+        url: doc.location.href,
+        manifest: manifest2,
+        window: window
+      }
+
+      Social.installProvider(data, function(addonManifest) {
+        SocialService.enableProvider(manifest2.origin, function(provider) {
+          is(provider.origin, manifest2.origin, "provider is installed");
+          let id = SocialStatus._toolbarHelper.idFromOrigin(manifest2.origin);
+          let widget = CustomizableUI.getWidget(id).forWindow(window);
+          ok(widget.node, "button added to widget set");
+          checkSocialUI(window);
+          BrowserTestUtils.removeTab(tab).then(next);
+        });
+      });
+    });
+  },
+  testStatusPanel: function(next) {
+    let icon = {
+      name: "testIcon",
+      iconURL: "chrome://browser/skin/Info.png",
+      counter: 1
+    };
+
+    // click on panel to open and wait for visibility
+    let provider = Social._getProviderFromOrigin(manifest2.origin);
+    let id = SocialStatus._toolbarHelper.idFromOrigin(manifest2.origin);
+    let widget = CustomizableUI.getWidget(id);
+    let btn = widget.forWindow(window).node;
+
+    // Disable the transition
+    let panel = document.getElementById("social-notification-panel");
+    panel.setAttribute("animate", "false");
+    BrowserTestUtils.waitForEvent(panel, "popupshown").then(() => {
+      ensureFrameLoaded(panel.firstChild).then(() => {
+        let mm = panel.firstChild.messageManager;
+        mm.sendAsyncMessage("socialTest-sendEvent", { name: "Social:Notification", data: icon });
+        BrowserTestUtils.waitForCondition(
+          () => { return btn.getAttribute("badge"); }, "button updated by notification").then(() => {
+            is(btn.style.listStyleImage, "url(\"" + icon.iconURL + "\")", "notification icon updated");
+            panel.hidePopup();
+          });
+        });
+    });
+    BrowserTestUtils.waitForEvent(panel, "popuphidden").then(() => {
+      panel.removeAttribute("animate");
+      next();
+    });
+    btn.click(); // open the panel
+  },
+
+  testPanelOffline: function(next) {
+    // click on panel to open and wait for visibility
+    let provider = Social._getProviderFromOrigin(manifest2.origin);
+    ok(provider.enabled, "provider is enabled");
+    let id = SocialStatus._toolbarHelper.idFromOrigin(manifest2.origin);
+    let widget = CustomizableUI.getWidget(id);
+    let btn = widget.forWindow(window).node;
+    ok(btn, "got a status button");
+    let frameId = btn.getAttribute("notificationFrameId");
+    let frame = document.getElementById(frameId);
+
+    goOffline().then(function() {
+      info("testing offline error page");
+      // wait for popupshown
+      let panel = document.getElementById("social-notification-panel");
+      BrowserTestUtils.waitForEvent(panel, "popupshown").then(() => {
+        ensureFrameLoaded(frame).then(() => {
+          is(frame.contentDocument.documentURI.indexOf("about:socialerror?mode=tryAgainOnly"), 0, "social error page is showing "+frame.contentDocument.documentURI);
+          // We got our error page, reset to avoid test leak.
+          BrowserTestUtils.waitForEvent(frame, "load", true).then(() => {
+            is(frame.contentDocument.documentURI, "about:blank", "closing error panel");
+            BrowserTestUtils.waitForEvent(panel, "popuphidden").then(next);
+            panel.hidePopup();
+          });
+          goOnline().then(() => {
+            info("resetting error panel");
+            frame.setAttribute("src", "about:blank");
+          });
+        });
+      });
+      // reload after going offline, wait for unload to open panel
+      BrowserTestUtils.waitForEvent(frame, "unload", true).then(() => {
+        btn.click();
+      });
+      frame.contentDocument.location.reload();
+    });
+  },
+
+  testButtonOnDisable: function(next) {
+    // enable the provider now
+    let provider = Social._getProviderFromOrigin(manifest2.origin);
+    ok(provider, "provider is installed");
+    SocialService.disableProvider(manifest2.origin, function() {
+      let id = SocialStatus._toolbarHelper.idFromOrigin(manifest2.origin);
+      BrowserTestUtils.waitForCondition(() => { return !document.getElementById(id) },
+                                        "button does not exist after disabling the provider").then(() => {
+        Social.uninstallProvider(manifest2.origin, next);
+       });
+    });
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_window.js
@@ -0,0 +1,251 @@
+// 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/.
+
+// Test the top-level window UI for social.
+
+var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
+
+// This function should "reset" Social such that the next time Social.init()
+// is called (eg, when a new window is opened), it re-performs all
+// initialization.
+function resetSocial() {
+  Social.initialized = false;
+  Social.providers = [];
+  // *sob* - listeners keep getting added...
+  SocialService._providerListeners.clear();
+}
+
+var createdWindows = [];
+
+function openWindowAndWaitForInit(parentWin, callback) {
+  // this notification tells us SocialUI.init() has been run...
+  let topic = "browser-delayed-startup-finished";
+  let w = parentWin.OpenBrowserWindow();
+  createdWindows.push(w);
+  Services.obs.addObserver(function providerSet(subject, topic, data) {
+    Services.obs.removeObserver(providerSet, topic);
+    info(topic + " observer was notified - continuing test");
+    executeSoon(() => callback(w));
+  }, topic, false);
+}
+
+function closeWindow(w, cb) {
+  waitForNotification("domwindowclosed", cb);
+  w.close();
+}
+
+function closeOneWindow(cb) {
+  let w = createdWindows.pop();
+  if (!w || w.closed) {
+    cb();
+    return;
+  }
+  closeWindow(w, function() {
+    closeOneWindow(cb);
+  });
+  w.close();
+}
+
+function postTestCleanup(cb) {
+  closeOneWindow(cb);
+}
+
+var manifest = { // normal provider
+  name: "provider 1",
+  origin: "https://example.com",
+  sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
+  iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
+};
+var manifest2 = { // used for testing install
+  name: "provider test1",
+  origin: "https://test1.example.com",
+  sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html",
+  iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png",
+};
+
+function test() {
+  waitForExplicitFinish();
+  requestLongerTimeout(2);
+  runSocialTests(tests, undefined, postTestCleanup);
+}
+
+var tests = {
+  // check when social is totally disabled at startup (ie, no providers enabled)
+  testInactiveStartup: function(cbnext) {
+    is(Social.providers.length, 0, "needs zero providers to start this test.");
+    ok(!SocialService.hasEnabledProviders, "no providers are enabled");
+    resetSocial();
+    openWindowAndWaitForInit(window, function(w1) {
+      checkSocialUI(w1);
+      // Now social is (re-)initialized, open a secondary window and check that.
+      openWindowAndWaitForInit(window, function(w2) {
+        checkSocialUI(w2);
+        checkSocialUI(w1);
+        cbnext();
+      });
+    });
+  },
+
+  // Check when providers are enabled and social is turned on at startup.
+  testEnabledStartup: function(cbnext) {
+    setManifestPref("social.manifest.test", manifest);
+    ok(!SocialSidebar.opened, "sidebar is closed initially");
+    SocialService.addProvider(manifest, function() {
+      SocialService.addProvider(manifest2, function (provider) {
+        SocialSidebar.show();
+        BrowserTestUtils.waitForCondition(
+          () => SocialSidebar.opened, "sidebar did not open").then(() => {
+          ok(SocialSidebar.opened, "first window sidebar is open");
+          openWindowAndWaitForInit(window, function(w1) {
+            ok(w1.SocialSidebar.opened, "new window sidebar is open");
+            ok(SocialService.hasEnabledProviders, "providers are enabled");
+            checkSocialUI(w1);
+            // now init is complete, open a second window
+            openWindowAndWaitForInit(window, function(w2) {
+              ok(w1.SocialSidebar.opened, "w1 sidebar is open");
+              ok(w2.SocialSidebar.opened, "w2 sidebar is open");
+              checkSocialUI(w2);
+              checkSocialUI(w1);
+
+              // disable social and re-check
+              SocialService.disableProvider(manifest.origin, function() {
+                SocialService.disableProvider(manifest2.origin, function() {
+                  ok(!Social.enabled, "social is disabled");
+                  is(Social.providers.length, 0, "no providers");
+                  ok(!w1.SocialSidebar.opened, "w1 sidebar is closed");
+                  ok(!w2.SocialSidebar.opened, "w2 sidebar is closed");
+                  checkSocialUI(w2);
+                  checkSocialUI(w1);
+                  Services.prefs.clearUserPref("social.manifest.test");
+                  cbnext();
+                });
+              });
+            });
+          });
+        });
+      }, cbnext);
+    }, cbnext);
+  },
+
+  testGlobalState: function(cbnext) {
+    setManifestPref("social.manifest.test", manifest);
+    ok(!SocialSidebar.opened, "sidebar is closed initially");
+    ok(!Services.prefs.prefHasUserValue("social.sidebar.provider"), "global state unset");
+    // mimick no session state in opener so we exercise the global state via pref
+    SessionStore.deleteWindowValue(window, "socialSidebar");
+    ok(!SessionStore.getWindowValue(window, "socialSidebar"), "window state unset");
+    SocialService.addProvider(manifest, function() {
+      openWindowAndWaitForInit(window, function(w1) {
+        w1.SocialSidebar.show();
+        BrowserTestUtils.waitForCondition(() => w1.SocialSidebar.opened, "sidebar opened").then(() => {
+          ok(Services.prefs.prefHasUserValue("social.sidebar.provider"), "global state set");
+          ok(!SocialSidebar.opened, "1. main sidebar is still closed");
+          ok(w1.SocialSidebar.opened, "1. window sidebar is open");
+          closeWindow(w1, function() {
+            // this time, the global state should cause the sidebar to be opened
+            // in the new window
+            openWindowAndWaitForInit(window, function(w1) {
+              ok(!SocialSidebar.opened, "2. main sidebar is still closed");
+              ok(w1.SocialSidebar.opened, "2. window sidebar is open");
+              w1.SocialSidebar.hide();
+              ok(!w1.SocialSidebar.opened, "2. window sidebar is closed");
+              ok(!Services.prefs.prefHasUserValue("social.sidebar.provider"), "2. global state unset");
+              // global state should now be no sidebar gets opened on new window
+              closeWindow(w1, function() {
+                ok(!Services.prefs.prefHasUserValue("social.sidebar.provider"), "3. global state unset");
+                ok(!SocialSidebar.opened, "3. main sidebar is still closed");
+                openWindowAndWaitForInit(window, function(w1) {
+                  ok(!Services.prefs.prefHasUserValue("social.sidebar.provider"), "4. global state unset");
+                  ok(!SocialSidebar.opened, "4. main sidebar is still closed");
+                  ok(!w1.SocialSidebar.opened, "4. window sidebar is closed");
+                  SocialService.disableProvider(manifest.origin, function() {
+                    Services.prefs.clearUserPref("social.manifest.test");
+                    cbnext();
+                  });
+                });
+              });
+            });
+          });
+        });        
+      });
+    });
+  },
+
+  // Check per window sidebar functionality, including migration from using
+  // prefs to using session state, and state inheritance of windows (new windows
+  // inherit state from the opener).
+  testPerWindowSidebar: function(cbnext) {
+    function finishCheck() {
+      // disable social and re-check
+      SocialService.disableProvider(manifest.origin, function() {
+        SocialService.disableProvider(manifest2.origin, function() {
+          ok(!Social.enabled, "social is disabled");
+          is(Social.providers.length, 0, "no providers");
+          Services.prefs.clearUserPref("social.manifest.test");
+          cbnext();
+        });
+      });
+    }
+
+    setManifestPref("social.manifest.test", manifest);
+    ok(!SocialSidebar.opened, "sidebar is closed initially");
+    SocialService.addProvider(manifest, function() {
+      SocialService.addProvider(manifest2, function (provider) {
+        // test the migration of the social.sidebar.open pref. We'll set a user
+        // level pref to indicate it was open (along with the old
+        // social.provider.current pref), then we'll open a window. During the
+        // restoreState of the window, those prefs should be migrated, and the
+        // sidebar should be opened.  Both prefs are then removed.
+        Services.prefs.setCharPref("social.provider.current", "https://example.com");
+        Services.prefs.setBoolPref("social.sidebar.open", true);
+
+        openWindowAndWaitForInit(window, function(w1) {
+          ok(w1.SocialSidebar.opened, "new window sidebar is open");
+          ok(SocialService.hasEnabledProviders, "providers are enabled");
+          ok(!Services.prefs.prefHasUserValue("social.provider.current"), "social.provider.current pref removed");
+          ok(!Services.prefs.prefHasUserValue("social.sidebar.open"), "social.sidebar.open pref removed");
+          checkSocialUI(w1);
+          // now init is complete, open a second window, it's state should be the same as the opener
+          openWindowAndWaitForInit(w1, function(w2) {
+            ok(w1.SocialSidebar.opened, "w1 sidebar is open");
+            ok(w2.SocialSidebar.opened, "w2 sidebar is open");
+            checkSocialUI(w2);
+            checkSocialUI(w1);
+
+            // change the sidebar in w2
+            w2.SocialSidebar.show(manifest2.origin);
+            let sbrowser1 = w1.document.getElementById("social-sidebar-browser");
+            is(manifest.origin, sbrowser1.getAttribute("origin"), "w1 sidebar origin matches");
+            let sbrowser2 = w2.document.getElementById("social-sidebar-browser");
+            is(manifest2.origin, sbrowser2.getAttribute("origin"), "w2 sidebar origin matches");
+
+            // hide sidebar, w1 sidebar should still be open
+            w2.SocialSidebar.hide();
+            ok(w1.SocialSidebar.opened, "w1 sidebar is opened");
+            ok(!w2.SocialSidebar.opened, "w2 sidebar is closed");
+            ok(sbrowser2.parentNode.hidden, "w2 sidebar is hidden");
+
+            // open a 3rd window from w2, it should inherit the state of w2
+            openWindowAndWaitForInit(w2, function(w3) {
+              // since the sidebar is not open, we need to ensure the provider
+              // is selected to test we inherited the provider from the opener
+              w3.SocialSidebar.ensureProvider();
+              is(w3.SocialSidebar.provider, w2.SocialSidebar.provider, "w3 has same provider as w2");
+              ok(!w3.SocialSidebar.opened, "w2 sidebar is closed");
+
+              // open a 4th window from w1, it should inherit the state of w1
+              openWindowAndWaitForInit(w1, function(w4) {
+                is(w4.SocialSidebar.provider, w1.SocialSidebar.provider, "w4 has same provider as w1");
+                ok(w4.SocialSidebar.opened, "w4 sidebar is opened");
+
+                finishCheck();
+              });
+            });
+
+          });
+        });
+      }, cbnext);
+    }, cbnext);
+  }
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4cbbe18e6125b6591a6a5eb38b036e38b97b235a
GIT binary patch
literal 785
zc$@(d1Md8ZP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004XF*Lt006JZ
zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!qe(<TRCwBA
z{Qv(y10?_;fLL%zw7b|zDSi7iUFO#hga1tb{=Z;myLwlb^Tq$qFOz`MTYz|a#qvkn
z00M{s<N|E?r5yLaUonCI|D8Ym|KFxH|Nqz-{=aUi_y70z_y2ddnEh{$klhSY2M|Ck
z?RFMS`L8dmVfgpYhvDCUR))Vov;Y2Q5U!oTz-;frz`!Q}6kuXt?wQ8$^3_cS?xWKg
zUcUPQa^!jt0T4heY=8f7GW}%|<*1#)%D@ct(eFPD3^K9|3<5xn5AQQDNJ=v>T)V`;
z@ckRZ^-BvFRDz=zUcdbe(z^ph00a<=;QwFJOuv}<!5-hfiGcwWsCtGB4AOE849cnu
z3}3%60A0uM@#gJcqTCE@-{0M5`0?`(1JM7^Kw1C-h)LwncP%D%ZUK-p8JL(E7+BdE
z7#JBD7}(et7&ti@7(RUhdg(92b9v?OBEq~3EFAnOfejEqOftW|m@*es@H0Gq0Mx+2
zz`(%{F^G*7=nWx=K|pW(VqxVM6BA*0ar-1l-&L3a00G1z_xp=3BNsO}!_k8bK({e4
z7#K4!D5)?oh=?)#J$B&Vx9wYhvj6_c`kr5qMa-Ik;pLkz@Bjn>fB<6R`S)M^|L<SS
z48Fk(455(>|BcNUeogIRcpKyM|I+$37dS1glimmkFD}})gW=zwzYMS5d<5y+4Kn~B
zfLOK&$X(K!+@vG^?F;8Wpujsuj^CH~#m<J_|7;};RkSEl?sb-*SRxY(GsD|=pFjb4
z2xb640I`G|zm^K5Q(*$4K&+0LFx^B&1+b=AfB<45GRgZ(iVI?u00<yr4N&Ld=45#B
z>OBKAAHV@X05Or0^WMD&r@*7gVgLa|tO3(EZadC!`|i`}AU3iDK!5=N56SIJ3p?Tv
P00000NkvXXu0mjf%wuI)
--- a/browser/base/content/test/social/head.js
+++ b/browser/base/content/test/social/head.js
@@ -51,25 +51,27 @@ function checkProviderPrefsEmpty(isError
 
 function defaultFinishChecks() {
   checkProviderPrefsEmpty(true);
   finish();
 }
 
 function runSocialTestWithProvider(manifest, callback, finishcallback) {
 
-  let SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+  let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
 
   let manifests = Array.isArray(manifest) ? manifest : [manifest];
 
   // Check that none of the provider's content ends up in history.
   function finishCleanUp() {
+    ok(!SocialSidebar.provider, "no provider in sidebar");
+    SessionStore.setWindowValue(window, "socialSidebar", "");
     for (let i = 0; i < manifests.length; i++) {
       let m = manifests[i];
-      for (let what of ['iconURL', 'shareURL']) {
+      for (let what of ['sidebarURL', 'iconURL', 'shareURL', 'markURL']) {
         if (m[what]) {
           yield promiseSocialUrlNotRemembered(m[what]);
         }
       };
     }
     for (let i = 0; i < gURLsNotRemembered.length; i++) {
       yield promiseSocialUrlNotRemembered(gURLsNotRemembered[i]);
     }
@@ -188,55 +190,397 @@ function runSocialTests(tests, cbPreTest
     });
   }
   runNextTest();
 }
 
 // A fairly large hammer which checks all aspects of the SocialUI for
 // internal consistency.
 function checkSocialUI(win) {
-  let SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+  let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
+  win = win || window;
+  let doc = win.document;
+  let enabled = win.SocialUI.enabled;
+  let active = Social.providers.length > 0 && !win.SocialUI._chromeless &&
+               !PrivateBrowsingUtils.isWindowPrivate(win);
+  let sidebarEnabled = win.SocialSidebar.provider ? enabled : false;
+
   // if we have enabled providers, we should also have instances of those
   // providers
   if (SocialService.hasEnabledProviders) {
     ok(Social.providers.length > 0, "providers are enabled");
   } else {
     is(Social.providers.length, 0, "providers are not enabled");
   }
+
+  // some local helpers to avoid log-spew for the many checks made here.
+  let numGoodTests = 0, numTests = 0;
+  function _ok(what, msg) {
+    numTests++;
+    if (!ok)
+      ok(what, msg)
+    else
+      ++numGoodTests;
+  }
+  function _is(a, b, msg) {
+    numTests++;
+    if (a != b)
+      is(a, b, msg)
+    else
+      ++numGoodTests;
+  }
+  function isbool(a, b, msg) {
+    _is(!!a, !!b, msg);
+  }
+  isbool(win.SocialSidebar.canShow, sidebarEnabled, "social sidebar active?");
+
+  let contextMenus = [
+    {
+      type: "link",
+      id: "context-marklinkMenu",
+      label: "social.marklinkMenu.label"
+    },
+    {
+      type: "page",
+      id: "context-markpageMenu",
+      label: "social.markpageMenu.label"
+    }
+  ];
+
+  for (let c of contextMenus) {
+    let leMenu = document.getElementById(c.id);
+    let parent, menus;
+    let markProviders = SocialMarks.getProviders();
+    if (markProviders.length > SocialMarks.MENU_LIMIT) {
+      // menus should be in a submenu, not in the top level of the context menu
+      parent = leMenu.firstChild;
+      menus = document.getElementsByClassName("context-mark" + c.type);
+      _is(menus.length, 0, "menu's are not in main context menu\n");
+      menus = parent.childNodes;
+      _is(menus.length, markProviders.length, c.id + " menu exists for each mark provider");
+    } else {
+      // menus should be in the top level of the context menu, not in a submenu
+      parent = leMenu.parentNode;
+      menus = document.getElementsByClassName("context-mark" + c.type);
+      _is(menus.length, markProviders.length, c.id + " menu exists for each mark provider");
+      menus = leMenu.firstChild.childNodes;
+      _is(menus.length, 0, "menu's are not in context submenu\n");
+    }
+    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?");
+
+  // 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);
+    cb();
+  }
+  Services.obs.addObserver(observer, topic, false);
 }
 
 function setManifestPref(name, manifest) {
   let string = Cc["@mozilla.org/supports-string;1"].
                createInstance(Ci.nsISupportsString);
   string.data = JSON.stringify(manifest);
   Services.prefs.setComplexValue(name, Ci.nsISupportsString, string);
 }
 
 function getManifestPrefname(aManifest) {
   // is same as the generated name in SocialServiceInternal.getManifestPrefname
   let originUri = Services.io.newURI(aManifest.origin, null, null);
   return "social.manifest." + originUri.hostPort.replace('.','-');
 }
 
+function setBuiltinManifestPref(name, manifest) {
+  // we set this as a default pref, it must not be a user pref
+  manifest.builtin = true;
+  let string = Cc["@mozilla.org/supports-string;1"].
+               createInstance(Ci.nsISupportsString);
+  string.data = JSON.stringify(manifest);
+  Services.prefs.getDefaultBranch(null).setComplexValue(name, Ci.nsISupportsString, string);
+  // verify this is set on the default branch
+  let stored = Services.prefs.getComplexValue(name, Ci.nsISupportsString).data;
+  is(stored, string.data, "manifest '"+name+"' stored in default prefs");
+  // don't dirty our manifest, we'll need it without this flag later
+  delete manifest.builtin;
+  // verify we DO NOT have a user-level pref
+  ok(!Services.prefs.prefHasUserValue(name), "manifest '"+name+"' is not in user-prefs");
+}
+
+function resetBuiltinManifestPref(name) {
+  Services.prefs.getDefaultBranch(null).deleteBranch(name);
+  is(Services.prefs.getDefaultBranch(null).getPrefType(name),
+     Services.prefs.PREF_INVALID, "default manifest removed");
+}
+
+function ensureEventFired(elem, event) {
+  return BrowserTestUtils.waitForEvent(elem, event, true);
+}
+
 function ensureFrameLoaded(frame, uri) {
   return new Promise(resolve => {
     if (frame.contentDocument && frame.contentDocument.readyState == "complete" &&
         (!uri || frame.contentDocument.location.href == uri)) {
       resolve();
     } else {
       frame.addEventListener("load", function handler() {
         if (uri && frame.contentDocument.location.href != uri)
           return;
         frame.removeEventListener("load", handler, true);
         resolve()
       }, true);
     }
   });
 }
 
+// chat test help functions
+
+// And lots of helpers for the resize tests.
+function get3ChatsForCollapsing(mode, cb) {
+  // We make one chat, then measure its size.  We then resize the browser to
+  // ensure a second can be created fully visible but a third can not - then
+  // create the other 2.  first will will be collapsed, second fully visible
+  // and the third also visible and the "selected" one.
+  let chatbar = getChatBar();
+  let chatWidth = undefined;
+  let num = 0;
+  is(chatbar.childNodes.length, 0, "chatbar starting empty");
+  is(chatbar.menupopup.childNodes.length, 0, "popup starting empty");
+
+  makeChat(mode, "first chat", function() {
+    // got the first one.
+    checkPopup();
+    ok(chatbar.menupopup.parentNode.collapsed, "menu selection isn't visible");
+    // we kinda cheat here and get the width of the first chat, assuming
+    // that all future chats will have the same width when open.
+    chatWidth = chatbar.calcTotalWidthOf(chatbar.selectedChat);
+    let desired = chatWidth * 2.5;
+    resizeWindowToChatAreaWidth(desired, function(sizedOk) {
+      ok(sizedOk, "can't do any tests without this width");
+      checkPopup();
+      makeChat(mode, "second chat", function() {
+        is(chatbar.childNodes.length, 2, "now have 2 chats");
+        checkPopup();
+        // and create the third.
+        makeChat(mode, "third chat", function() {
+          is(chatbar.childNodes.length, 3, "now have 3 chats");
+          checkPopup();
+          // XXX - this is a hacky implementation detail around the order of
+          // the chats.  Ideally things would be a little more sane wrt the
+          // other in which the children were created.
+          let second = chatbar.childNodes[2];
+          let first = chatbar.childNodes[1];
+          let third = chatbar.childNodes[0];
+          is(first.collapsed, true, "first collapsed state as promised");
+          is(second.collapsed, false, "second collapsed state as promised");
+          is(third.collapsed, false, "third collapsed state as promised");
+          is(chatbar.selectedChat, third, "third is selected as promised")
+          info("have 3 chats for collapse testing - starting actual test...");
+          cb(first, second, third);
+        }, mode);
+      }, mode);
+    });
+  }, mode);
+}
+
+function makeChat(mode, uniqueid, cb) {
+  info("making a chat window '" + uniqueid +"'");
+  let provider = SocialSidebar.provider;
+  let chatUrl = provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
+  // chatURL is not a part of the provider class, but is added by tests if we
+  // want to use a specific url (different than above) for testing
+  if (provider.chatURL) {
+    chatUrl = provider.chatURL;
+  }
+  // Note that we use promiseChatLoaded instead of the callback to ensure the
+  // content has started loading.
+  let chatbox = getChatBar().openChat({
+    origin: provider.origin,
+    title: provider.name,url: chatUrl + "?id=" + uniqueid,
+    mode: mode
+  });
+  chatbox.promiseChatLoaded.then(
+    () => {
+    info("chat window has opened");
+    chatbox.content.messageManager.sendAsyncMessage("Social:SetDocumentTitle", {
+      title: uniqueid
+    });
+    cb(chatbox);
+  });
+}
+
+function checkPopup() {
+  // popup only showing if any collapsed popup children.
+  let chatbar = getChatBar();
+  let numCollapsed = 0;
+  for (let chat of chatbar.childNodes) {
+    if (chat.collapsed) {
+      numCollapsed += 1;
+      // and it have a menuitem weakmap
+      is(chatbar.menuitemMap.get(chat).nodeName, "menuitem", "collapsed chat has a menu item");
+    } else {
+      ok(!chatbar.menuitemMap.has(chat), "open chat has no menu item");
+    }
+  }
+  is(chatbar.menupopup.parentNode.collapsed, numCollapsed == 0, "popup matches child collapsed state");
+  is(chatbar.menupopup.childNodes.length, numCollapsed, "popup has correct count of children");
+  // todo - check each individual elt is what we expect?
+}
+// Resize the main window so the chat area's boxObject is |desired| wide.
+// Does a callback passing |true| if the window is now big enough or false
+// if we couldn't resize large enough to satisfy the test requirement.
+function resizeWindowToChatAreaWidth(desired, cb, count = 0) {
+  let current = getChatBar().getBoundingClientRect().width;
+  let delta = desired - current;
+  info(count + ": resizing window so chat area is " + desired + " wide, currently it is "
+       + current + ".  Screen avail is " + window.screen.availWidth
+       + ", current outer width is " + window.outerWidth);
+
+  // WTF?  Sometimes we will get fractional values due to the - err - magic
+  // of DevPointsPerCSSPixel etc, so we allow a couple of pixels difference.
+  let widthDeltaCloseEnough = function(d) {
+    return Math.abs(d) < 2;
+  }
+
+  // attempting to resize by (0,0), unsurprisingly, doesn't cause a resize
+  // event - so just callback saying all is well.
+  if (widthDeltaCloseEnough(delta)) {
+    info(count + ": skipping this as screen width is close enough");
+    executeSoon(function() {
+      cb(true);
+    });
+    return;
+  }
+  // On lo-res screens we may already be maxed out but still smaller than the
+  // requested size, so asking to resize up also will not cause a resize event.
+  // So just callback now saying the test must be skipped.
+  if (window.screen.availWidth - window.outerWidth < delta) {
+    info(count + ": skipping this as screen available width is less than necessary");
+    executeSoon(function() {
+      cb(false);
+    });
+    return;
+  }
+  function resize_handler(event) {
+    // we did resize - but did we get far enough to be able to continue?
+    let newSize = getChatBar().getBoundingClientRect().width;
+    let sizedOk = widthDeltaCloseEnough(newSize - desired);
+    if (!sizedOk)
+      return;
+    window.removeEventListener("resize", resize_handler, true);
+    info(count + ": resized window width is " + newSize);
+    executeSoon(function() {
+      cb(sizedOk);
+    });
+  }
+  // Otherwise we request resize and expect a resize event
+  window.addEventListener("resize", resize_handler, true);
+  window.resizeBy(delta, 0);
+}
+
+function resizeAndCheckWidths(first, second, third, checks, cb) {
+  if (checks.length == 0) {
+    cb(); // nothing more to check!
+    return;
+  }
+  let count = checks.length;
+  let [width, numExpectedVisible, why] = checks.shift();
+  info("<< Check " + count + ": " + why);
+  info(count + ": " + "resizing window to " + width + ", expect " + numExpectedVisible + " visible items");
+  resizeWindowToChatAreaWidth(width, function(sizedOk) {
+    checkPopup();
+    ok(sizedOk, count+": window resized correctly");
+    function collapsedObserver(r, m) {
+      if ([first, second, third].filter(item => !item.collapsed).length == numExpectedVisible) {
+        if (m) {
+          m.disconnect();
+        }
+        ok(true, count + ": " + "correct number of chats visible");
+        info(">> Check " + count);
+        executeSoon(function() {
+          resizeAndCheckWidths(first, second, third, checks, cb);
+        });
+      }
+    }
+    let m = new MutationObserver(collapsedObserver);
+    m.observe(first, {attributes: true });
+    m.observe(second, {attributes: true });
+    m.observe(third, {attributes: true });
+    // and just in case we are already at the right size, explicitly call the
+    // observer.
+    collapsedObserver(undefined, m);
+  }, count);
+}
+
+function getChatBar() {
+  let cb = document.getElementById("pinnedchats");
+  cb.hidden = false;
+  return cb;
+}
+
+function getPopupWidth() {
+  let chatbar = getChatBar();
+  let popup = chatbar.menupopup;
+  ok(!popup.parentNode.collapsed, "asking for popup width when it is visible");
+  let cs = document.defaultView.getComputedStyle(popup.parentNode);
+  let margins = parseInt(cs.marginLeft) + parseInt(cs.marginRight);
+  return popup.parentNode.getBoundingClientRect().width + margins;
+}
+
+function promiseNodeRemoved(aNode) {
+  return new Promise(resolve => {
+    let parent = aNode.parentNode;
+
+    let observer = new MutationObserver(function onMutatations(mutations) {
+      for (let mutation of mutations) {
+        for (let i = 0; i < mutation.removedNodes.length; i++) {
+          let node = mutation.removedNodes.item(i);
+          if (node != aNode) {
+            continue;
+          }
+          observer.disconnect();
+          resolve();
+        }
+      }
+    });
+    observer.observe(parent, {childList: true});
+  });
+}
+
+function promiseCloseChat(chat) {
+  let promise = promiseNodeRemoved(chat);
+  chat.close();
+  return promise;
+}
+
+function closeAllChats() {
+  let chatbar = getChatBar();
+  while (chatbar.selectedChat) {
+    yield promiseCloseChat(chatbar.selectedChat);
+  }
+}
+
+function openChatViaUser() {
+  let sidebarDoc = document.getElementById("social-sidebar-browser").contentDocument;
+  let button = sidebarDoc.getElementById("chat-opener");
+  // Note we must use synthesizeMouseAtCenter() rather than calling
+  // .click() directly as this causes nsIDOMWindowUtils.isHandlingUserInput
+  // to be true.
+  EventUtils.synthesizeMouseAtCenter(button, {}, sidebarDoc.defaultView);
+}
+
+
 // Support for going on and offline.
 // (via browser/base/content/test/browser_bookmark_titles.js)
 var origProxyType = Services.prefs.getIntPref('network.proxy.type');
 
 function toggleOfflineStatus(goOffline) {
   // Bug 968887 fix.  when going on/offline, wait for notification before continuing
   return new Promise(resolve => {
     if (!goOffline) {
--- a/browser/base/content/test/social/social_activate.html
+++ b/browser/base/content/test/social/social_activate.html
@@ -8,17 +8,18 @@
 var data = {
   // currently required
   "name": "Demo Social Service",
   "iconURL": "chrome://branding/content/icon16.png",
   "icon32URL": "chrome://branding/content/favicon32.png",
   "icon64URL": "chrome://branding/content/icon64.png",
 
   // at least one of these must be defined
-  "shareURL": "/browser/browser/base/content/test/social/social_share.html",
+  "sidebarURL": "/browser/browser/base/content/test/social/social_sidebar.html",
+  "statusURL": "/browser/browser/base/content/test/social/social_panel.html",
   "postActivationURL": "/browser/browser/base/content/test/social/social_postActivation.html",
 
   // should be available for display purposes
   "description": "A short paragraph about this provider",
   "author": "Shane Caraveo, Mozilla",
 
   // optional
   "version": "1.0"
--- a/browser/base/content/test/social/social_activate_basic.html
+++ b/browser/base/content/test/social/social_activate_basic.html
@@ -8,17 +8,17 @@
 var data = {
   // currently required
   "name": "Demo Social Service",
   "iconURL": "chrome://branding/content/icon16.png",
   "icon32URL": "chrome://branding/content/favicon32.png",
   "icon64URL": "chrome://branding/content/icon64.png",
 
   // at least one of these must be defined
-  "shareURL": "/browser/browser/base/content/test/social/social_share.html",
+  "sidebarURL": "/browser/browser/base/content/test/social/social_sidebar_empty.html",
   "postActivationURL": "/browser/browser/base/content/test/social/social_postActivation.html",
 
   // should be available for display purposes
   "description": "A short paragraph about this provider",
   "author": "Shane Caraveo, Mozilla",
 
   // optional
   "version": "1.0"
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/social_chat.html
@@ -0,0 +1,15 @@
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>test chat window</title>
+  </head>
+  <body>
+    <p>This is a test social chat window.</p>
+    <!-- a couple of input fields to help with focus testing -->
+    <input id="input1"/>
+    <input id="input2"/>
+
+    <!-- an iframe here so this one page generates multiple load events -->
+    <iframe id="iframe" src="data:text/plain:this is an iframe"></iframe>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/social_flyout.html
@@ -0,0 +1,25 @@
+<html>
+  <head>
+    <meta charset="utf-8">
+    <script>
+      window.addEventListener("socialTest-MakeWider", function(e) {
+        document.body.setAttribute("style", "width: 500px; height: 500px; margin: 0; overflow: hidden;");
+        document.body.offsetWidth; // force a layout flush
+        var evt = document.createEvent("CustomEvent");
+        evt.initCustomEvent("SocialTest-DoneMakeWider", true, true, {});
+        document.documentElement.dispatchEvent(evt);
+      }, false);
+      window.addEventListener("socialTest-CloseSelf", function(e) {
+        window.close();
+        var evt = document.createEvent("CustomEvent");
+        evt.initCustomEvent("SocialTest-DoneCloseSelf", true, true, {});
+        document.documentElement.dispatchEvent(evt);
+      }, false);
+    </script>
+  </head>
+  <body style="width: 400px; height: 400px; margin: 0; overflow: hidden;">
+    <p>This is a test social flyout panel.</p>
+    <a id="traversal" href="https://test.example.com">test link</a>
+  </body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/social_mark.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <link id="siteicon" rel="icon" href="./icon.png"/>
+    <title>Demo Mark Window</title>
+    <script type="text/javascript">
+
+    function updateTextNode(parent, text) {
+      var textNode = parent.childNodes[0];
+      if (textNode)
+        parent.removeChild(textNode);
+      textNode = document.createTextNode(text);
+      parent.appendChild(textNode);
+    }
+    function onLoad() {
+      updateTextNode(document.getElementById("shared"), location.search);
+      socialMarkUpdate(true);
+    }
+    function socialMarkUpdate(isMarked) {
+        var evt = document.createEvent("CustomEvent");
+        evt.initCustomEvent("socialMarkUpdate", true, true, JSON.stringify({marked: isMarked}));
+        document.documentElement.dispatchEvent(evt);
+    }
+    var shareData;
+    addEventListener("OpenGraphData", function(e) {
+      shareData = JSON.parse(e.detail);
+      updateTextNode(document.getElementById("shared"), shareData.url);
+      socialMarkUpdate(true);
+    });
+    </script>
+</head>
+
+<body onload="onLoad()">
+  <div id="content">
+    <h3>This window shows the mark data</h3>
+    <div>Page Marked: <div id="shared" class="textbox"></div></div>
+    <button id="unmark" onclick="socialMarkUpdate(false); window.close()">Unmark</button>
+    <button onclick="window.close();">Close</button>
+  </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/social_panel.html
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <meta charset="utf-8">
+  </head>
+  <body>
+    <p>This is a test social panel.</p>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/social_sidebar.html
@@ -0,0 +1,17 @@
+<html>
+  <head>
+    <meta charset="utf-8">
+    <script>
+      addEventListener("test-flyout-open", function(e) {
+        navigator.mozSocial.openPanel("social_flyout.html");
+      }, false);
+      addEventListener("test-flyout-close", function(e) {
+        navigator.mozSocial.closePanel();
+      }, false);
+    </script>
+  </head>
+  <body>
+    <p>This is a test social sidebar.</p>
+    <button id="chat-opener" onclick="navigator.mozSocial.openChatWindow('./social_chat.html');"/>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/social_sidebar_empty.html
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <meta charset="utf-8">
+  </head>
+  <body>
+    <p>This is a test social sidebar.</p>
+  </body>
+</html>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4d3e72b8018ea0be59ca08a45c2ab7c1b6dcd73c
GIT binary patch
literal 779
zc$@(X1N8ifP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004XF*Lt006JZ
zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!ok>JNRCwBA
z{Qv(y10?_;fLL%z*k!g$ed1M`{)1n};NP$R|JlC2xElMZ^Tq$qFOz`MTYz|a#qvkn
z00M{s<N|D%yXWP<!0o^OpMLr8|C+o1{u{3T^Ivbu_5Z)WzyH6x#q583gzRRJI)DIT
zu}E)cx*(sw=HGt?pZ|;utPD&H4F7?6f`>4JiySk906PN%6Eg$DG&^R7pQleUJU{x3
z;pMv@AV;nT5dZ<i@`sU)lZl;4bc#0zD+^HLA7GHkax*XpurV;)|IEN3{f~j+(pv_G
zZ-0OW?V8G<5*)?w`t4_s-W?zUAb?nYu?k8vb2IUSIGY{<Z3ohZ!VC;@ybKJgd<+a<
zelsutUB__y^~YZ}+zf2r-`!{U@$(M@(ErasdH@25={u{47B?r8029!5W*}w<1}h^_
zf(^*#1Umf_(15={1C;+i|1KiT%fQ0Hj}q7b0mSr$L&mhih?)NZ&{Z5D0~jF&u>xr!
z4v0b0Krj7yagAS0OoZXZ?UNvVS78PK1Q5#?E;(IpRz~iFFBlkp0{v?&!oZ*cbh;QP
z1H*yGfB$X0_wDCTMt0T&?td%{9~c;3zWD+VKo9^3Ag2EuJmSCp{$~zWWMGI?Vqh>6
z{m;;I_7{WC^0)uj9J+MDT81-8_}81ot=ZWO|Ni`Cc=hHZNZ)Rl0RRESBKK^|rKXiy
zI$t=&IT`-{W8nDr?zh;7OJ_g#gjz8$2!j=MbXdH$@e@mAVqs=@`|cAc01v?o00<zK
zYkeW9Kspr>ETTZHj-D{xL`4O#rdWUgVj?oh`%8)oVwC_0AYu(r=i%mLc=75z12iAN
z0YCsTk&^S?y$7ejqsU?a0Yt0;(>HEA&T#wg)9D~KvIIbY0RX^P>0zF&!}|aL002ov
JPDHLkV1h<LU9$iH
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -96,16 +96,17 @@ browser.jar:
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)
 #ifdef CAN_DRAW_IN_TITLEBAR
         content/browser/browser-tabsintitlebar.js       (content/browser-tabsintitlebar.js)
 #else
         content/browser/browser-tabsintitlebar.js       (content/browser-tabsintitlebar-stub.js)
 #endif
         content/browser/browser-thumbnails.js         (content/browser-thumbnails.js)
         content/browser/browser-trackingprotection.js (content/browser-trackingprotection.js)
+*       content/browser/chatWindow.xul                (content/chatWindow.xul)
         content/browser/tab-content.js                (content/tab-content.js)
         content/browser/content.js                    (content/content.js)
         content/browser/social-content.js             (content/social-content.js)
         content/browser/defaultthemes/1.footer.jpg    (content/defaultthemes/1.footer.jpg)
         content/browser/defaultthemes/1.header.jpg    (content/defaultthemes/1.header.jpg)
         content/browser/defaultthemes/1.icon.jpg      (content/defaultthemes/1.icon.jpg)
         content/browser/defaultthemes/1.preview.jpg   (content/defaultthemes/1.preview.jpg)
         content/browser/defaultthemes/2.footer.jpg    (content/defaultthemes/2.footer.jpg)
@@ -183,16 +184,18 @@ browser.jar:
 *       content/browser/viewSourceOverlay.xul         (content/viewSourceOverlay.xul)
 #ifndef XP_MACOSX
 *       content/browser/webrtcIndicator.xul           (content/webrtcIndicator.xul)
         content/browser/webrtcIndicator.js            (content/webrtcIndicator.js)
 #endif
 #ifdef XP_WIN
         content/browser/win6BrowserOverlay.xul        (content/win6BrowserOverlay.xul)
 #endif
+        content/browser/socialmarks.xml               (content/socialmarks.xml)
+        content/browser/socialchat.xml                (content/socialchat.xml)
 # the following files are browser-specific overrides
 *       content/browser/license.html                  (/toolkit/content/license.html)
 % override chrome://global/content/license.html chrome://browser/content/license.html
 #ifdef MOZ_SAFE_BROWSING
         content/browser/report-phishing-overlay.xul     (content/report-phishing-overlay.xul)
         content/browser/blockedSite.xhtml               (content/blockedSite.xhtml)
 % overlay chrome://browser/content/browser.xul chrome://browser/content/report-phishing-overlay.xul
 #endif
--- a/browser/base/moz.build
+++ b/browser/base/moz.build
@@ -11,16 +11,17 @@ MOCHITEST_MANIFESTS += [
 ]
 
 MOCHITEST_CHROME_MANIFESTS += [
     'content/test/chrome/chrome.ini',
 ]
 
 BROWSER_CHROME_MANIFESTS += [
     'content/test/alerts/browser.ini',
+    'content/test/chat/browser.ini',
     'content/test/general/browser.ini',
     'content/test/newtab/browser.ini',
     'content/test/plugins/browser.ini',
     'content/test/popupNotifications/browser.ini',
     'content/test/referrer/browser.ini',
     'content/test/social/browser.ini',
     'content/test/tabPrompts/browser.ini',
     'content/test/urlbar/browser.ini',
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -544,35 +544,41 @@ const CustomizableWidgets = [
     onViewShowing: function(aEvent) {
       // Populate the subview with whatever menuitems are in the
       // sidebar menu. We skip menu elements, because the menu panel has no way
       // of dealing with those right now.
       let doc = aEvent.target.ownerDocument;
       let win = doc.defaultView;
       let menu = doc.getElementById("viewSidebarMenu");
 
-      // First clear any existing menuitems then populate. Add it to the
+      // First clear any existing menuitems then populate. Social sidebar
+      // options may not have been added yet, so we do that here. Add it to the
       // standard menu first, then copy all sidebar options to the panel.
+      win.SocialSidebar.clearProviderMenus();
+      let providerMenuSeps = menu.getElementsByClassName("social-provider-menu");
+      if (providerMenuSeps.length > 0)
+        win.SocialSidebar.populateProviderMenu(providerMenuSeps[0]);
+
       let sidebarItems = doc.getElementById("PanelUI-sidebarItems");
       clearSubview(sidebarItems);
       fillSubviewFromMenuItems([...menu.children], sidebarItems);
     }
   }, {
     id: "social-share-button",
     // custom build our button so we can attach to the share command
     type: "custom",
     onBuild: function(aDocument) {
       let node = aDocument.createElementNS(kNSXUL, "toolbarbutton");
       node.setAttribute("id", this.id);
       node.classList.add("toolbarbutton-1");
       node.classList.add("chromeclass-toolbar-additional");
       node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
       node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
       node.setAttribute("removable", "true");
-      node.setAttribute("observes", "Social:PageShareable");
+      node.setAttribute("observes", "Social:PageShareOrMark");
       node.setAttribute("command", "Social:SharePage");
 
       let listener = {
         onWidgetAdded: (aWidgetId) => {
           if (aWidgetId != this.id)
             return;
 
           Services.obs.notifyObservers(null, "social:" + this.id + "-added", null);
--- a/browser/extensions/pocket/bootstrap.js
+++ b/browser/extensions/pocket/bootstrap.js
@@ -10,17 +10,17 @@ Cu.import("resource://services-common/ut
 Cu.import("resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SocialService",
-                                  "resource:///modules/SocialService.jsm");
+                                  "resource://gre/modules/SocialService.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
                                   "resource://gre/modules/ReaderMode.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Pocket",
                                   "chrome://pocket/content/Pocket.jsm");
 XPCOMUtils.defineLazyGetter(this, "gPocketBundle", function() {
   return Services.strings.createBundle("chrome://pocket/locale/pocket.properties");
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -156,16 +156,17 @@ These should match what Safari and other
 <!ENTITY closeWindow.label "Close Window">
 <!ENTITY closeWindow.accesskey "d">
 
 <!ENTITY bookmarksMenu.label "Bookmarks">
 <!ENTITY bookmarksMenu.accesskey "B">
 <!ENTITY bookmarkThisPageCmd.label "Bookmark This Page">
 <!ENTITY editThisBookmarkCmd.label "Edit This Bookmark">
 <!ENTITY bookmarkThisPageCmd.commandkey "d">
+<!ENTITY markPageCmd.commandkey "l">
 <!-- LOCALIZATION NOTE (findShareServices.label):
   -  Use the unicode ellipsis char, \u2026,
   -  or use "..." if \u2026 doesn't suit traditions in your locale. -->
 <!ENTITY findShareServices.label "Find more Share services…">
 <!ENTITY sharePageCmd.label "Share This Page">
 <!ENTITY sharePageCmd.commandkey "S">
 <!ENTITY sharePageCmd.accesskey "s">
 <!-- LOCALIZATION NOTE (shareLink.accesskey): must be different than the following share access keys -->
@@ -769,18 +770,32 @@ you can use these alternative items. Oth
 <!ENTITY syncSignIn.label             "Sign In To &syncBrand.shortName.label;…">
 <!ENTITY syncSignIn.accesskey         "Y">
 <!ENTITY syncSyncNowItem.label        "Sync Now">
 <!ENTITY syncSyncNowItem.accesskey    "S">
 <!ENTITY syncReAuthItem.label         "Reconnect to &syncBrand.shortName.label;…">
 <!ENTITY syncReAuthItem.accesskey     "R">
 <!ENTITY syncToolbarButton.label      "Sync">
 
+<!ENTITY socialToolbar.title        "Social Toolbar Button">
+
+<!ENTITY social.ok.label       "OK">
+<!ENTITY social.ok.accesskey   "O">
+
+<!ENTITY social.toggleSidebar.label "Show sidebar">
+<!ENTITY social.toggleSidebar.accesskey "s">
+
 <!ENTITY social.addons.label "Manage Services…">
 
+<!ENTITY social.toggleNotifications.label "Show desktop notifications">
+<!ENTITY social.toggleNotifications.accesskey "n">
+
+<!ENTITY social.learnMore.label "Learn more…">
+<!ENTITY social.learnMore.accesskey "l">
+
 <!ENTITY social.directory.label "Activations Directory">
 <!ENTITY social.directory.text "You can activate Share services from the directory.">
 <!ENTITY social.directory.button "Take me there!">
 <!ENTITY social.directory.introText "Click on a service to add it to &brandShortName;.">
 <!ENTITY social.directory.viewmore.text "View More">
 
 <!ENTITY customizeMode.menuAndToolbars.header2 "Additional Tools and Features">
 <!ENTITY customizeMode.menuAndToolbars.empty "Want more tools?">
@@ -791,16 +806,25 @@ you can use these alternative items. Oth
 <!ENTITY customizeMode.lwthemes "Themes">
 <!ENTITY customizeMode.lwthemes.myThemes "My Themes">
 <!ENTITY customizeMode.lwthemes.recommended "Recommended">
 <!ENTITY customizeMode.lwthemes.menuManage "Manage">
 <!ENTITY customizeMode.lwthemes.menuManage.accessKey "M">
 <!ENTITY customizeMode.lwthemes.menuGetMore "Get More Themes">
 <!ENTITY customizeMode.lwthemes.menuGetMore.accessKey "G">
 
+<!ENTITY social.chatBar.commandkey "c">
+<!ENTITY social.chatBar.label "Focus chats">
+<!ENTITY social.chatBar.accesskey "c">
+
+<!ENTITY social.markpageMenu.accesskey "P">
+<!ENTITY social.markpageMenu.label "Save Page To…">
+<!ENTITY social.marklinkMenu.accesskey "L">
+<!ENTITY social.marklinkMenu.label "Save Link To…">
+
 <!ENTITY getUserMedia.selectCamera.label "Camera to share:">
 <!ENTITY getUserMedia.selectCamera.accesskey "C">
 <!ENTITY getUserMedia.selectMicrophone.label "Microphone to share:">
 <!ENTITY getUserMedia.selectMicrophone.accesskey "M">
 <!ENTITY getUserMedia.audioCapture.label "Audio from the tab will be shared.">
 <!ENTITY getUserMedia.allWindowsShared.message "All visible windows on your screen will be shared.">
 
 <!ENTITY trackingProtection.title "Tracking Protection">
new file mode 100644
--- /dev/null
+++ b/browser/modules/Chat.jsm
@@ -0,0 +1,323 @@
+/* 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", "kDefaultButtonSet"];
+
+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");
+
+const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const kDefaultButtonSet = new Set(["minimize", "swap", "close"]);
+const kHiddenDefaultButtons = new Set(["minimize", "close"]);
+var gCustomButtons = new Map();
+
+// 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").includes("extrachrome") ||
+                   docElem.getAttribute('chromehidden').includes("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
+ */
+
+var Chat = {
+
+  /**
+   * Iterator of <chatbox> elements from this module in all windows.
+   */
+  get chatboxes() {
+    return function*() {
+      let winEnum = Services.wm.getEnumerator("navigator:browser");
+      while (winEnum.hasMoreElements()) {
+        let win = winEnum.getNext();
+        let chatbar = win.document.getElementById("pinnedchats");
+        if (!chatbar)
+          continue;
+
+        // Make a new array instead of the live NodeList so this iterator can be
+        // used for closing/deleting.
+        let chatboxes = [...chatbar.children];
+        for (let chatbox of chatboxes) {
+          yield chatbox;
+        }
+      }
+
+      // include standalone chat windows
+      winEnum = Services.wm.getEnumerator("Social:Chat");
+      while (winEnum.hasMoreElements()) {
+        let win = winEnum.getNext();
+        if (win.closed)
+          continue;
+        yield win.document.getElementById("chatter");
+      }
+    }();
+  },
+
+  /**
+   * Open a new chatbox.
+   *
+   * @param contentWindow [optional]
+   *        The content window that requested this chat.  May be null.
+   * @param options
+   *        Object that may contain the following properties:
+   *         - origin
+   *        The origin for the chat.  This is primarily used as an identifier
+   *        to help identify all chats from the same provider.
+   *         - title
+   *        The title to be used if a new chat window is created.
+   *         - 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.
+   *         - mode [optional]
+   *        May be undefined or 'minimized'
+   *         - 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)
+   *         - remote [optional]
+   *         Indicates if the chatbox browser should use the remote bindings
+   *         to run in the content process when TRUE.
+   * @param callback
+   *        Function to be invoked once the chat constructed. The chatbox binding
+   *        is passed as the first argument.
+   *
+   * @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, options, 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;
+    if (options.remote) {
+      // Double check that current window can handle remote browser elements.
+      let browser = chromeWindow.gBrowser && chromeWindow.gBrowser.selectedBrowser;
+      if (!browser || browser.getAttribute("remote") != "true") {
+        options.remote = false;
+      }
+    }
+    let chatbox = chatbar.openChat(options, 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" in options)) {
+      let dwu = chromeWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                            .getInterface(Ci.nsIDOMWindowUtils);
+      options.focus = dwu.isHandlingUserInput;
+    }
+    if (options.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) {
+    for (let chatbox of this.chatboxes) {
+      if (chatbox.content.getAttribute("origin") != origin) {
+        continue;
+      }
+      chatbox.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;
+  },
+
+  /**
+   * Adds a button to the collection of custom buttons that can be added to the
+   * titlebar of a chatbox.
+   * For the button to be visible, `Chat#loadButtonSet` has to be called with
+   * the new buttons' ID in the buttonSet argument.
+   *
+   * @param  {Object} button Button object that may contain the following fields:
+   *   - {String}   id          Button identifier.
+   *   - {Function} [onBuild]   Function that returns a valid DOM node to
+   *                            represent the button.
+   *   - {Function} [onCommand] Callback function that is invoked when the DOM
+   *                            node is clicked.
+   */
+  registerButton: function(button) {
+    if (gCustomButtons.has(button.id))
+      return;
+    gCustomButtons.set(button.id, button);
+  },
+
+  /**
+   * Load a set of predefined buttons in a chatbox' titlebar.
+   *
+   * @param  {XULDOMNode} chatbox   Chatbox XUL element.
+   * @param  {Set|String} buttonSet Set of buttons to show in the titlebar. This
+   *                                may be a comma-separated string or a predefined
+   *                                set object.
+   */
+  loadButtonSet: function(chatbox, buttonSet = kDefaultButtonSet) {
+    if (!buttonSet)
+      return;
+
+    // When the buttonSet is coming from an XML attribute, it will be a string.
+    if (typeof buttonSet == "string") {
+      buttonSet = buttonSet.split(",").map(button => button.trim());
+    }
+
+    // Make sure to keep the current set around.
+    chatbox.setAttribute("buttonSet", [...buttonSet].join(","));
+
+    let isUndocked = !chatbox.chatbar;
+    let document = chatbox.ownerDocument;
+    let titlebarNode = document.getAnonymousElementByAttribute(chatbox, "class",
+      "chat-titlebar");
+    let buttonsSeen = new Set();
+
+    for (let buttonId of buttonSet) {
+      buttonId = buttonId.trim();
+      buttonsSeen.add(buttonId);
+      let nodes, node;
+      if (kDefaultButtonSet.has(buttonId)) {
+        node = document.getAnonymousElementByAttribute(chatbox, "anonid", buttonId);
+        if (!node)
+          continue;
+
+        node.hidden = isUndocked && kHiddenDefaultButtons.has(buttonId) ? true : false;
+      } else if (gCustomButtons.has(buttonId)) {
+        let button = gCustomButtons.get(buttonId);
+        let buttonClass = "chat-" + buttonId;
+        // Custom buttons are not defined in the chatbox binding, thus not
+        // anonymous elements.
+        nodes = titlebarNode.getElementsByClassName(buttonClass);
+        node = nodes && nodes.length ? nodes[0] : null;
+        if (!node) {
+          // Allow custom buttons to build their own button node.
+          if (button.onBuild) {
+            node = button.onBuild(chatbox);
+          } else {
+            // We can also build a normal toolbarbutton to insert.
+            node = document.createElementNS(kNSXUL, "toolbarbutton");
+            node.classList.add(buttonClass);
+            node.classList.add("chat-toolbarbutton");
+          }
+
+          if (button.onCommand) {
+            node.addEventListener("command", e => {
+              button.onCommand(e, chatbox);
+            });
+          }
+          titlebarNode.appendChild(node);
+        }
+
+        // When the chat is undocked and the button wants to be visible then, it
+        // will be.
+        node.hidden = isUndocked && !button.visibleWhenUndocked;
+      } else {
+        Cu.reportError("Chatbox button '" + buttonId + "' could not be found!\n");
+      }
+    }
+
+    // Hide any button that is part of the default set, but not of the current set.
+    for (let button of kDefaultButtonSet) {
+      if (!buttonsSeen.has(button))
+        document.getAnonymousElementByAttribute(chatbox, "anonid", button).hidden = true;
+    }
+  }
+};
--- a/browser/modules/Social.jsm
+++ b/browser/modules/Social.jsm
@@ -1,15 +1,16 @@
 /* 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";
 
-this.EXPORTED_SYMBOLS = ["Social", "OpenGraphBuilder",
+this.EXPORTED_SYMBOLS = ["Social", "CreateSocialStatusWidget",
+                         "CreateSocialMarkWidget", "OpenGraphBuilder",
                          "DynamicResizeWatcher", "sizeSocialPanelToContent"];
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 
 // The minimum sizes for the auto-resize panel code, minimum size necessary to
 // properly show the error page in the panel.
@@ -17,25 +18,65 @@ const PANEL_MIN_HEIGHT = 190;
 const PANEL_MIN_WIDTH = 330;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SocialService",
-  "resource:///modules/SocialService.jsm");
+  "resource://gre/modules/SocialService.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
   "resource://gre/modules/PageMetadata.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   "resource://gre/modules/Promise.jsm");
 
 
+function promiseSetAnnotation(aURI, providerList) {
+  let deferred = Promise.defer();
+
+  // Delaying to catch issues with asynchronous behavior while waiting
+  // to implement asynchronous annotations in bug 699844.
+  Services.tm.mainThread.dispatch(function() {
+    try {
+      if (providerList && providerList.length > 0) {
+        PlacesUtils.annotations.setPageAnnotation(
+          aURI, "social/mark", JSON.stringify(providerList), 0,
+          PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
+      } else {
+        PlacesUtils.annotations.removePageAnnotation(aURI, "social/mark");
+      }
+    } catch(e) {
+      Cu.reportError("SocialAnnotation failed: " + e);
+    }
+    deferred.resolve();
+  }, Ci.nsIThread.DISPATCH_NORMAL);
+
+  return deferred.promise;
+}
+
+function promiseGetAnnotation(aURI) {
+  let deferred = Promise.defer();
+
+  // Delaying to catch issues with asynchronous behavior while waiting
+  // to implement asynchronous annotations in bug 699844.
+  Services.tm.mainThread.dispatch(function() {
+    let val = null;
+    try {
+      val = PlacesUtils.annotations.getPageAnnotation(aURI, "social/mark");
+    } catch (ex) { }
+
+    deferred.resolve(val);
+  }, Ci.nsIThread.DISPATCH_NORMAL);
+
+  return deferred.promise;
+}
+
 this.Social = {
   initialized: false,
   lastEventReceived: 0,
   providers: [],
   _disabledForSafeMode: false,
 
   init: function Social_init() {
     this._disabledForSafeMode = Services.appinfo.inSafeMode && this.enabled;
@@ -103,16 +144,21 @@ this.Social = {
     this.providers = providers;
     Services.obs.notifyObservers(null, "social:providers-changed", null);
   },
 
   get enabled() {
     return !this._disabledForSafeMode && this.providers.length > 0;
   },
 
+  toggleNotifications: function SocialNotifications_toggle() {
+    let prefValue = Services.prefs.getBoolPref("social.toast-notifications.enabled");
+    Services.prefs.setBoolPref("social.toast-notifications.enabled", !prefValue);
+  },
+
   _getProviderFromOrigin: function (origin) {
     for (let p of this.providers) {
       if (p.origin == origin) {
         return p;
       }
     }
     return null;
   },
@@ -129,19 +175,149 @@ this.Social = {
     SocialService.uninstallProvider(origin, aCallback);
   },
 
   // Activation functionality
   activateFromOrigin: function (origin, callback) {
     // It's OK if the provider has already been activated - we still get called
     // back with it.
     SocialService.enableProvider(origin, callback);
+  },
+
+  // Page Marking functionality
+  isURIMarked: function(origin, aURI, aCallback) {
+    promiseGetAnnotation(aURI).then(function(val) {
+      if (val) {
+        let providerList = JSON.parse(val);
+        val = providerList.indexOf(origin) >= 0;
+      }
+      aCallback(!!val);
+    }).then(null, Cu.reportError);
+  },
+
+  markURI: function(origin, aURI, aCallback) {
+    // update or set our annotation
+    promiseGetAnnotation(aURI).then(function(val) {
+
+      let providerList = val ? JSON.parse(val) : [];
+      let marked = providerList.indexOf(origin) >= 0;
+      if (marked)
+        return;
+      providerList.push(origin);
+      // we allow marking links in a page that may not have been visited yet.
+      // make sure there is a history entry for the uri, then annotate it.
+      let place = {
+        uri: aURI,
+        visits: [{
+          visitDate: Date.now() + 1000,
+          transitionType: Ci.nsINavHistoryService.TRANSITION_LINK
+        }]
+      };
+      PlacesUtils.asyncHistory.updatePlaces(place, {
+        handleError: () => Cu.reportError("couldn't update history for socialmark annotation"),
+        handleResult: function () {},
+        handleCompletion: function () {
+          promiseSetAnnotation(aURI, providerList).then(function() {
+            if (aCallback)
+              schedule(function() { aCallback(true); } );
+          }).then(null, Cu.reportError);
+        }
+      });
+    }).then(null, Cu.reportError);
+  },
+
+  unmarkURI: function(origin, aURI, aCallback) {
+    // this should not be called if this.provider or the port is null
+    // set our annotation
+    promiseGetAnnotation(aURI).then(function(val) {
+      let providerList = val ? JSON.parse(val) : [];
+      let marked = providerList.indexOf(origin) >= 0;
+      if (marked) {
+        // remove the annotation
+        providerList.splice(providerList.indexOf(origin), 1);
+        promiseSetAnnotation(aURI, providerList).then(function() {
+          if (aCallback)
+            schedule(function() { aCallback(false); } );
+        }).then(null, Cu.reportError);
+      }
+    }).then(null, Cu.reportError);
   }
 };
 
+function schedule(callback) {
+  Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
+}
+
+function CreateSocialStatusWidget(aId, aProvider) {
+  if (!aProvider.statusURL)
+    return;
+  let widget = CustomizableUI.getWidget(aId);
+  // The widget is only null if we've created then destroyed the widget.
+  // Once we've actually called createWidget the provider will be set to
+  // PROVIDER_API.
+  if (widget && widget.provider == CustomizableUI.PROVIDER_API)
+    return;
+
+  CustomizableUI.createWidget({
+    id: aId,
+    type: "custom",
+    removable: true,
+    defaultArea: CustomizableUI.AREA_NAVBAR,
+    onBuild: function(aDocument) {
+      let node = aDocument.createElement("toolbarbutton");
+      node.id = this.id;
+      node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional social-status-button badged-button");
+      node.style.listStyleImage = "url(" + (aProvider.icon32URL || aProvider.iconURL) + ")";
+      node.setAttribute("origin", aProvider.origin);
+      node.setAttribute("label", aProvider.name);
+      node.setAttribute("tooltiptext", aProvider.name);
+      node.setAttribute("oncommand", "SocialStatus.showPopup(this);");
+      node.setAttribute("constrain-size", "true");
+
+      return node;
+    }
+  });
+}
+
+function CreateSocialMarkWidget(aId, aProvider) {
+  if (!aProvider.markURL)
+    return;
+  let widget = CustomizableUI.getWidget(aId);
+  // The widget is only null if we've created then destroyed the widget.
+  // Once we've actually called createWidget the provider will be set to
+  // PROVIDER_API.
+  if (widget && widget.provider == CustomizableUI.PROVIDER_API)
+    return;
+
+  CustomizableUI.createWidget({
+    id: aId,
+    type: "custom",
+    removable: true,
+    defaultArea: CustomizableUI.AREA_NAVBAR,
+    onBuild: function(aDocument) {
+      let node = aDocument.createElement("toolbarbutton");
+      node.id = this.id;
+      node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional social-mark-button");
+      node.setAttribute("type", "socialmark");
+      node.setAttribute("constrain-size", "true");
+      node.style.listStyleImage = "url(" + (aProvider.unmarkedIcon || aProvider.icon32URL || aProvider.iconURL) + ")";
+      node.setAttribute("origin", aProvider.origin);
+
+      let window = aDocument.defaultView;
+      let menuLabel = window.gNavigatorBundle.getFormattedString("social.markpageMenu.label", [aProvider.name]);
+      node.setAttribute("label", menuLabel);
+      node.setAttribute("tooltiptext", menuLabel);
+      node.setAttribute("observes", "Social:PageShareOrMark");
+
+      return node;
+    }
+  });
+}
+
+
 function sizeSocialPanelToContent(panel, iframe, requestedSize) {
   let doc = iframe.contentDocument;
   if (!doc || !doc.body) {
     return;
   }
   // We need an element to use for sizing our panel.  See if the body defines
   // an id for that element, otherwise use the body itself.
   let body = doc.body;
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -12,16 +12,17 @@ XPCSHELL_TESTS_MANIFESTS += [
 
 EXTRA_JS_MODULES += [
     'AboutHome.jsm',
     'AboutNewTab.jsm',
     'BrowserUITelemetry.jsm',
     'BrowserUsageTelemetry.jsm',
     'CaptivePortalWatcher.jsm',
     'CastingApps.jsm',
+    'Chat.jsm',
     'ContentClick.jsm',
     'ContentCrashHandlers.jsm',
     'ContentLinkHandler.jsm',
     'ContentObservers.jsm',
     'ContentSearch.jsm',
     'ContentWebRTC.jsm',
     'DirectoryLinksProvider.jsm',
     'E10SUtils.jsm',
@@ -37,17 +38,16 @@ EXTRA_JS_MODULES += [
     'ProcessHangMonitor.jsm',
     'ReaderParent.jsm',
     'RecentWindow.jsm',
     'RemotePrompt.jsm',
     'Sanitizer.jsm',
     'SelfSupportBackend.jsm',
     'SitePermissions.jsm',
     'Social.jsm',
-    'SocialService.jsm',
     'TabGroupsMigrator.jsm',
     'TransientPrefs.jsm',
     'webrtcUI.jsm',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     EXTRA_JS_MODULES += [
         'Windows8WindowFrameColor.jsm',
--- a/browser/modules/test/unit/social/head.js
+++ b/browser/modules/test/unit/social/head.js
@@ -111,100 +111,17 @@ function do_initialize_social(enabledOnS
     // expecting 2 providers installed
     do_wait_observer("social:providers-changed", function() {
       do_check_eq(Social.providers.length, 2, "2 providers installed");
       do_execute_soon(cb);
     });
   }
 
   // import and initialize everything
-  SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+  SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
   do_check_eq(enabledOnStartup, SocialService.hasEnabledProviders, "Service has enabled providers");
   Social = Cu.import("resource:///modules/Social.jsm", {}).Social;
   do_check_false(Social.initialized, "Social is not initialized");
   Social.init();
   do_check_true(Social.initialized, "Social is initialized");
   if (!enabledOnStartup)
     do_execute_soon(cb);
 }
-
-function AsyncRunner() {
-  do_test_pending();
-  do_register_cleanup(() => this.destroy());
-
-  this._callbacks = {
-    done: do_test_finished,
-    error: function (err) {
-      // xpcshell test functions like do_check_eq throw NS_ERROR_ABORT on
-      // failure.  Ignore those so they aren't rethrown here.
-      if (err !== Cr.NS_ERROR_ABORT) {
-        if (err.stack) {
-          err = err + " - See following stack:\n" + err.stack +
-                      "\nUseless do_throw stack";
-        }
-        do_throw(err);
-      }
-    },
-    consoleError: function (scriptErr) {
-      // Try to ensure the error is related to the test.
-      let filename = scriptErr.sourceName || scriptErr.toString() || "";
-      if (filename.indexOf("/toolkit/components/social/") >= 0)
-        do_throw(scriptErr);
-    },
-  };
-  this._iteratorQueue = [];
-
-  // This catches errors reported to the console, e.g., via Cu.reportError, but
-  // not on the runner's stack.
-  Cc["@mozilla.org/consoleservice;1"].
-    getService(Ci.nsIConsoleService).
-    registerListener(this);
-}
-
-AsyncRunner.prototype = {
-
-  appendIterator: function appendIterator(iter) {
-    this._iteratorQueue.push(iter);
-  },
-
-  next: function next(arg) {
-    if (!this._iteratorQueue.length) {
-      this.destroy();
-      this._callbacks.done();
-      return;
-    }
-
-    try {
-      var { done, value: val } = this._iteratorQueue[0].next(arg);
-      if (done) {
-        this._iteratorQueue.shift();
-        this.next();
-        return;
-      }
-    }
-    catch (err) {
-      this._callbacks.error(err);
-    }
-
-    // val is an iterator => prepend it to the queue and start on it
-    // val is otherwise truthy => call next
-    if (val) {
-      if (typeof(val) != "boolean")
-        this._iteratorQueue.unshift(val);
-      this.next();
-    }
-  },
-
-  destroy: function destroy() {
-    Cc["@mozilla.org/consoleservice;1"].
-      getService(Ci.nsIConsoleService).
-      unregisterListener(this);
-    this.destroy = function alreadyDestroyed() {};
-  },
-
-  observe: function observe(msg) {
-    if (msg instanceof Ci.nsIScriptError &&
-        !(msg.flags & Ci.nsIScriptError.warningFlag))
-    {
-      this._callbacks.consoleError(msg);
-    }
-  },
-};
--- a/browser/modules/test/unit/social/test_social.js
+++ b/browser/modules/test/unit/social/test_social.js
@@ -14,17 +14,17 @@ function testStartupEnabled() {
   // wait on startup before continuing
   do_check_eq(Social.providers.length, 2, "two social providers enabled");
   do_check_true(Social.providers[0].enabled, "provider 0 is enabled");
   do_check_true(Social.providers[1].enabled, "provider 1 is enabled");
   run_next_test();
 }
 
 function testDisableAfterStartup() {
-  let SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService;
+  let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
   SocialService.disableProvider(Social.providers[0].origin, function() {
     do_wait_observer("social:providers-changed", function() {
       do_check_eq(Social.enabled, false, "Social is disabled");
       do_check_eq(Social.providers.length, 0, "no social providers available");
       do_test_finished();
       run_next_test();
     });
     SocialService.disableProvider(Social.providers[0].origin)
--- a/browser/modules/test/unit/social/xpcshell.ini
+++ b/browser/modules/test/unit/social/xpcshell.ini
@@ -2,12 +2,8 @@
 head = head.js
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 support-files = blocklist.xml
 
 [test_social.js]
 [test_socialDisabledStartup.js]
-[test_SocialService.js]
-[test_SocialServiceMigration21.js]
-[test_SocialServiceMigration22.js]
-[test_SocialServiceMigration29.js]
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1359,17 +1359,16 @@ html|span.ac-emphasize-text-url {
 }
 
 #reader-mode-button:hover:active,
 #reader-mode-button[readeractive] {
   -moz-image-region: rect(0, 48px, 16px, 32px);
 }
 
 /* social share panel */
-%include ../shared/social/social.inc.css
 
 .social-share-frame {
   border-top: 1px solid #f8f8f8;
   width: 756px;
   height: 150px;
 }
 
 #share-container {
@@ -1408,16 +1407,22 @@ html|span.ac-emphasize-text-url {
   display: none;
 }
 .share-provider-button > .toolbarbutton-icon {
   width: 16px;
   min-height: 16px;
   max-height: 16px;
 }
 
+/* social recommending panel */
+
+#social-mark-button {
+  -moz-image-region: rect(0, 16px, 16px, 0);
+}
+
 /* bookmarks menu-button */
 
 #bookmarks-menu-button[cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker {
   -moz-appearance: none !important;
   -moz-box-align: center;
 }
 
 #bookmarks-menu-button[cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
@@ -1802,16 +1807,40 @@ notification.pluginVulnerable > .notific
   color: #FDF3DE;
   min-width: 16px;
   text-shadow: none;
   background-image: linear-gradient(#B4211B, #8A1915);
   border-radius: 1px;
   margin-inline-end: 2px;
 }
 
+/* social toolbar provider menu */
+#social-statusarea-popup {
+  margin-top: 0;
+  margin-left: -12px;
+  margin-right: -12px;
+}
+
+.social-statusarea-user {
+  list-style-image:url("chrome://global/skin/icons/information-32.png");
+}
+
+.social-statusarea-user-portrait {
+  width: 32px;
+  height: 32px;
+  border-radius: 2px;
+  margin: 10px;
+}
+
+.social-panel > .panel-arrowcontainer > .panel-arrowcontent {
+  padding: 0;
+}
+
+%include ../shared/social/chat.inc.css
+
 /* Customization mode */
 
 %include ../shared/customizableui/customizeMode.inc.css
 
 #main-window[customize-entered] > #tab-view-deck {
   background-image: url("chrome://browser/skin/customizableui/customizeMode-gridTexture.png"),
                     linear-gradient(to bottom, #bcbcbc, #b5b5b5);
   background-attachment: fixed;
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1987,31 +1987,34 @@ html|span.ac-emphasize-text-url {
   border-top: 1px solid #f8f8f8;
   min-width: 756px;
   height: 150px;
   /* we resize our panels dynamically, make it look nice */
 }
 
 #share-container {
   min-width: 756px;
+  background-color: white;
   background-repeat: no-repeat;
   background-position: center center;
 }
 #share-container[loading] {
   background-image: url(chrome://browser/skin/tabbrowser/pendingpaint.png);
 }
 #share-container > browser {
   transition: opacity 150ms ease-in-out;
   opacity: 1;
 }
 #share-container[loading] > browser {
   opacity: 0;
 }
 
-#manage-share-providers {
+#manage-share-providers,
+#social-sidebar-button:hover,
+#social-sidebar-button:hover:active {
   -moz-image-region: rect(18px, 468px, 36px, 450px);
 }
 
 .social-share-toolbar {
   border-bottom: 1px solid #dedede;
   padding: 2px;
 }
 
@@ -2030,16 +2033,22 @@ html|span.ac-emphasize-text-url {
 }
 
 .share-provider-button > .toolbarbutton-icon {
   width: 16px;
   min-height: 16px;
   max-height: 16px;
 }
 
+/* social recommending panel */
+
+#social-mark-button {
+  -moz-image-region: rect(0, 16px, 16px, 0);
+}
+
 /* BOOKMARKING PANEL */
 #editBookmarkPanelStarIcon {
   list-style-image: url("chrome://browser/skin/places/starred48.png");
   width: 48px;
   height: 48px;
 }
 
 #editBookmarkPanelStarIcon[unstarred] {
@@ -2950,16 +2959,20 @@ menuitem:hover > hbox > .alltabs-endimag
 
 %include ../shared/notification-icons.inc.css
 
 .notification-anchor-icon:-moz-focusring {
   box-shadow: 0 0 2px 1px -moz-mac-focusring inset,
               0 0 3px 2px -moz-mac-focusring;
 }
 
+#social-notification-icon > .toolbarbutton-icon {
+  height: 16px;
+}
+
 /* Translation */
 
 %include ../shared/translation/infobar.inc.css
 
 notification[value="translation"] {
   color: #484848;
   background-color: #EFEFEF;
   background-image: none;
@@ -3205,18 +3218,39 @@ menulist.translate-infobar-element > .me
 #developer-toolbar-toolbox-button[error-count]:before {
   color: #FDF3DE;
   min-width: 16px;
   text-shadow: none;
   background-image: linear-gradient(#B4211B, #8A1915);
   border-radius: 1px;
 }
 
-/* Share */
-%include ../shared/social/social.inc.css
+/* === end of social toolbar button === */
+
+/* === social toolbar provider menu  === */
+
+.social-statusarea-user {
+  list-style-image:url("chrome://global/skin/icons/information-32.png");
+}
+
+.social-statusarea-user-portrait {
+  width: 32px;
+  height: 32px;
+  margin: 4px;
+  margin-inline-start: 0;
+}
+
+.social-panel > .panel-arrowcontainer > .panel-arrowcontent {
+  padding: 0;
+}
+
+/* fixup rounded corners for osx panels */
+.social-panel > .social-panel-frame {
+  border-radius: inherit;
+}
 
 #social-share-panel {
   min-height: 100px;
   min-width: 300px;
   transition: height .3s ease-in-out, width .3s ease-in-out;
 }
 
 #share-container,
@@ -3232,16 +3266,20 @@ menulist.translate-infobar-element > .me
   border-top-right-radius: inherit;
 }
 
 #social-share-provider-buttons {
   border-top-left-radius: inherit;
   border-top-right-radius: inherit;
 }
 
+/* === end of social toolbar provider menu === */
+
+%include ../shared/social/chat.inc.css
+
 /* Customization mode */
 
 %include ../shared/customizableui/customizeMode.inc.css
 
 #main-window[customizing] {
   background-color: rgb(178,178,178);
 }
 
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -94,16 +94,17 @@
   skin/classic/browser/badge-add-engine.png                    (../shared/search/badge-add-engine.png)
   skin/classic/browser/badge-add-engine@2x.png                 (../shared/search/badge-add-engine@2x.png)
   skin/classic/browser/search-indicator-badge-add.png          (../shared/search/search-indicator-badge-add.png)
   skin/classic/browser/search-indicator-badge-add@2x.png       (../shared/search/search-indicator-badge-add@2x.png)
   skin/classic/browser/search-history-icon.svg                 (../shared/search/history-icon.svg)
   skin/classic/browser/search-indicator-magnifying-glass.svg   (../shared/search/search-indicator-magnifying-glass.svg)
   skin/classic/browser/search-arrow-go.svg                     (../shared/search/search-arrow-go.svg)
   skin/classic/browser/gear.svg                                (../shared/search/gear.svg)
+  skin/classic/browser/social/chat-icons.svg                   (../shared/social/chat-icons.svg)
   skin/classic/browser/social/gear_default.png                 (../shared/social/gear_default.png)
   skin/classic/browser/social/gear_clicked.png                 (../shared/social/gear_clicked.png)
   skin/classic/browser/tabbrowser/connecting.png               (../shared/tabbrowser/connecting.png)
   skin/classic/browser/tabbrowser/connecting@2x.png            (../shared/tabbrowser/connecting@2x.png)
   skin/classic/browser/tabbrowser/crashed.svg                  (../shared/tabbrowser/crashed.svg)
   skin/classic/browser/tabbrowser/pendingpaint.png             (../shared/tabbrowser/pendingpaint.png)
 * skin/classic/browser/tabbrowser/tab-audio.svg                (../shared/tabbrowser/tab-audio.svg)
   skin/classic/browser/tabbrowser/tab-audio-small.svg          (../shared/tabbrowser/tab-audio-small.svg)
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/social/chat-icons.svg
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-3 -3 16 16">
+  <style>
+    use:not(:target) {
+      display: none;
+    }
+    use {
+      fill: #666;
+    }
+    use[id$="-hover"] {
+      fill: #4a4a4a;
+    }
+    use[id$="-active"] {
+      fill: #4a4a4a;
+    }
+    use[id$="-disabled"] {
+      fill: #666;
+    }
+    use[id$="-white"] {
+      fill: #fff;
+    }
+  </style>
+  <defs>
+    <polygon id="close-shape" points="10,1.717 8.336,0.049 5.024,3.369 1.663,0 0,1.668 3.36,5.037 0.098,8.307 1.762,9.975 5.025,6.705 8.311,10 9.975,8.332 6.688,5.037"/>
+    <path id="dropdown-shape" fill-rule="evenodd" d="M9,3L4.984,7L1,3H9z"/>
+    <g id="expand-shape">
+      <path fill-rule="evenodd" d="M9.429,7.072v2.143c0,0.531-0.188,0.985-0.566,1.363c-0.377,0.377-0.832,0.565-1.363,0.565H1.929 c-0.531,0-0.986-0.188-1.363-0.565C0.188,10.2,0,9.746,0,9.214V3.643c0-0.531,0.188-0.985,0.566-1.362 c0.377-0.378,0.832-0.566,1.363-0.566h4.714c0.062,0,0.114,0.021,0.154,0.061s0.06,0.092,0.06,0.154v0.428 c0,0.063-0.02,0.114-0.06,0.154S6.705,2.572,6.643,2.572H1.929c-0.295,0-0.547,0.104-0.757,0.314S0.857,3.348,0.857,3.643v5.571 c0,0.295,0.105,0.547,0.315,0.757s0.462,0.314,0.757,0.314H7.5c0.294,0,0.547-0.104,0.757-0.314 c0.209-0.21,0.314-0.462,0.314-0.757V7.072c0-0.062,0.02-0.114,0.061-0.154c0.04-0.04,0.091-0.061,0.154-0.061h0.428 c0.062,0,0.114,0.021,0.154,0.061S9.429,7.009,9.429,7.072z"/>
+      <path fill-rule="evenodd" d="M7.07,5.82L6.179,4.93C6.127,4.878,6.101,4.818,6.101,4.75s0.026-0.128,0.079-0.18l2.594-2.594L7.648,0.852 C7.549,0.753,7.5,0.636,7.5,0.5s0.049-0.252,0.148-0.351S7.864,0,8,0h3.5c0.136,0,0.252,0.05,0.351,0.149S12,0.365,12,0.5V4 c0,0.136-0.05,0.253-0.149,0.351C11.752,4.451,11.635,4.5,11.5,4.5c-0.136,0-0.253-0.05-0.352-0.149l-1.124-1.125L7.429,5.82 c-0.052,0.052-0.112,0.079-0.18,0.079"/>
+    </g>
+    <rect id="minimize-shape" y="7.5" width="10" height="2.2"/>
+    <path id="exit-shape" fill-rule="evenodd" d="M5.01905144,3.00017279 C5.01277908,3.00005776 5.0064926,3 5.00019251,3 L1.99980749,3 C1.44371665,3 1,3.44762906 1,3.99980749 L1,7.00019251 C1,7.55628335 1.44762906,8 1.99980749,8 L5.00019251,8 C5.00649341,8 5.01277988,7.99994253 5.01905144,7.99982809 L5.01905144,8.5391818 C5.01905144,10.078915 5.37554713,10.2645548 5.81530684,9.9314625 L10.8239665,6.13769619 C11.2653143,5.80340108 11.2637262,5.26455476 10.8239665,4.93146254 L5.81530684,1.13769619 C5.37395904,0.80340108 5.01905144,0.98023404 5.01905144,1.52997693 L5.01905144,3.00017279 Z M-1,1 L4,1 L4,2 L0,2 L0,9 L4,9 L4,10.0100024 L-1,10.0100021 L-1,1 Z" />
+  </defs>
+  <use id="close" xlink:href="#close-shape"/>
+  <use id="close-active" xlink:href="#close-shape"/>
+  <use id="close-disabled" xlink:href="#close-shape"/>
+  <use id="close-hover" xlink:href="#close-shape"/>
+  <use id="exit-white" xlink:href="#exit-shape"/>
+  <use id="expand" xlink:href="#expand-shape"/>
+  <use id="expand-active" xlink:href="#expand-shape"/>
+  <use id="expand-disabled" xlink:href="#expand-shape"/>
+  <use id="expand-hover" xlink:href="#expand-shape"/>
+  <use id="expand-white" xlink:href="#expand-shape"/>
+  <use id="minimize" xlink:href="#minimize-shape"/>
+  <use id="minimize-active" xlink:href="#minimize-shape"/>
+  <use id="minimize-disabled" xlink:href="#minimize-shape"/>
+  <use id="minimize-hover" xlink:href="#minimize-shape"/>
+  <use id="minimize-white" xlink:href="#minimize-shape"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/social/chat.inc.css
@@ -0,0 +1,238 @@
+%if 0
+/* 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/. */
+%endif
+
+#social-sidebar-header {
+  padding: 3px;
+}
+
+#manage-share-providers,
+#social-sidebar-button {
+  list-style-image: url("chrome://browser/skin/Toolbar.png");
+  -moz-image-region: rect(0, 468px, 18px, 450px);
+}
+
+#social-sidebar-button {
+  -moz-appearance: none;
+  border: none;
+  padding: 0;
+  margin: 2px;
+}
+#manage-share-providers > .toolbarbutton-icon,
+#social-sidebar-button > .toolbarbutton-icon {
+  min-height: 18px;
+  min-width: 18px;
+}
+
+#social-sidebar-button > .toolbarbutton-menu-dropmarker {
+  display: none;
+}
+
+#social-sidebar-button[loading="true"] {
+  list-style-image: url("chrome://global/skin/icons/loading.png");
+}
+
+#social-sidebar-favico {
+  max-height: 16px;
+  max-width: 16px;
+  padding: 0;
+  margin: 2px;
+}
+
+.chat-status-icon {
+  max-height: 16px;
+  max-width: 16px;
+  padding: 0;
+}
+
+.chat-toolbarbutton {
+  -moz-appearance: none;
+  border: none;
+  padding: 0 3px;
+  margin: 0;
+  background: none;
+}
+
+.chat-toolbarbutton > .toolbarbutton-text {
+  display: none;
+}
+
+.chat-toolbarbutton > .toolbarbutton-icon {
+  width: 16px;
+  height: 16px;
+}
+
+.chat-close-button {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#close");
+}
+
+.chat-close-button:hover {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#close-hover");
+}
+
+.chat-close-button:hover:active {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#close-active");
+}
+
+.chat-minimize-button {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize");
+}
+
+.chat-minimize-button:hover {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize-hover");
+}
+
+.chat-minimize-button:hover:active {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize-active");
+}
+
+.chat-swap-button {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#expand");
+  transform: rotate(180deg);
+}
+
+.chat-swap-button:hover {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#expand-hover");
+}
+
+.chat-swap-button:hover:active {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#expand-active");
+}
+
+chatbar > chatbox > .chat-titlebar > .chat-swap-button {
+  transform: none;
+}
+
+.chat-title {
+  color: #666;
+  text-shadow: none;
+  cursor: inherit;
+}
+
+.chat-titlebar {
+  height: 26px;
+  min-height: 26px;
+  width: 100%;
+  margin: 0;
+  padding: 5px 4px;
+  border: 1px solid #ebebeb;
+  border-bottom: 0;
+  border-top-left-radius: 4px;
+  border-top-right-radius: 4px;
+  cursor: pointer;
+  background-color: #ebebeb;
+}
+
+.chat-titlebar[selected] {
+  background-color: #f0f0f0;
+}
+
+.chat-titlebar > .notification-anchor-icon {
+  margin-left: 2px;
+  margin-right: 2px;
+}
+
+.chat-titlebar[minimized="true"] {
+  border-bottom: none;
+}
+
+.chat-titlebar[activity] {
+  background-image: radial-gradient(ellipse closest-side at center, rgb(255,255,255), transparent);
+  background-repeat: no-repeat;
+  background-size: 100% 20px;
+  background-position: 0 -10px;
+}
+
+.chat-frame {
+  padding: 0;
+  margin: 0;
+  overflow: hidden;
+}
+
+.chatbar-button {
+  list-style-image: url("chrome://browser/skin/social/services-16.png");
+  margin: 0;
+  padding: 2px;
+  height: 21px;
+  width: 21px;
+  border: 1px solid #ccc;
+  border-bottom: none;
+  background-color: #d9d9d9;
+  background-image: linear-gradient(rgba(255,255,255,.43), transparent);
+  border-top-left-radius: 3px;
+  border-top-right-radius: 3px;
+}
+
+@media (min-resolution: 2dppx) {
+  .chatbar-button {
+    list-style-image: url("chrome://browser/skin/social/services-16@2x.png");
+  }
+}
+
+.chatbar-button:hover,
+.chatbar-button[open="true"] {
+  background-color: #f0f0f0;
+}
+
+.chatbar-button[activity]:not([open]) {
+  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%);
+}
+
+.chatbar-button > .toolbarbutton-icon {
+  width: 16px;
+}
+
+.chatbar-button > menupopup > .menuitem-iconic > .menu-iconic-left > .menu-iconic-icon {
+  width: auto;
+  height: auto;
+  max-height: 16px;
+  max-width: 16px;
+}
+
+.chatbar-button[open="true"] {
+  box-shadow: inset 0 2px 5px rgba(0,0,0,0.6), 0 1px rgba(255,255,255,0.2);
+}
+
+.chatbar-button > .toolbarbutton-text,
+.chatbar-button > .toolbarbutton-menu-dropmarker {
+  display: none;
+}
+
+.chatbar-button > menupopup > menuitem[activity] {
+  font-weight: bold;
+}
+
+.chatbar-innerbox {
+  background: transparent;
+  overflow: hidden;
+}
+
+chatbar {
+  margin-inline-end: 20px;
+}
+
+chatbox {
+  margin-inline-start: 4px;
+  background-color: transparent;
+}
+
+chatbar > chatbox {
+  /* Apply the same border-radius as the .chat-titlebar to make the box-shadow
+     go round nicely. */
+  border-top-left-radius: 4px;
+  border-top-right-radius: 4px;
+  box-shadow: 0 0 5px rgba(0,0,0,.3);
+  /* Offset the chatbox the same amount as the box-shadows' spread, to make it
+     visible. */
+  margin-inline-end: 5px;
+}
+
+window > chatbox {
+  margin-inline-start: 0px;
+  margin: 0px;
+  border: none;
+  padding: 0px;
+  border-radius: 4px;
+}
deleted file mode 100644
--- a/browser/themes/shared/social/social.inc.css
+++ /dev/null
@@ -1,23 +0,0 @@
-%if 0
-/* 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/. */
-%endif
-
-#manage-share-providers {
-  list-style-image: url("chrome://browser/skin/Toolbar.png");
-  -moz-image-region: rect(0, 468px, 18px, 450px);
-}
-
-#manage-share-providers > .toolbarbutton-icon {
-  min-height: 18px;
-  min-width: 18px;
-}
-
-.social-panel > .panel-arrowcontainer > .panel-arrowcontent {
-  padding: 0;
-}
-/* fixup corners for share panel */
-.social-panel > .social-panel-frame {
-  border-radius: inherit;
-}
--- a/browser/themes/windows/browser-aero.css
+++ b/browser/themes/windows/browser-aero.css
@@ -49,17 +49,19 @@
   @media (-moz-os-version: windows-vista),
          (-moz-os-version: windows-win7) {
     .sidebar-header:not(:-moz-lwtheme),
     #sidebar-header:not(:-moz-lwtheme) {
       background-color: #EEF3FA;
     }
 
     .sidebar-splitter,
-    #appcontent ~ .sidebar-splitter {
+    #appcontent ~ .sidebar-splitter,
+    .chatbar-button,
+    chatbar > chatbox {
       border-color: #A9B7C9;
     }
 
     #navigator-toolbox > toolbar:not(#toolbar-menubar):not(#TabsToolbar):not(:-moz-lwtheme),
     #browser-bottombox:not(:-moz-lwtheme),
     .browserContainer > findbar {
       background-color: @customToolbarColor@;
     }
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1799,21 +1799,16 @@ html|span.ac-emphasize-text-url {
 }
 
 #reader-mode-button:hover:active,
 #reader-mode-button[readeractive] {
   -moz-image-region: rect(0, 48px, 16px, 32px);
 }
 
 /* social share panel */
-%include ../shared/social/social.inc.css
-
-.social-panel-frame {
-  border-radius: inherit;
-}
 
 .social-share-frame {
   min-width: 756px;
   height: 150px;
 }
 #share-container {
   min-width: 756px;
   background-color: white;
@@ -1850,16 +1845,22 @@ html|span.ac-emphasize-text-url {
   display: none;
 }
 .share-provider-button > .toolbarbutton-icon {
   width: 16px;
   min-height: 16px;
   max-height: 16px;
 }
 
+
+/* fixup corners for share panel */
+.social-panel > .social-panel-frame {
+  border-radius: inherit;
+}
+
 #social-share-panel {
   min-height: 100px;
   min-width: 766px;
 }
 
 #share-container,
 .social-share-frame {
   border-top-left-radius: 0;
@@ -1873,16 +1874,22 @@ html|span.ac-emphasize-text-url {
   border-top-right-radius: inherit;
 }
 
 #social-share-provider-buttons {
   border-top-left-radius: inherit;
   border-top-right-radius: inherit;
 }
 
+/* social recommending panel */
+
+#social-mark-button {
+  -moz-image-region: rect(0, 16px, 16px, 0);
+}
+
 /* bookmarks menu-button */
 
 #nav-bar #bookmarks-menu-button[cui-areatype="toolbar"]:not([overflowedItem=true]) > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
   padding-top: var(--toolbarbutton-vertical-inner-padding);
   padding-bottom: var(--toolbarbutton-vertical-inner-padding);
 }
 
 #BMB_bookmarksPopup[side="top"],
@@ -2452,16 +2459,63 @@ notification.pluginVulnerable > .notific
   color: #FDF3DE;
   min-width: 16px;
   text-shadow: none;
   background-image: linear-gradient(#B4211B, #8A1915);
   border-radius: 1px;
   margin-inline-end: 5px;
 }
 
+/* social toolbar provider menu */
+.social-statusarea-popup {
+  margin-top: 0;
+  margin-left: -12px;
+  margin-right: -12px;
+}
+
+.social-statusarea-user {
+  -moz-appearance: none;
+  border-bottom: 1px solid rgb(221,221,221);
+  background-color: -moz-Dialog;
+  position: relative;
+  cursor: pointer;
+  list-style-image:url("chrome://global/skin/icons/information-32.png");
+}
+
+.social-statusarea-user-portrait {
+  width: 32px;
+  height: 32px;
+  border-radius: 2px;
+  margin: 10px;
+}
+
+.social-statusarea-loggedInStatus {
+  -moz-appearance: none;
+  background: transparent;
+  border: none;
+  color: -moz-nativehyperlinktext;
+  min-width: 0;
+  margin: 0 6px;
+  list-style-image: none;
+}
+
+.social-statusarea-user[_moz-menuactive] > vbox > .social-statusarea-loggedInStatus {
+  text-decoration: underline;
+}
+
+.social-panel > .panel-arrowcontainer > .panel-arrowcontent {
+  padding: 0;
+}
+
+.social-panel-frame {
+  border-radius: inherit;
+}
+
+%include ../shared/social/chat.inc.css
+
 /* Customization mode */
 
 %include ../shared/customizableui/customizeMode.inc.css
 
 /**
  * This next rule is a hack to disable subpixel anti-aliasing on all
  * labels during the customize mode transition. Subpixel anti-aliasing
  * on Windows with Direct2D layers acceleration is particularly slow to
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -609,17 +609,24 @@ Tester.prototype = {
             // This will prevent false positives for tests that were the last
             // to touch the sidebar. They will thus not be blamed for leaking
             // a document.
             let sidebar = document.getElementById("sidebar");
             sidebar.setAttribute("src", "data:text/html;charset=utf-8,");
             sidebar.docShell.createAboutBlankContentViewer(null);
             sidebar.setAttribute("src", "about:blank");
 
+            // Do the same for the social sidebar.
+            let socialSidebar = document.getElementById("social-sidebar-browser");
+            socialSidebar.setAttribute("src", "data:text/html;charset=utf-8,");
+            socialSidebar.docShell.createAboutBlankContentViewer(null);
+            socialSidebar.setAttribute("src", "about:blank");
+
             SelfSupportBackend.uninit();
+            SocialFlyout.unload();
             SocialShare.uninit();
           }
 
           // Destroy BackgroundPageThumbs resources.
           let {BackgroundPageThumbs} =
             Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", {});
           BackgroundPageThumbs._destroy();
 
--- a/toolkit/components/moz.build
+++ b/toolkit/components/moz.build
@@ -70,16 +70,19 @@ if CONFIG['MOZ_BUILD_APP'] != 'mobile/an
     DIRS += ['narrate', 'viewsource'];
 
     if CONFIG['NS_PRINTING']:
         DIRS += ['printing']
 
 if CONFIG['MOZ_CRASHREPORTER']:
     DIRS += ['crashes']
 
+if CONFIG['MOZ_SOCIAL']:
+    DIRS += ['social']
+
 if CONFIG['BUILD_CTYPES']:
     DIRS += ['ctypes']
 
 if CONFIG['MOZ_FEEDS']:
     DIRS += ['feeds']
 
 if CONFIG['MOZ_XUL']:
     DIRS += ['autocomplete', 'satchel']
new file mode 100644
--- /dev/null
+++ b/toolkit/components/social/MozSocialAPI.jsm
@@ -0,0 +1,298 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { 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",
+  "hookWindowCloseForPanelClose"
+];
+
+this.MozSocialAPI = {
+  _enabled: false,
+  _everEnabled: false,
+  set enabled(val) {
+    let enable = !!val;
+    if (enable == this._enabled) {
+      return;
+    }
+    this._enabled = enable;
+
+    if (enable) {
+      Services.obs.addObserver(injectController, "document-element-inserted", false);
+
+      if (!this._everEnabled) {
+        this._everEnabled = true;
+        Services.telemetry.getHistogramById("SOCIAL_ENABLED_ON_SESSION").add(true);
+      }
+
+    } else {
+      Services.obs.removeObserver(injectController, "document-element-inserted");
+    }
+  }
+};
+
+// Called on document-element-inserted, checks that the API should be injected,
+// and then calls attachToWindow as appropriate
+function injectController(doc, topic, data) {
+  try {
+    let window = doc.defaultView;
+    if (!window || PrivateBrowsingUtils.isContentWindowPrivate(window))
+      return;
+
+    // Do not attempt to load the API into about: error pages
+    if (doc.documentURIObject.scheme == "about") {
+      return;
+    }
+
+    let containingBrowser = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                                  .getInterface(Ci.nsIWebNavigation)
+                                  .QueryInterface(Ci.nsIDocShell)
+                                  .chromeEventHandler;
+    // limit injecting into social panels or same-origin browser tabs if
+    // social.debug.injectIntoTabs is enabled
+    let allowTabs = false;
+    try {
+      allowTabs = containingBrowser.contentWindow == window &&
+                  Services.prefs.getBoolPref("social.debug.injectIntoTabs");
+    } catch(e) {}
+
+    let origin = containingBrowser.getAttribute("origin");
+    if (!allowTabs && !origin) {
+      return;
+    }
+
+    // we always handle window.close on social content, even if they are not
+    // "enabled".
+    hookWindowCloseForPanelClose(window);
+
+    SocialService.getProvider(doc.nodePrincipal.origin, function(provider) {
+      if (provider && provider.enabled) {
+        attachToWindow(provider, window);
+      }
+    });
+  } catch(e) {
+    Cu.reportError("MozSocialAPI injectController: unable to attachToWindow for " + doc.location + ": " + e);
+  }
+}
+
+// Loads mozSocial support functions associated with provider into targetWindow
+function attachToWindow(provider, targetWindow) {
+  // If the loaded document isn't from the provider's origin (or a protocol
+  // that inherits the principal), don't attach the mozSocial API.
+  let targetDocURI = targetWindow.document.documentURIObject;
+  if (!provider.isSameOrigin(targetDocURI)) {
+    let msg = "MozSocialAPI: not attaching mozSocial API for " + provider.origin +
+              " to " + targetDocURI.spec + " since origins differ."
+    Services.console.logStringMessage(msg);
+    return;
+  }
+
+  let mozSocialObj = {
+    openChatWindow: {
+      enumerable: true,
+      configurable: true,
+      writable: true,
+      value: function(toURL, callback) {
+        let url = targetWindow.document.documentURIObject.resolve(toURL);
+        let dwu = getChromeWindow(targetWindow)
+          .QueryInterface(Ci.nsIInterfaceRequestor)
+          .getInterface(Ci.nsIDOMWindowUtils);
+        openChatWindow(targetWindow, provider, url, chatWin => {
+          if (chatWin && dwu.isHandlingUserInput)
+            chatWin.focus();
+          if (callback)
+            callback(!!chatWin);
+        });
+      }
+    },
+    openPanel: {
+      enumerable: true,
+      configurable: true,
+      writable: true,
+      value: function(toURL, offset, callback) {
+        let chromeWindow = getChromeWindow(targetWindow);
+        if (!chromeWindow.SocialFlyout)
+          return;
+        let url = targetWindow.document.documentURIObject.resolve(toURL);
+        if (!provider.isSameOrigin(url))
+          return;
+        chromeWindow.SocialFlyout.open(url, offset, callback);
+      }
+    },
+    closePanel: {
+      enumerable: true,
+      configurable: true,
+      writable: true,
+      value: function(toURL, offset, callback) {
+        let chromeWindow = getChromeWindow(targetWindow);
+        if (!chromeWindow.SocialFlyout || !chromeWindow.SocialFlyout.panel)
+          return;
+        chromeWindow.SocialFlyout.panel.hidePopup();
+      }
+    },
+    // allow a provider to share to other providers through the browser
+    share: {
+      enumerable: true,
+      configurable: true,
+      writable: true,
+      value: function(data) {
+        let chromeWindow = getChromeWindow(targetWindow);
+        if (!chromeWindow.SocialShare || chromeWindow.SocialShare.shareButton.hidden)
+          throw new Error("Share is unavailable");
+        // ensure user action initates the share
+        let dwu = chromeWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIDOMWindowUtils);
+        if (!dwu.isHandlingUserInput)
+          throw new Error("Attempt to share without user input");
+
+        // limit to a few params we want to support for now
+        let dataOut = {};
+        for (let sub of ["url", "title", "description", "source"]) {
+          dataOut[sub] = data[sub];
+        }
+        if (data.image)
+          dataOut.previews = [data.image];
+
+        chromeWindow.SocialShare.sharePage(null, dataOut);
+      }
+    },
+    getAttention: {
+      enumerable: true,
+      configurable: true,
+      writable: true,
+      value: function() {
+        getChromeWindow(targetWindow).getAttention();
+      }
+    },
+    isVisible: {
+      enumerable: true,
+      configurable: true,
+      get: function() {
+        return targetWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                           .getInterface(Ci.nsIWebNavigation)
+                           .QueryInterface(Ci.nsIDocShell).isActive;
+      }
+    }
+  };
+
+  let contentObj = Cu.createObjectIn(targetWindow);
+  Object.defineProperties(contentObj, mozSocialObj);
+  Cu.makeObjectPropsNormal(contentObj);
+
+  targetWindow.navigator.wrappedJSObject.__defineGetter__("mozSocial", function() {
+    // We do this in a getter, so that we create these objects
+    // only on demand (this is a potential concern, since
+    // otherwise we might add one per iframe, and keep them
+    // alive for as long as the window is alive).
+    delete targetWindow.navigator.wrappedJSObject.mozSocial;
+    return targetWindow.navigator.wrappedJSObject.mozSocial = contentObj;
+  });
+}
+
+function hookWindowCloseForPanelClose(targetWindow) {
+  let _mozSocialDOMWindowClose;
+
+  if ("messageManager" in targetWindow) {
+    let _mozSocialSwapped;
+    let mm = targetWindow.messageManager;
+    mm.sendAsyncMessage("Social:HookWindowCloseForPanelClose");
+    mm.addMessageListener("Social:DOMWindowClose", _mozSocialDOMWindowClose = function() {
+      targetWindow.removeEventListener("SwapDocShells", _mozSocialSwapped);
+      closePanel(targetWindow);
+    });
+
+    targetWindow.addEventListener("SwapDocShells", _mozSocialSwapped = function(ev) {
+      targetWindow.removeEventListener("SwapDocShells", _mozSocialSwapped);
+
+      targetWindow = ev.detail;
+      targetWindow.messageManager.addMessageListener("Social:DOMWindowClose", _mozSocialDOMWindowClose);
+    });
+    return;
+  }
+
+  // We allow window.close() to close the panel, so add an event handler for
+  // this, then cancel the event (so the window itself doesn't die) and
+  // close the panel instead.
+  // However, this is typically affected by the dom.allow_scripts_to_close_windows
+  // preference, but we can avoid that check by setting a flag on the window.
+  let dwu = targetWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Ci.nsIDOMWindowUtils);
+  dwu.allowScriptsToClose();
+
+  targetWindow.addEventListener("DOMWindowClose", _mozSocialDOMWindowClose = function(evt) {
+    let elt = targetWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                .getInterface(Ci.nsIWebNavigation)
+                .QueryInterface(Ci.nsIDocShell)
+                .chromeEventHandler;
+    closePanel(elt);
+    // preventDefault stops the default window.close() function being called,
+    // which doesn't actually close anything but causes things to get into
+    // a bad state (an internal 'closed' flag is set and debug builds start
+    // asserting as the window is used.).
+    // None of the windows we inject this API into are suitable for this
+    // default close behaviour, so even if we took no action above, we avoid
+    // the default close from doing anything.
+    evt.preventDefault();
+  }, true);
+}
+
+function closePanel(elt) {
+  while (elt) {
+    if (elt.localName == "panel") {
+      elt.hidePopup();
+      break;
+    } else if (elt.localName == "chatbox") {
+      elt.close();
+      break;
+    }
+    elt = elt.parentNode;
+  }
+}
+
+function schedule(callback) {
+  Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
+}
+
+function getChromeWindow(contentWin) {
+  return contentWin.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIWebNavigation)
+                   .QueryInterface(Ci.nsIDocShellTreeItem)
+                   .rootTreeItem
+                   .QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIDOMWindow);
+}
+
+this.openChatWindow =
+ 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;
+  }
+
+  let chatbox = Chat.open(contentWindow, {
+    origin: provider.origin,
+    title: provider.name,
+    url: fullURI.spec,
+    mode: mode
+  });
+  if (callback) {
+    chatbox.promiseChatLoaded.then(() => {
+      callback(chatbox);
+    });
+  }
+}
+
+this.closeAllChatWindows = function closeAllChatWindows(provider) {
+  return Chat.closeAll(provider.origin);
+}
rename from browser/modules/SocialService.jsm
rename to toolkit/components/social/SocialService.jsm
--- a/browser/modules/SocialService.jsm
+++ b/toolkit/components/social/SocialService.jsm
@@ -11,16 +11,18 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
 const URI_EXTENSION_STRINGS  = "chrome://mozapps/locale/extensions/extensions.properties";
 const ADDON_TYPE_SERVICE     = "service";
 const ID_SUFFIX              = "@services.mozilla.org";
 const STRING_TYPE_NAME       = "type.%ID%.name";
 
+XPCOMUtils.defineLazyModuleGetter(this, "MozSocialAPI", "resource://gre/modules/MozSocialAPI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "closeAllChatWindows", "resource://gre/modules/MozSocialAPI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "etld",
                                    "@mozilla.org/network/effective-tld-service;1",
                                    "nsIEffectiveTLDService");
 
 /**
  * The SocialService is the public API to social providers - it tracks which
@@ -134,16 +136,17 @@ var SocialServiceInternal = {
 
 XPCOMUtils.defineLazyGetter(SocialServiceInternal, "providers", function () {
   initService();
   let providers = {};
   for (let manifest of this.manifests) {
     try {
       if (ActiveProviders.has(manifest.origin)) {
         // enable the api when a provider is enabled
+        MozSocialAPI.enabled = true;
         let provider = new SocialProvider(manifest);
         providers[provider.origin] = provider;
       }
     } catch (err) {
       Cu.reportError("SocialService: failed to load provider: " + manifest.origin +
                      ", exception: " + err);
     }
   }
@@ -396,16 +399,17 @@ this.SocialService = {
   },
 
   // Adds a provider given a manifest, and returns the added provider.
   addProvider: function addProvider(manifest, onDone) {
     if (SocialServiceInternal.providers[manifest.origin])
       throw new Error("SocialService.addProvider: provider with this origin already exists");
 
     // enable the api when a provider is enabled
+    MozSocialAPI.enabled = true;
     let provider = new SocialProvider(manifest);