Bug 755136: implement social sidebar, r=gavin
authorShane Caraveo <mixedpuppy@gmail.com>
Wed, 18 Jul 2012 11:40:05 -0700
changeset 105232 3a05d298599e9dc3e7f652ff36b00bbec9f85fe4
parent 105231 2585b74429d0e93e867d8ae8d2bfa71ccd36caca
child 105233 d2265454057b702decbcc26e15a8946e16f4e4b9
push idunknown
push userunknown
push dateunknown
reviewersgavin
bugs755136
milestone17.0a1
Bug 755136: implement social sidebar, r=gavin
browser/app/profile/firefox.js
browser/base/content/browser-sets.inc
browser/base/content/browser-social.js
browser/base/content/browser.xul
browser/base/content/test/Makefile.in
browser/base/content/test/browser_shareButton.js
browser/base/content/test/browser_social_sidebar.js
browser/base/content/test/browser_social_toolbar.js
browser/base/content/test/head.js
browser/locales/en-US/chrome/browser/browser.dtd
browser/modules/Social.jsm
toolkit/components/social/SocialService.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1179,9 +1179,10 @@ pref("pdfjs.previousHandler.preferredAct
 pref("pdfjs.previousHandler.alwaysAskBeforeHandling", false);
 
 // The maximum amount of decoded image data we'll willingly keep around (we
 // might keep around more than this, but we'll try to get down to this value).
 // (This is intentionally on the high side; see bug 746055.)
 pref("image.mem.max_decoded_image_kb", 256000);
 
 // Example social provider
-pref("social.manifest.motown", "{\"origin\":\"https://motown-dev.mozillalabs.com\",\"name\":\"MoTown\",\"workerURL\":\"https://motown-dev.mozillalabs.com/social/worker.js\",\"iconURL\":\"https://motown-dev.mozillalabs.com/images/motown-icon.png\"}");
+pref("social.manifest.motown", "{\"origin\":\"https://motown-dev.mozillalabs.com\",\"name\":\"MoTown\",\"workerURL\":\"https://motown-dev.mozillalabs.com/social/worker.js\",\"iconURL\":\"https://motown-dev.mozillalabs.com/images/motown-icon.png\",\"sidebarURL\":\"https://motown-dev.mozillalabs.com/social/sidebar\"}");
+pref("social.sidebar.open", true);
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -102,16 +102,17 @@
     <command id="Tools:Sanitize"
      oncommand="Cc['@mozilla.org/browser/browserglue;1'].getService(Ci.nsIBrowserGlue).sanitize(window);"/>
     <command id="Tools:PrivateBrowsing" oncommand="gPrivateBrowsingUI.toggleMode();"/>
     <command id="History:UndoCloseTab" oncommand="undoCloseTab();"/>
     <command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/>
     <command id="Browser:ToggleAddonBar" oncommand="toggleAddonBar();"/>
     <command id="Social:SharePage" oncommand="SocialShareButton.sharePage();"/>
     <command id="Social:UnsharePage" oncommand="SocialShareButton.unsharePage();"/>
+    <command id="Social:ToggleSidebar" oncommand="Social.toggleSidebar();"/>
   </commandset>
 
   <commandset id="placesCommands">
     <command id="Browser:ShowAllBookmarks"
              oncommand="PlacesCommandHook.showPlacesOrganizer('AllBookmarks');"/>
     <command id="Browser:ShowAllHistory"
              oncommand="PlacesCommandHook.showPlacesOrganizer('History');"/>
   </commandset>
@@ -176,16 +177,17 @@
     <broadcaster id="singleFeedMenuitemState" disabled="true"/>
     <broadcaster id="multipleFeedsMenuState" hidden="true"/>
     <broadcaster id="tabviewGroupsNumber" groups="1"/>
 #ifdef MOZ_SERVICES_SYNC
     <broadcaster id="sync-setup-state"/>
     <broadcaster id="sync-syncnow-state"/>
 #endif
     <broadcaster id="workOfflineMenuitemState"/>
+    <broadcaster id="socialSidebarBroadcaster" hidden="true"/>
   </broadcasterset>
 
   <keyset id="mainKeyset">
     <key id="key_newNavigator"
          key="&newNavigatorCmd.key;"
          command="cmd_newNavigator"
          modifiers="accel"/>
     <key id="key_newNavigatorTab" key="&tabCmd.commandkey;" modifiers="accel" command="cmd_newNavigatorTab"/>
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -4,56 +4,64 @@
 
 let SocialUI = {
   // Called on delayed startup to initialize UI
   init: function SocialUI_init() {
     Services.obs.addObserver(this, "social:pref-changed", false);
     Services.obs.addObserver(this, "social:ambient-notification-changed", false);
     Services.obs.addObserver(this, "social:profile-changed", false);
 
+    Services.prefs.addObserver("social.sidebar.open", this, false);
+
     Social.init(this._providerReady.bind(this));
   },
 
   // Called on window unload
   uninit: function SocialUI_uninit() {
     Services.obs.removeObserver(this, "social:pref-changed");
     Services.obs.removeObserver(this, "social:ambient-notification-changed");
     Services.obs.removeObserver(this, "social:profile-changed");
+
+    Services.prefs.removeObserver("social.sidebar.open", this);
   },
 
   showProfile: function SocialUI_showProfile() {
     if (Social.provider)
       openUILink(Social.provider.profile.profileURL);
   },
 
   observe: function SocialUI_observe(subject, topic, data) {
     switch (topic) {
       case "social:pref-changed":
         SocialShareButton.updateButtonHiddenState();
         SocialToolbar.updateButtonHiddenState();
+        SocialSidebar.updateSidebar();
         break;
       case "social:ambient-notification-changed":
         SocialToolbar.updateButton();
         break;
       case "social:profile-changed":
         SocialToolbar.updateProfile();
         break;
+      case "nsPref:changed":
+        SocialSidebar.updateSidebar();
     }
   },
 
   // Called once Social.jsm's provider has been set
   _providerReady: function SocialUI_providerReady() {
     SocialToolbar.init();
     SocialShareButton.init();
+    SocialSidebar.init();
   }
 }
 
 let SocialShareButton = {
+  // Called once, after window load, when the Social.provider object is initialized
   init: function SSB_init() {
-    this.sharePopup.hidden = false;
     this.updateButtonHiddenState();
 
     let profileRow = document.getElementById("editSharePopupHeader");
     let profile = Social.provider.profile;
     if (profile && profile.portrait && profile.displayName) {
       profileRow.hidden = false;
       let portrait = document.getElementById("socialUserPortrait");
       portrait.style.listStyleImage = profile.portrait;
@@ -93,16 +101,18 @@ let SocialShareButton = {
 
   panelShown: function SSB_panelShown(aEvent) {
     let sharePopupOkButton = document.getElementById("editSharePopupOkButton");
     if (sharePopupOkButton)
       sharePopupOkButton.focus();
   },
 
   sharePage: function SSB_sharePage() {
+    this.sharePopup.hidden = false;
+
     let uri = gBrowser.currentURI;
     if (!Social.isPageShared(uri)) {
       Social.sharePage(uri);
       this.updateShareState();
     } else {
       this.sharePopup.openPopup(this.shareButton, "bottomcenter topright");
     }
   },
@@ -243,14 +253,65 @@ var SocialToolbar = {
 
     panel.addEventListener("popuphiding", function onpopuphiding() {
       panel.removeEventListener("popuphiding", onpopuphiding);
       // unload the panel
       document.getElementById("social-toolbar-button").removeAttribute("open");
       notifBrowser.setAttribute("src", "about:blank");
     });
 
-    notifBrowser.service = Social.provider;
+    notifBrowser.setAttribute("origin", Social.provider.origin);
     notifBrowser.setAttribute("src", iconImage.getAttribute("contentPanel"));
     document.getElementById("social-toolbar-button").setAttribute("open", "true");
     panel.openPopup(iconImage, "bottomcenter topleft", 0, 0, false, false);
   }
 }
+
+var SocialSidebar = {
+  // Called once, after window load, when the Social.provider object is initialized
+  init: function SocialSidebar_init() {
+    this.updateSidebar();
+  },
+
+  // Whether the sidebar can be shown for this window.
+  get canShow() {
+    return Social.uiVisible && Social.provider.sidebarURL && !this.chromeless;
+  },
+
+  // Whether this is a "chromeless window" (e.g. popup window). We don't show
+  // the sidebar in these windows.
+  get chromeless() {
+    let docElem = document.documentElement;
+    return docElem.getAttribute('disablechrome') ||
+           docElem.getAttribute('chromehidden').indexOf("extrachrome") >= 0;
+  },
+
+  // Whether the user has toggled the sidebar on (for windows where it can appear)
+  get enabled() {
+    return Services.prefs.getBoolPref("social.sidebar.open");
+  },
+
+  updateSidebar: function SocialSidebar_updateSidebar() {
+    // Hide the toggle menu item if the sidebar cannot appear
+    let command = document.getElementById("Social:ToggleSidebar");
+    command.hidden = !this.canShow;
+
+    // Hide the sidebar if it cannot appear, or has been toggled off.
+    // Also set the command "checked" state accordingly.
+    let hideSidebar = !this.canShow || !this.enabled;
+    let broadcaster = document.getElementById("socialSidebarBroadcaster");
+    broadcaster.hidden = hideSidebar;
+    command.setAttribute("checked", !hideSidebar);
+
+    // If the sidebar is hidden, unload its document
+    // XXX this results in a poor UX, we should revisit
+    let sbrowser = document.getElementById("social-sidebar-browser");
+    if (broadcaster.hidden) {
+      sbrowser.removeAttribute("origin");
+      sbrowser.setAttribute("src", "about:blank");
+      return;
+    }
+
+    // Load the sidebar document
+    sbrowser.setAttribute("origin", Social.provider.origin);
+    sbrowser.setAttribute("src", Social.provider.sidebarURL);
+  }
+}
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -633,16 +633,22 @@
                 <image id="social-statusarea-user-portrait"/>
                 <vbox>
                   <label id="social-statusarea-notloggedin"
                          value="&social.notLoggedIn.label;"/>
                   <button id="social-statusarea-username"
                           oncommand="SocialUI.showProfile(); document.getElementById('social-statusarea-popup').hidePopup();"/>
                 </vbox>
               </hbox>
+              <menuitem id="social-toggle-sidebar-menuitem"
+                        type="checkbox"
+                        autocheck="false"
+                        command="Social:ToggleSidebar"
+                        label="&social.toggleSidebar.label;"
+                        accesskey="&social.toggleSidebar.accesskey;"/>
             </menupopup>
           </button>
           <hbox id="social-status-iconbox" flex="1">
             <box class="social-notification-icon-container" collapsed="true"
                      onclick="SocialToolbar.showAmbientPopup(this);">
               <image class="social-notification-icon-image"/>
               <box class="social-notification-icon-counter" collapsed="true"/>
             </box>
@@ -1019,16 +1025,27 @@
       <tabbrowser id="content" disablehistory="true"
                   flex="1" contenttooltip="aHTMLTooltip"
                   tabcontainer="tabbrowser-tabs"
                   contentcontextmenu="contentAreaContextMenu"
                   autocompletepopup="PopupAutoComplete"
                   onclick="contentAreaClick(event, false);"/>
       <statuspanel id="statusbar-display" inactive="true"/>
     </vbox>
+    <splitter id="social-sidebar-splitter"
+              class="chromeclass-extrachrome"
+              observes="socialSidebarBroadcaster"/>
+    <vbox id="social-sidebar-box"
+          class="chromeclass-extrachrome"
+          observes="socialSidebarBroadcaster">
+      <browser id="social-sidebar-browser"
+               type="content"
+               flex="1"
+               style="min-width: 14em; width: 18em; max-width: 36em;"/>
+    </vbox>
     <splitter id="devtools-side-splitter" hidden="true"/>
     <vbox id="devtools-sidebar-box" hidden="true"
           style="min-width: 18em; width: 22em; max-width: 42em;" persist="width">
       <toolbar id="devtools-sidebar-toolbar"
                class="devtools-toolbar"
                nowindowdrag="true"/>
       <deck id="devtools-sidebar-deck" flex="1"/>
     </vbox>
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -252,16 +252,17 @@ endif
                  browser_minimize.js \
                  browser_aboutSyncProgress.js \
                  browser_middleMouse_inherit.js \
                  redirect_bug623155.sjs \
                  browser_tabDrop.js \
                  browser_lastAccessedTab.js \
                  browser_bug734076.js \
                  browser_social_toolbar.js \
+                 browser_social_sidebar.js \
                  $(NULL)
 
 ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT))
 _BROWSER_FILES += \
 		browser_bug462289.js \
 		$(NULL)
 else
 _BROWSER_FILES += \
--- a/browser/base/content/test/browser_shareButton.js
+++ b/browser/base/content/test/browser_shareButton.js
@@ -46,17 +46,16 @@ function testInitial() {
   ok(Social.provider, "Social provider is active");
   ok(Social.provider.enabled, "Social provider is enabled");
   ok(Social.provider.port, "Social provider has a port to its FrameWorker");
 
   shareButton = SocialShareButton.shareButton;
   sharePopup = SocialShareButton.sharePopup;
   ok(shareButton, "share button exists");
   ok(sharePopup, "share popup exists");
-  ok(!sharePopup.hidden, "share popup is not hidden");
 
   okButton = document.getElementById("editSharePopupOkButton");
   undoButton = document.getElementById("editSharePopupUndoButton");
 
   is(shareButton.hidden, false, "share button should be visible");
 
   // Test clicking the share button
   shareButton.addEventListener("click", function listener() {
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/browser_social_sidebar.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
+
+function test() {
+  // XXX Bug 775779
+  if (Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2).isDebugBuild) {
+    ok(true, "can't run social sidebar test in debug builds because they falsely report leaks");
+    return;
+  }
+
+  waitForExplicitFinish();
+
+  let manifest = { // normal provider
+    name: "provider 1",
+    origin: "https://example1.com",
+    sidebarURL: "https://example1.com/sidebar.html",
+    workerURL: "https://example1.com/worker.js",
+    iconURL: "chrome://branding/content/icon48.png"
+  };
+  runSocialTestWithProvider(manifest, doTest);
+}
+
+function doTest() {
+  ok(SocialSidebar.canShow, "social sidebar should be able to be shown");
+  ok(SocialSidebar.enabled, "social sidebar should be on by default");
+
+  let command = document.getElementById("Social:ToggleSidebar");
+  let sidebar = document.getElementById("social-sidebar-box");
+
+  // Check the the sidebar is initially visible, and loaded
+  ok(!command.hidden, "sidebar toggle command should be visible");
+  is(command.getAttribute("checked"), "true", "sidebar toggle command should be checked");
+  ok(!sidebar.hidden, "sidebar itself should be visible");
+  ok(Services.prefs.getBoolPref("social.sidebar.open"), "sidebar open pref should be true");
+  is(sidebar.firstChild.getAttribute('src'), "https://example1.com/sidebar.html", "sidebar url should be set");
+
+  // Now toggle it!
+  info("Toggling sidebar");
+  Social.toggleSidebar();
+  is(command.getAttribute("checked"), "false", "sidebar toggle command should not be checked");
+  ok(sidebar.hidden, "sidebar itself should not be visible");
+  ok(!Services.prefs.getBoolPref("social.sidebar.open"), "sidebar open pref should be false");
+  is(sidebar.firstChild.getAttribute('src'), "about:blank", "sidebar url should not be set");
+
+  // Remove the test provider
+  SocialService.removeProvider(Social.provider.origin, finish);
+}
+
+// XXX test sidebar in popup
--- a/browser/base/content/test/browser_social_toolbar.js
+++ b/browser/base/content/test/browser_social_toolbar.js
@@ -1,94 +1,69 @@
 /* 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/. */
 
 let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
-let gProvider;
 
 function test() {
   waitForExplicitFinish();
 
-  Services.prefs.setBoolPref("social.enabled", true);
-  registerCleanupFunction(function () {
-    Services.prefs.clearUserPref("social.enabled");
-  });
-
-  let oldProvider;
-  function saveOldProviderAndStartTestWith(provider) {
-    oldProvider = Social.provider;
-    registerCleanupFunction(function () {
-      Social.provider = oldProvider;
-    });
-    Social.provider = gProvider = provider;
-    runTests(tests, undefined, undefined, function () {
-      SocialService.removeProvider(provider.origin, finish);
-    });
-  }
-
   let manifest = { // normal provider
     name: "provider 1",
     origin: "https://example1.com",
     workerURL: "https://example1.com/worker.js",
     iconURL: "chrome://branding/content/icon48.png"
   };
-  SocialService.addProvider(manifest, function(provider) {
-    // If the UI is already active, run the test immediately, otherwise wait
-    // for initialization.
-    if (Social.provider) {
-      saveOldProviderAndStartTestWith(provider);
-    } else {
-      Services.obs.addObserver(function obs() {
-        Services.obs.removeObserver(obs, "test-social-ui-ready");
-        saveOldProviderAndStartTestWith(provider);
-      }, "test-social-ui-ready", false);
-    }
+  runSocialTestWithProvider(manifest, function () {
+    runTests(tests, undefined, undefined, function () {
+      SocialService.removeProvider(Social.provider.origin, finish);
+    });
   });
 }
 
 var tests = {
   testProfileSet: function(next) {
     let profile = {
       portrait: "chrome://branding/content/icon48.png",
       userName: "trickster",
       displayName: "Kuma Lisa",
       profileURL: "http://en.wikipedia.org/wiki/Kuma_Lisa"
     }
-    gProvider.updateUserProfile(profile);
+    Social.provider.updateUserProfile(profile);
     // check dom values
     let portrait = document.getElementById("social-statusarea-user-portrait").getAttribute("src");
     is(portrait, profile.portrait, "portrait is set");
     let userButton = document.getElementById("social-statusarea-username");
     ok(!userButton.hidden, "username is visible");
     is(userButton.label, profile.userName, "username is set");
     next();
   },
   testAmbientNotifications: function(next) {
     let ambience = {
       name: "testIcon",
       iconURL: "chrome://branding/content/icon48.png",
       contentPanel: "about:blank",
       counter: 42
     };
-    gProvider.setAmbientNotification(ambience);
+    Social.provider.setAmbientNotification(ambience);
 
     let statusIcons = document.getElementById("social-status-iconbox");
     ok(!statusIcons.firstChild.collapsed, "status icon is visible");
     ok(!statusIcons.firstChild.lastChild.collapsed, "status value is visible");
     is(statusIcons.firstChild.lastChild.textContent, "42", "status value is correct");
 
     ambience.counter = 0;
-    gProvider.setAmbientNotification(ambience);
+    Social.provider.setAmbientNotification(ambience);
     ok(statusIcons.firstChild.lastChild.collapsed, "status value is not visible");
     is(statusIcons.firstChild.lastChild.textContent, "", "status value is correct");
     next();
   },
   testProfileUnset: function(next) {
-    gProvider.updateUserProfile({});
+    Social.provider.updateUserProfile({});
     // check dom values
     let portrait = document.getElementById("social-statusarea-user-portrait").getAttribute("src");
     is(portrait, "chrome://browser/skin/social/social.png", "portrait is generic");
     let userButton = document.getElementById("social-statusarea-username");
     ok(userButton.hidden, "username is not visible");
     let ambience = document.getElementById("social-status-iconbox").firstChild;
     while (ambience) {
       ok(ambience.collapsed, "ambient icon is collapsed");
--- a/browser/base/content/test/head.js
+++ b/browser/base/content/test/head.js
@@ -82,8 +82,39 @@ function waitForCondition(condition, nex
     }
     if (condition()) {
       moveOn();
     }
     tries++;
   }, 100);
   var moveOn = function() { clearInterval(interval); nextTest(); };
 }
+
+function runSocialTestWithProvider(manifest, callback) {
+  let oldProvider;
+  function saveOldProviderAndStartTestWith(provider) {
+    oldProvider = Social.provider;
+    Social.provider = provider;
+
+    // Now that we've set the UI's provider, enable the social functionality
+    Services.prefs.setBoolPref("social.enabled", true);
+
+    registerCleanupFunction(function () {
+      Social.provider = oldProvider;
+      Services.prefs.clearUserPref("social.enabled");
+    });
+
+    callback();
+  }
+
+  SocialService.addProvider(manifest, function(provider) {
+    // If the UI is already active, run the test immediately, otherwise wait
+    // for initialization.
+    if (Social.provider) {
+      saveOldProviderAndStartTestWith(provider);
+    } else {
+      Services.obs.addObserver(function obs() {
+        Services.obs.removeObserver(obs, "test-social-ui-ready");
+        saveOldProviderAndStartTestWith(provider);
+      }, "test-social-ui-ready", false);
+    }
+  });
+}
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -657,8 +657,10 @@ toolbar button -->
 <!ENTITY social.notLoggedIn.label   "Not logged in">
 
 <!ENTITY social.sharePopup.undo.label     "Unshare">
 <!ENTITY social.sharePopup.undo.accesskey "U">
 <!ENTITY social.sharePopup.ok.label       "OK">
 <!ENTITY social.sharePopup.ok.accesskey   "O">
 <!ENTITY social.sharePopup.shared.label   "You shared this page.">
 <!ENTITY social.sharePopup.portrait.arialabel "User profile picture">
+<!ENTITY social.toggleSidebar.label "Show sidebar">
+<!ENTITY social.toggleSidebar.accesskey "s">
--- a/browser/modules/Social.jsm
+++ b/browser/modules/Social.jsm
@@ -29,22 +29,23 @@ let Social = {
     SocialService.getProviderList(function (providers) {
       if (providers.length)
         this.provider = providers[0];
       callback();
       Services.obs.notifyObservers(null, "test-social-ui-ready", "");
     }.bind(this));
   },
 
-  get enabled() {
-    return SocialService.enabled;
+  get uiVisible() {
+    return this.provider && this.provider.enabled && this.provider.port;
   },
 
-  get uiVisible() {
-    return this.provider && this.provider.enabled && this.provider.port;
+  toggleSidebar: function SocialSidebar_toggle() {
+    let prefValue = Services.prefs.getBoolPref("social.sidebar.open");
+    Services.prefs.setBoolPref("social.sidebar.open", !prefValue);
   },
 
   sendWorkerMessage: function Social_sendWorkerMessage(message) {
     // Responses aren't handled yet because there is no actions to perform
     // based on the response from the provider at this point.
     if (this.provider && this.provider.port)
       this.provider.port.postMessage(message);
   },
--- a/toolkit/components/social/SocialService.jsm
+++ b/toolkit/components/social/SocialService.jsm
@@ -8,32 +8,144 @@ const { classes: Cc, interfaces: Ci, uti
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "getFrameWorkerHandle", "resource://gre/modules/FrameWorker.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WorkerAPI", "resource://gre/modules/WorkerAPI.jsm");
 
 /**
+ * The SocialService is the public API to social providers - it tracks which
+ * providers are installed and enabled, and is the entry-point for access to
+ * the provider itself.
+ */
+
+// Internal helper methods and state
+let SocialServiceInternal = {
+  enabled: Services.prefs.getBoolPref("social.enabled"),
+  get providerArray() {
+    return [p for ([, p] of Iterator(this.providers))];
+  }
+};
+
+XPCOMUtils.defineLazyGetter(SocialServiceInternal, "providers", function () {
+  // Initialize the service (add a pref observer)
+  function prefObserver(subject, topic, data) {
+    SocialService._setEnabled(Services.prefs.getBoolPref(data));
+  }
+  Services.prefs.addObserver("social.enabled", prefObserver, false);
+  Services.obs.addObserver(function xpcomShutdown() {
+    Services.obs.removeObserver(xpcomShutdown, "xpcom-shutdown");
+    Services.prefs.removeObserver("social.enabled", prefObserver);
+  }, "xpcom-shutdown", false);
+
+  // Now retrieve the providers
+  let providers = {};
+  let MANIFEST_PREFS = Services.prefs.getBranch("social.manifest.");
+  let prefs = MANIFEST_PREFS.getChildList("", {});
+  prefs.forEach(function (pref) {
+    try {
+      var manifest = JSON.parse(MANIFEST_PREFS.getCharPref(pref));
+      if (manifest && typeof(manifest) == "object") {
+        let provider = new SocialProvider(manifest, SocialServiceInternal.enabled);
+        providers[provider.origin] = provider;
+      }
+    } catch (err) {
+      Cu.reportError("SocialService: failed to load provider: " + pref +
+                     ", exception: " + err);
+    }
+  });
+
+  return providers;
+});
+
+function schedule(callback) {
+  Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
+}
+
+// Public API
+const SocialService = {
+  get enabled() {
+    return SocialServiceInternal.enabled;
+  },
+  set enabled(val) {
+    let enable = !!val;
+    if (enable == SocialServiceInternal.enabled)
+      return;
+
+    Services.prefs.setBoolPref("social.enabled", enable);
+    this._setEnabled(enable);
+  },
+  _setEnabled: function _setEnabled(enable) {
+    SocialServiceInternal.providerArray.forEach(function (p) p.enabled = enable);
+    SocialServiceInternal.enabled = enable;
+    Services.obs.notifyObservers(null, "social:pref-changed", enable ? "enabled" : "disabled");
+  },
+
+  // 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");
+
+    let provider = new SocialProvider(manifest, SocialServiceInternal.enabled);
+    SocialServiceInternal.providers[provider.origin] = provider;
+
+    schedule(function () {
+      onDone(provider);
+    });
+  },
+
+  // Removes a provider with the given origin, and notifies when the removal is
+  // complete.
+  removeProvider: function removeProvider(origin, onDone) {
+    if (!(origin in SocialServiceInternal.providers))
+      throw new Error("SocialService.removeProvider: no provider with this origin exists!");
+
+    let provider = SocialServiceInternal.providers[origin];
+    provider.enabled = false;
+
+    delete SocialServiceInternal.providers[origin];
+
+    if (onDone)
+      schedule(onDone);
+  },
+
+  // Returns a single provider object with the specified origin.
+  getProvider: function getProvider(origin, onDone) {
+    schedule((function () {
+      onDone(SocialServiceInternal.providers[origin] || null);
+    }).bind(this));
+  },
+
+  // Returns an array of installed provider origins.
+  getProviderList: function getProviderList(onDone) {
+    schedule(function () {
+      onDone(SocialServiceInternal.providerArray);
+    });
+  }
+};
+
+/**
  * The SocialProvider object represents a social provider, and allows
  * access to its FrameWorker (if it has one).
  *
  * @constructor
  * @param {jsobj} object representing the manifest file describing this provider
  * @param {bool} whether the provider should be initially enabled (defaults to true)
  */
 function SocialProvider(input, enabled) {
   if (!input.name)
     throw new Error("SocialProvider must be passed a name");
   if (!input.origin)
     throw new Error("SocialProvider must be passed an origin");
 
   this.name = input.name;
   this.iconURL = input.iconURL;
   this.workerURL = input.workerURL;
+  this.sidebarURL = input.sidebarURL;
   this.origin = input.origin;
   this.ambientNotificationIcons = {};
 
   // If enabled is |undefined|, default to true.
   this._enabled = !(enabled == false);
   if (this._enabled)
     this._activate();
 }
@@ -148,120 +260,8 @@ SocialProvider.prototype = {
     try {
       return getFrameWorkerHandle(this.workerURL, window).port;
     } catch (ex) {
       Cu.reportError("SocialProvider: retrieving worker port failed:" + ex);
       return null;
     }
   }
 }
-
-/**
- * The SocialService is the public API to social providers - it tracks which
- * providers are installed and enabled, and is the entry-point for access to
- * the provider itself.
- */
-
-
-// Internal helper methods and state
-let SocialServiceInternal = {
-  enabled: Services.prefs.getBoolPref("social.enabled"),
-  get providerArray() {
-    return [p for ([, p] of Iterator(this.providers))];
-  }
-};
-
-XPCOMUtils.defineLazyGetter(SocialServiceInternal, "providers", function () {
-  // Initialize the service (add a pref observer)
-  function prefObserver(subject, topic, data) {
-    SocialService._setEnabled(Services.prefs.getBoolPref(data));
-  }
-  Services.prefs.addObserver("social.enabled", prefObserver, false);
-  Services.obs.addObserver(function xpcomShutdown() {
-    Services.obs.removeObserver(xpcomShutdown, "xpcom-shutdown");
-    Services.prefs.removeObserver("social.enabled", prefObserver);
-  }, "xpcom-shutdown", false);
-
-  // Now retrieve the providers
-  let providers = {};
-  let MANIFEST_PREFS = Services.prefs.getBranch("social.manifest.");
-  let prefs = MANIFEST_PREFS.getChildList("", {});
-  prefs.forEach(function (pref) {
-    try {
-      var manifest = JSON.parse(MANIFEST_PREFS.getCharPref(pref));
-      if (manifest && typeof(manifest) == "object") {
-        let provider = new SocialProvider(manifest, SocialServiceInternal.enabled);
-        providers[provider.origin] = provider;
-      }
-    } catch (err) {
-      Cu.reportError("SocialService: failed to load provider: " + pref +
-                     ", exception: " + err);
-    }
-  });
-
-  return providers;
-});
-
-function schedule(callback) {
-  Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
-}
-
-// Public API
-const SocialService = {
-  get enabled() {
-    return SocialServiceInternal.enabled;
-  },
-  set enabled(val) {
-    let enable = !!val;
-    if (enable == SocialServiceInternal.enabled)
-      return;
-
-    Services.prefs.setBoolPref("social.enabled", enable);
-    this._setEnabled(enable);
-  },
-  _setEnabled: function _setEnabled(enable) {
-    SocialServiceInternal.providerArray.forEach(function (p) p.enabled = enable);
-    SocialServiceInternal.enabled = enable;
-    Services.obs.notifyObservers(null, "social:pref-changed", enable ? "enabled" : "disabled");
-  },
-
-  // 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");
-
-    let provider = new SocialProvider(manifest, SocialServiceInternal.enabled);
-    SocialServiceInternal.providers[provider.origin] = provider;
-
-    schedule(function () {
-      onDone(provider);
-    });
-  },
-
-  // Removes a provider with the given origin, and notifies when the removal is
-  // complete.
-  removeProvider: function removeProvider(origin, onDone) {
-    if (!(origin in SocialServiceInternal.providers))
-      throw new Error("SocialService.removeProvider: no provider with this origin exists!");
-
-    let provider = SocialServiceInternal.providers[origin];
-    provider.enabled = false;
-
-    delete SocialServiceInternal.providers[origin];
-
-    if (onDone)
-      schedule(onDone);
-  },
-
-  // Returns a single provider object with the specified origin.
-  getProvider: function getProvider(origin, onDone) {
-    schedule((function () {
-      onDone(SocialServiceInternal.providers[origin] || null);
-    }).bind(this));
-  },
-
-  // Returns an array of installed provider origins.
-  getProviderList: function getProviderList(onDone) {
-    schedule(function () {
-      onDone(SocialServiceInternal.providerArray);
-    });
-  }
-};