merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 02 Oct 2015 11:44:11 +0200
changeset 265708 af01a617cb76f0ced4e33f82a9567bbba44b9f24
parent 265670 9024f6d05c3b22985b25ef486409a58f22136b38 (current diff)
parent 265707 18960b8cb8e2a4853d0ad7f619c2420d3f7bdf57 (diff)
child 265709 5f16c6c2b969f70e8da10ee34853246d593af412
push id66003
push usercbook@mozilla.com
push dateFri, 02 Oct 2015 11:37:40 +0000
treeherdermozilla-inbound@3fd732d24f46 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone44.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
browser/themes/shared/webrtc/indicator.css
--- a/addon-sdk/source/lib/sdk/panel.js
+++ b/addon-sdk/source/lib/sdk/panel.js
@@ -21,16 +21,17 @@ const { WorkerHost } = require("./conten
 const { Worker } = require("./deprecated/sync-worker");
 const { Disposable } = require("./core/disposable");
 const { WeakReference } = require('./core/reference');
 const { contract: loaderContract } = require("./content/loader");
 const { contract } = require("./util/contract");
 const { on, off, emit, setListeners } = require("./event/core");
 const { EventTarget } = require("./event/target");
 const domPanel = require("./panel/utils");
+const { getDocShell } = require('./frame/utils');
 const { events } = require("./panel/events");
 const systemEvents = require("./system/events");
 const { filter, pipe, stripListeners } = require("./event/utils");
 const { getNodeView, getActiveView } = require("./view/core");
 const { isNil, isObject, isNumber } = require("./lang/type");
 const { getAttachEventType } = require("./content/utils");
 const { number, boolean, object } = require('./deprecated/api-utils');
 const { Style } = require("./stylesheet/style");
@@ -68,19 +69,36 @@ var panelContract = contract(merge({
   // contentStyle* / contentScript* are sharing the same validation constraints,
   // so they can be mostly reused, except for the messages.
   contentStyle: merge(Object.create(loaderContract.rules.contentScript), {
     msg: 'The `contentStyle` option must be a string or an array of strings.'
   }),
   contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), {
     msg: 'The `contentStyleFile` option must be a local URL or an array of URLs'
   }),
-  contextMenu: boolean
+  contextMenu: boolean,
+  allow: {
+    is: ['object', 'undefined', 'null'],
+    map: function (allow) { return { script: !allow || allow.script !== false }}
+  },
 }, displayContract.rules, loaderContract.rules));
 
+function Allow(panel) {
+  return {
+    get script() { return getDocShell(viewFor(panel).backgroundFrame).allowJavascript; },
+    set script(value) { return setScriptState(panel, value); },
+  };
+}
+
+function setScriptState(panel, value) {
+  let view = viewFor(panel);
+  getDocShell(view.backgroundFrame).allowJavascript = value;
+  getDocShell(view.viewFrame).allowJavascript = value;
+  view.setAttribute("sdkscriptenabled", "" + value);
+}
 
 function isDisposed(panel) !views.has(panel);
 
 var panels = new WeakMap();
 var models = new WeakMap();
 var views = new WeakMap();
 var workers = new WeakMap();
 var styles = new WeakMap();
@@ -142,17 +160,18 @@ const Panel = Class({
     if (model.contentStyle || model.contentStyleFile) {
       styles.set(this, Style({
         uri: model.contentStyleFile,
         source: model.contentStyle
       }));
     }
 
     // Setup view
-    let view = domPanel.make();
+    let viewOptions = {allowJavascript: !model.allow || (model.allow.script !== false)};
+    let view = domPanel.make(null, viewOptions);
     panels.set(view, this);
     views.set(this, view);
 
     // Load panel content.
     domPanel.setURL(view, model.contentURL);
 
     // Allow context menu
     domPanel.allowContextMenu(view, model.contextMenu);
@@ -207,16 +226,22 @@ const Panel = Class({
     let model = modelFor(this);
     model.contentURL = panelContract({ contentURL: value }).contentURL;
     domPanel.setURL(viewFor(this), model.contentURL);
     // Detach worker so that messages send will be queued until it's
     // reatached once panel content is ready.
     workerFor(this).detach();
   },
 
+  get allow() { return Allow(this); },
+  set allow(value) {
+    let allowJavascript = panelContract({ allow: value }).allow.script;
+    return setScriptState(this, value);
+  },
+
   /* Public API: Panel.isShowing */
   get isShowing() !isDisposed(this) && domPanel.isOpen(viewFor(this)),
 
   /* Public API: Panel.show */
   show: function show(options={}, anchor) {
     if (options instanceof Ci.nsIDOMElement) {
       [anchor, options] = [options, null];
     }
--- a/addon-sdk/source/lib/sdk/panel/utils.js
+++ b/addon-sdk/source/lib/sdk/panel/utils.js
@@ -9,17 +9,17 @@ module.metadata = {
 };
 
 const { Cc, Ci } = require("chrome");
 const { setTimeout } = require("../timers");
 const { platform } = require("../system");
 const { getMostRecentBrowserWindow, getOwnerBrowserWindow,
         getHiddenWindow, getScreenPixelsPerCSSPixel } = require("../window/utils");
 
-const { create: createFrame, swapFrameLoaders } = require("../frame/utils");
+const { create: createFrame, swapFrameLoaders, getDocShell } = require("../frame/utils");
 const { window: addonWindow } = require("../addon/window");
 const { isNil } = require("../lang/type");
 const { data } = require('../self');
 
 const events = require("../system/events");
 
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
@@ -242,29 +242,30 @@ function setupPanelFrame(frame) {
   frame.setAttribute("transparent", "transparent");
   frame.setAttribute("autocompleteenabled", true);
   if (platform === "darwin") {
     frame.style.borderRadius = "6px";
     frame.style.padding = "1px";
   }
 }
 
-function make(document) {
+function make(document, options) {
   document = document || getMostRecentBrowserWindow().document;
   let panel = document.createElementNS(XUL_NS, "panel");
   panel.setAttribute("type", "arrow");
+  panel.setAttribute("sdkscriptenabled", "" + options.allowJavascript);
 
   // Note that panel is a parent of `viewFrame` who's `docShell` will be
   // configured at creation time. If `panel` and there for `viewFrame` won't
   // have an owner document attempt to access `docShell` will throw. There
   // for we attach panel to a document.
   attach(panel, document);
 
   let frameOptions =  {
-    allowJavascript: true,
+    allowJavascript: options.allowJavascript,
     allowPlugins: true,
     allowAuth: true,
     allowWindowControl: false,
     // Need to override `nodeName` to use `iframe` as `browsers` save session
     // history and in consequence do not dispatch "inner-window-destroyed"
     // notifications.
     browser: false,
     // Note that use of this URL let's use swap frame loaders earlier
@@ -279,18 +280,26 @@ function make(document) {
   setupPanelFrame(viewFrame);
 
   function onDisplayChange({type, target}) {
     // Events from child element like <select /> may propagate (dropdowns are
     // popups too), in which case frame loader shouldn't be swapped.
     // See Bug 886329
     if (target !== this) return;
 
-    try { swapFrameLoaders(backgroundFrame, viewFrame); }
-    catch(error) { console.exception(error); }
+    try {
+      swapFrameLoaders(backgroundFrame, viewFrame);
+      // We need to re-set this because... swapFrameLoaders. Or something.
+      let shouldEnableScript = panel.getAttribute("sdkscriptenabled") == "true";
+      getDocShell(backgroundFrame).allowJavascript = shouldEnableScript;
+      getDocShell(viewFrame).allowJavascript = shouldEnableScript;
+    }
+    catch(error) {
+      console.exception(error);
+    }
     events.emit(type, { subject: panel });
   }
 
   function onContentReady({target, type}) {
     if (target === getContentDocument(panel)) {
       style(panel);
       events.emit(type, { subject: panel });
     }
@@ -326,16 +335,17 @@ function make(document) {
 
   panel.addEventListener("load", onContentLoad, true);
   backgroundFrame.addEventListener("load", onContentLoad, true);
 
   events.on("document-element-inserted", onContentChange);
 
 
   panel.backgroundFrame = backgroundFrame;
+  panel.viewFrame = viewFrame;
 
   // Store event listener on the panel instance so that it won't be GC-ed
   // while panel is alive.
   panel.onContentChange = onContentChange;
 
   return panel;
 }
 exports.make = make;
@@ -351,18 +361,20 @@ function attach(panel, document) {
 exports.attach = attach;
 
 function detach(panel) {
   if (panel.parentNode) panel.parentNode.removeChild(panel);
 }
 exports.detach = detach;
 
 function dispose(panel) {
-  panel.backgroundFrame.parentNode.removeChild(panel.backgroundFrame);
+  panel.backgroundFrame.remove();
+  panel.viewFrame.remove();
   panel.backgroundFrame = null;
+  panel.viewFrame = null;
   events.off("document-element-inserted", panel.onContentChange);
   panel.onContentChange = null;
   detach(panel);
 }
 exports.dispose = dispose;
 
 function style(panel) {
   /**
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1924,22 +1924,17 @@ pref("reader.errors.includeURLs", true);
 
 pref("browser.pocket.enabled", true);
 pref("browser.pocket.api", "api.getpocket.com");
 pref("browser.pocket.site", "getpocket.com");
 pref("browser.pocket.oAuthConsumerKey", "40249-e88c401e1b1f2242d9e441c4");
 pref("browser.pocket.useLocaleList", true);
 pref("browser.pocket.enabledLocales", "cs de en-GB en-US en-ZA es-ES es-MX fr hu it ja ja-JP-mac ko nl pl pt-BR pt-PT ru zh-CN zh-TW");
 
-// View source tabs are only enabled by default for Dev. Ed and Nightly.
-#ifdef RELEASE_BUILD
-pref("view_source.tab", false);
-#else
 pref("view_source.tab", true);
-#endif
 
 // Enable ServiceWorkers for Push API consumers.
 // Interception is still disabled on beta and release.
 pref("dom.serviceWorkers.enabled", true);
 
 #ifndef RELEASE_BUILD
 pref("dom.serviceWorkers.interception.enabled", true);
 #endif
--- a/browser/base/content/browser-fullScreen.js
+++ b/browser/base/content/browser-fullScreen.js
@@ -388,21 +388,21 @@ var FullScreen = {
       try {
         host = uri.host;
       } catch (e) { }
       let textElem = document.getElementById("fullscreen-domain-text");
       if (!host) {
         textElem.setAttribute("hidden", true);
       } else {
         textElem.removeAttribute("hidden");
-        let hostLabel = document.getElementById("fullscreen-domain");
+        let hostElem = document.getElementById("fullscreen-domain");
         // Document's principal's URI has a host. Display a warning including it.
         let utils = {};
         Cu.import("resource://gre/modules/DownloadUtils.jsm", utils);
-        hostLabel.value = utils.DownloadUtils.getURIHost(uri.spec)[0];
+        hostElem.textContent = utils.DownloadUtils.getURIHost(uri.spec)[0];
       }
       this._element.className = gIdentityHandler.fullscreenWarningClassName;
 
       // User should be allowed to explicitly disable
       // the prompt if they really want.
       if (this._timeoutHide.delay <= 0) {
         return;
       }
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -655,52 +655,52 @@ window[chromehidden~="toolbar"] toolbar:
 
 /*  Full Screen UI */
 
 #fullscr-toggler {
   height: 1px;
   background: black;
 }
 
-#fullscreen-warning {
+html|*#fullscreen-warning {
   position: fixed;
   z-index: 2147483647 !important;
   visibility: visible;
   transition: transform 300ms ease-in;
   /* To center the warning box horizontally,
      we use left: 50% with translateX(-50%). */
   top: 0; left: 50%;
   transform: translate(-50%, -100%);
-  /* We must specify a max-width, otherwise word-wrap:break-word doesn't
-     work in descendant <description> and <label> elements. Bug 630864. */
+  box-sizing: border-box;
+  width: -moz-max-content;
   max-width: 95%;
   pointer-events: none;
 }
-#fullscreen-warning:not([hidden]) {
+html|*#fullscreen-warning:not([hidden]) {
   display: flex;
 }
-#fullscreen-warning[onscreen] {
+html|*#fullscreen-warning[onscreen] {
   transform: translate(-50%, 50px);
 }
-#fullscreen-warning[ontop] {
+html|*#fullscreen-warning[ontop] {
   /* Use -10px to hide the border and border-radius on the top */
   transform: translate(-50%, -10px);
 }
 
-#fullscreen-domain-text,
-#fullscreen-generic-text {
+html|*#fullscreen-domain-text,
+html|*#fullscreen-generic-text {
   word-wrap: break-word;
   /* We must specify a min-width, otherwise word-wrap:break-word doesn't work. Bug 630864. */
   min-width: 1px
 }
-#fullscreen-domain-text:not([hidden]) + #fullscreen-generic-text {
+html|*#fullscreen-domain-text:not([hidden]) + html|*#fullscreen-generic-text {
   display: none;
 }
 
-#fullscreen-exit-button {
+html|*#fullscreen-exit-button {
   pointer-events: auto;
 }
 
 /* ::::: Ctrl-Tab Panel ::::: */
 
 .ctrlTab-preview > html|img,
 .ctrlTab-preview > html|canvas {
   min-width: inherit;
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -79,16 +79,20 @@ function pktUIGetter(prop) {
     },
     configurable: true,
     enumerable: true
   };
 }
 Object.defineProperty(window, "pktUI", pktUIGetter("pktUI"));
 Object.defineProperty(window, "pktUIMessaging", pktUIGetter("pktUIMessaging"));
 
+XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
+  return Services.strings.createBundle('chrome://browser/locale/browser.properties');
+});
+
 const nsIWebNavigation = Ci.nsIWebNavigation;
 
 var gLastBrowserCharset = null;
 var gProxyFavIcon = null;
 var gLastValidURLStr = "";
 var gInPrintPreviewMode = false;
 var gContextMenu = null; // nsContextMenu instance
 var gMultiProcessBrowser =
@@ -4028,16 +4032,51 @@ function openNewUserContextTab(event)
  */
 function updateUserContextUIVisibility()
 {
   let userContextEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled");
   document.getElementById("menu_newUserContext").hidden = !userContextEnabled;
 }
 
 /**
+ * Updates the User Context UI indicators if the browser is in a non-default context
+ */
+function updateUserContextUIIndicator(browser)
+{
+  let hbox = document.getElementById("userContext-icons");
+
+  if (!browser.hasAttribute("usercontextid")) {
+    hbox.removeAttribute("usercontextid");
+    return;
+  }
+
+  let label = document.getElementById("userContext-label");
+  let userContextId = browser.getAttribute("usercontextid");
+  hbox.setAttribute("usercontextid", userContextId);
+  switch (userContextId) {
+    case "1":
+      label.value = gBrowserBundle.GetStringFromName("usercontext.personal.label");
+      break;
+    case "2":
+      label.value = gBrowserBundle.GetStringFromName("usercontext.work.label");
+      break;
+    case "3":
+      label.value = gBrowserBundle.GetStringFromName("usercontext.banking.label");
+      break;
+    case "4":
+      label.value = gBrowserBundle.GetStringFromName("usercontext.shopping.label");
+      break;
+    // Display the context IDs for values outside the pre-defined range.
+    // Used for debugging, no localization necessary.
+    default:
+      label.value = "Context " + userContextId;
+  }
+}
+
+/**
  * Makes the Character Encoding menu enabled or disabled as appropriate.
  * To be called when the View menu or the app menu is opened.
  */
 function updateCharacterEncodingMenuState()
 {
   let charsetMenu = document.getElementById("charsetMenu");
   // gBrowser is null on Mac when the menubar shows in the context of
   // non-browser windows. The above elements may be null depending on
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -763,16 +763,20 @@
                        hidden="true"
                        tooltiptext="&pageReportIcon.tooltip;"
                        onclick="gPopupBlockerObserver.onReportButtonClick(event);"/>
                 <image id="reader-mode-button"
                        class="urlbar-icon"
                        hidden="true"
                        onclick="ReaderParent.buttonClick(event);"/>
               </hbox>
+              <hbox id="userContext-icons">
+                <label id="userContext-label"/>
+                <image id="userContext-indicator"/>
+              </hbox>
               <toolbarbutton id="urlbar-go-button"
                              class="chromeclass-toolbar-additional"
                              onclick="gURLBar.handleCommand(event);"
                              tooltiptext="&goEndCap.tooltip;"/>
               <toolbarbutton id="urlbar-reload-button"
                              class="chromeclass-toolbar-additional"
                              command="Browser:ReloadOrDuplicate"
                              onclick="checkForMiddleClick(this, event);"
@@ -1146,33 +1150,34 @@
                  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>
 
-  <hbox id="fullscreen-warning" hidden="true">
-    <description id="fullscreen-domain-text">
+  <html:div id="fullscreen-warning" hidden="true">
+    <html:div id="fullscreen-domain-text">
       &fullscreenWarning.beforeDomain.label;
-      <label id="fullscreen-domain"/>
+      <html:span id="fullscreen-domain"/>
       &fullscreenWarning.afterDomain.label;
-    </description>
-    <description id="fullscreen-generic-text">
+    </html:div>
+    <html:div id="fullscreen-generic-text">
       &fullscreenWarning.generic.label;
-    </description>
-    <button id="fullscreen-exit-button"
+    </html:div>
+    <html:button id="fullscreen-exit-button"
+                 onclick="FullScreen.exitDomFullScreen();">
 #ifdef XP_MACOSX
-            label="&exitDOMFullscreenMac.button;"
+            &exitDOMFullscreenMac.button;
 #else
-            label="&exitDOMFullscreen.button;"
+            &exitDOMFullscreen.button;
 #endif
-            oncommand="FullScreen.exitDomFullScreen();"/>
-  </hbox>
+    </html:button>
+  </html:div>
 
   <vbox id="browser-bottombox" layer="true">
     <notificationbox id="global-notificationbox"/>
     <toolbar id="developer-toolbar"
              hidden="true">
 #ifdef XP_MACOSX
           <toolbarbutton id="developer-toolbar-closebutton"
                          class="devtools-closebutton"
--- a/browser/base/content/socialchat.xml
+++ b/browser/base/content/socialchat.xml
@@ -48,34 +48,31 @@
           let anonid = (idPrefix || getterPrefix) + "icon";
           this.content.__defineGetter__(getter, () => {
             delete this.content[getter];
             return this.content[getter] = document.getAnonymousElementByAttribute(
               this, "anonid", anonid);
           });
         }
 
-        if (!this.chatbar) {
-          document.getAnonymousElementByAttribute(this, "anonid", "minimize").hidden = true;
-          document.getAnonymousElementByAttribute(this, "anonid", "close").hidden = true;
-        }
         let contentWindow = this.contentWindow;
         // process this._callbacks, then set to null so the chatbox creator
         // knows to make new callbacks immediately.
         if (this._callbacks) {
           for (let callback of this._callbacks) {
             callback(this);
           }
           this._callbacks = null;
         }
         this.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) {
           if (event.target != this.contentDocument)
             return;
           this.removeEventListener("DOMContentLoaded", DOMContentLoaded, true);
           this.isActive = !this.minimized;
+          this._chat.loadButtonSet(this, this.getAttribute("buttonSet"));
           this._deferredChatLoaded.resolve(this);
         }, true);
 
         if (this.src)
           this.setAttribute("src", this.src);
       ]]></constructor>
 
       <field name="_deferredChatLoaded" readonly="true">
@@ -87,16 +84,20 @@
           return this._deferredChatLoaded.promise;
         </getter>
       </property>
 
       <field name="content" readonly="true">
         document.getAnonymousElementByAttribute(this, "anonid", "content");
       </field>
 
+      <field name="_chat" readonly="true">
+        Cu.import("resource:///modules/Chat.jsm", {}).Chat;
+      </field>
+
       <property name="contentWindow">
         <getter>
           return this.content.contentWindow;
         </getter>
       </property>
 
       <property name="contentDocument">
         <getter>
@@ -166,20 +167,19 @@
           aTarget.content.popupnotificationanchor.className = this.content.popupnotificationanchor.className;
           aTarget.content.swapDocShells(this.content);
         ]]></body>
       </method>
 
       <method name="setDecorationAttributes">
         <parameter name="aTarget"/>
         <body><![CDATA[
-          for (let attr of ["dark", "customSize"]) {
-            if (this.hasAttribute(attr))
-              aTarget.setAttribute(attr, this.getAttribute(attr));
-          }
+          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;
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1228,16 +1228,18 @@
                 // some element has been focused and respect that.
                 document.activeElement.blur();
               }
 
               if (!gMultiProcessBrowser)
                 this._adjustFocusAfterTabSwitch(this.mCurrentTab);
             }
 
+            updateUserContextUIIndicator(gBrowser.selectedBrowser);
+
             this.tabContainer._setPositionalAttributes();
 
             if (!gMultiProcessBrowser) {
               let event = new CustomEvent("TabSwitchDone", {
                 bubbles: true,
                 cancelable: true
               });
               this.dispatchEvent(event);
@@ -1794,16 +1796,18 @@
             // if we're adding tabs, we're past interrupt mode, ditch the owner
             if (this.mCurrentTab.owner)
               this.mCurrentTab.owner = null;
 
             var t = document.createElementNS(NS_XUL, "tab");
 
             var uriIsAboutBlank = !aURI || aURI == "about:blank";
 
+            if (aUserContextId)
+              t.setAttribute("usercontextid", aUserContextId);
             t.setAttribute("crop", "end");
             t.setAttribute("onerror", "this.removeAttribute('image');");
             t.className = "tabbrowser-tab";
 
             // The new browser should be remote if this is an e10s window and
             // the uri to load can be loaded remotely.
             let remote = gMultiProcessBrowser &&
                          !aForceNotRemote &&
@@ -2514,16 +2518,20 @@
               aOurTab.setAttribute("muted", "true");
               ourBrowser.mute();
               modifiedAttrs.push("muted");
             }
             if (aOtherTab.hasAttribute("soundplaying")) {
               aOurTab.setAttribute("soundplaying", "true");
               modifiedAttrs.push("soundplaying");
             }
+            if (aOtherTab.hasAttribute("usercontextid")) {
+              aOurTab.setAttribute("usercontextid", aOtherTab.getAttribute("usercontextid"));
+              modifiedAttrs.push("usercontextid");
+            }
 
             // If the other tab is pending (i.e. has not been restored, yet)
             // then do not switch docShells but retrieve the other tab's state
             // and apply it to our tab.
             if (isPending) {
               SessionStore.setTabState(aOurTab, SessionStore.getTabState(aOtherTab));
 
               // Make sure to unregister any open URIs.
--- a/browser/base/content/test/chat/browser_chatwindow.js
+++ b/browser/base/content/test/chat/browser_chatwindow.js
@@ -126,8 +126,73 @@ add_chat_task(function* testChatWindowCh
   // 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");
+});
--- a/browser/base/content/test/chat/head.js
+++ b/browser/base/content/test/chat/head.js
@@ -1,17 +1,18 @@
 /* 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) {
+function promiseOpenChat(url, mode, focus, buttonSet = null) {
   let uri = Services.io.newURI(url, null, null);
   let origin = uri.prePath;
   let title = origin;
   let deferred = Promise.defer();
   // 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) {
     if (chatbox.contentDocument.readyState == "complete") {
@@ -23,16 +24,19 @@ function promiseOpenChat(url, mode, focu
       if (event.target != chatbox.contentDocument || chatbox.contentDocument.location.href == "about:blank") {
         return;
       }
       chatbox.removeEventListener("load", onload, true);
       deferred.resolve(chatbox);
     }, true);
   }
   let chatbox = Chat.open(null, origin, title, url, mode, focus, callback);
+  if (buttonSet) {
+    chatbox.setAttribute("buttonSet", buttonSet);
+  }
   return deferred.promise;
 }
 
 // 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;
--- a/browser/base/content/webrtcIndicator.js
+++ b/browser/base/content/webrtcIndicator.js
@@ -25,16 +25,22 @@ function init(event) {
     popup.addEventListener("command", onPopupMenuCommand);
   }
 
   let fxButton = document.getElementById("firefoxButton");
   fxButton.addEventListener("click", onFirefoxButtonClick);
   fxButton.addEventListener("mousedown", PositionHandler);
 
   updateIndicatorState();
+
+  // Alert accessibility implementations stuff just changed. We only need to do
+  // this initially, because changes after this will automatically fire alert
+  // events if things change materially.
+  let ev = new CustomEvent("AlertActive", {bubbles: true, cancelable: true});
+  document.documentElement.dispatchEvent(ev);
 }
 
 function updateIndicatorState() {
   updateWindowAttr("sharingvideo", webrtcUI.showCameraIndicator);
   updateWindowAttr("sharingaudio", webrtcUI.showMicrophoneIndicator);
   updateWindowAttr("sharingscreen", webrtcUI.showScreenSharingIndicator);
 
   // Camera and microphone button tooltip.
--- a/browser/base/content/webrtcIndicator.xul
+++ b/browser/base/content/webrtcIndicator.xul
@@ -7,17 +7,17 @@
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/webRTC-indicator.css" type="text/css"?>
 
 <!DOCTYPE window>
 
 <window xmlns:html="http://www.w3.org/1999/xhtml"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         id="webrtcIndicator"
-        html:role="alert"
+        role="alert"
         windowtype="Browser:WebRTCGlobalIndicator"
         onload="init(event);"
 #ifdef XP_MACOSX
         inwindowmenu="false"
 #endif
         sizemode="normal"
         hidechrome="true"
         orient="horizontal"
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -67,22 +67,24 @@ loop.conversation = (function(mozL10n) {
         return this._renderFeedbackForm();
       }
 
       switch(this.state.windowType) {
         // CallControllerView is used for both.
         case "incoming":
         case "outgoing": {
           return (React.createElement(CallControllerView, {
+            chatWindowDetached: this.state.chatWindowDetached, 
             dispatcher: this.props.dispatcher, 
             mozLoop: this.props.mozLoop, 
             onCallTerminated: this.handleCallTerminated}));
         }
         case "room": {
           return (React.createElement(DesktopRoomConversationView, {
+            chatWindowDetached: this.state.chatWindowDetached, 
             dispatcher: this.props.dispatcher, 
             mozLoop: this.props.mozLoop, 
             onCallTerminated: this.handleCallTerminated, 
             roomStore: this.props.roomStore}));
         }
         case "failed": {
           return (React.createElement(DirectCallFailureView, {
             contact: {}, 
@@ -133,31 +135,32 @@ loop.conversation = (function(mozL10n) {
       sdk: OT,
       mozLoop: navigator.mozLoop
     });
 
     // expose for functional tests
     loop.conversation._sdkDriver = sdkDriver;
 
     // Create the stores.
-    var conversationAppStore = new loop.store.ConversationAppStore({
-      dispatcher: dispatcher,
-      mozLoop: navigator.mozLoop
-    });
     var conversationStore = new loop.store.ConversationStore(dispatcher, {
       client: client,
       isDesktop: true,
       mozLoop: navigator.mozLoop,
       sdkDriver: sdkDriver
     });
     var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       isDesktop: true,
       mozLoop: navigator.mozLoop,
       sdkDriver: sdkDriver
     });
+    var conversationAppStore = new loop.store.ConversationAppStore({
+      activeRoomStore: activeRoomStore,
+      dispatcher: dispatcher,
+      mozLoop: navigator.mozLoop
+    });
     var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: sdkDriver
     });
 
@@ -171,20 +174,16 @@ loop.conversation = (function(mozL10n) {
     var locationHash = loop.shared.utils.locationData().hash;
     var windowId;
 
     var hash = locationHash.match(/#(.*)/);
     if (hash) {
       windowId = hash[1];
     }
 
-    window.addEventListener("unload", function(event) {
-      dispatcher.dispatch(new sharedActions.WindowUnload());
-    });
-
     React.render(
       React.createElement(AppControllerView, {
         dispatcher: dispatcher, 
         mozLoop: navigator.mozLoop, 
         roomStore: roomStore}), document.querySelector("#main"));
 
     document.documentElement.setAttribute("lang", mozL10n.getLanguage());
     document.documentElement.setAttribute("dir", mozL10n.getDirection());
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -67,22 +67,24 @@ loop.conversation = (function(mozL10n) {
         return this._renderFeedbackForm();
       }
 
       switch(this.state.windowType) {
         // CallControllerView is used for both.
         case "incoming":
         case "outgoing": {
           return (<CallControllerView
+            chatWindowDetached={this.state.chatWindowDetached}
             dispatcher={this.props.dispatcher}
             mozLoop={this.props.mozLoop}
             onCallTerminated={this.handleCallTerminated} />);
         }
         case "room": {
           return (<DesktopRoomConversationView
+            chatWindowDetached={this.state.chatWindowDetached}
             dispatcher={this.props.dispatcher}
             mozLoop={this.props.mozLoop}
             onCallTerminated={this.handleCallTerminated}
             roomStore={this.props.roomStore} />);
         }
         case "failed": {
           return (<DirectCallFailureView
             contact={{}}
@@ -133,31 +135,32 @@ loop.conversation = (function(mozL10n) {
       sdk: OT,
       mozLoop: navigator.mozLoop
     });
 
     // expose for functional tests
     loop.conversation._sdkDriver = sdkDriver;
 
     // Create the stores.
-    var conversationAppStore = new loop.store.ConversationAppStore({
-      dispatcher: dispatcher,
-      mozLoop: navigator.mozLoop
-    });
     var conversationStore = new loop.store.ConversationStore(dispatcher, {
       client: client,
       isDesktop: true,
       mozLoop: navigator.mozLoop,
       sdkDriver: sdkDriver
     });
     var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       isDesktop: true,
       mozLoop: navigator.mozLoop,
       sdkDriver: sdkDriver
     });
+    var conversationAppStore = new loop.store.ConversationAppStore({
+      activeRoomStore: activeRoomStore,
+      dispatcher: dispatcher,
+      mozLoop: navigator.mozLoop
+    });
     var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: sdkDriver
     });
 
@@ -171,20 +174,16 @@ loop.conversation = (function(mozL10n) {
     var locationHash = loop.shared.utils.locationData().hash;
     var windowId;
 
     var hash = locationHash.match(/#(.*)/);
     if (hash) {
       windowId = hash[1];
     }
 
-    window.addEventListener("unload", function(event) {
-      dispatcher.dispatch(new sharedActions.WindowUnload());
-    });
-
     React.render(
       <AppControllerView
         dispatcher={dispatcher}
         mozLoop={navigator.mozLoop}
         roomStore={roomStore} />, document.querySelector("#main"));
 
     document.documentElement.setAttribute("lang", mozL10n.getLanguage());
     document.documentElement.setAttribute("dir", mozL10n.getDirection());
--- a/browser/components/loop/content/js/conversationAppStore.js
+++ b/browser/components/loop/content/js/conversationAppStore.js
@@ -10,39 +10,55 @@ loop.store = loop.store || {};
  * the window data and store the window type.
  */
 loop.store.ConversationAppStore = (function() {
   "use strict";
 
   /**
    * Constructor
    *
-   * @param {Object} options Options for the store. Should contain the dispatcher.
+   * @param {Object} options Options for the store. Should contain the
+   *                         activeRoomStore, dispatcher and mozLoop objects.
    */
   var ConversationAppStore = function(options) {
+    if (!options.activeRoomStore) {
+      throw new Error("Missing option activeRoomStore");
+    }
     if (!options.dispatcher) {
       throw new Error("Missing option dispatcher");
     }
     if (!options.mozLoop) {
       throw new Error("Missing option mozLoop");
     }
 
+    this._activeRoomStore = options.activeRoomStore;
     this._dispatcher = options.dispatcher;
     this._mozLoop = options.mozLoop;
+    this._rootObj = ("rootObject" in options) ? options.rootObject : window;
     this._storeState = this.getInitialStoreState();
 
+    // Start listening for specific events, coming from the window object.
+    this._eventHandlers = {};
+    ["unload", "LoopHangupNow", "socialFrameAttached", "socialFrameDetached"]
+      .forEach(function(eventName) {
+        var handlerName = eventName + "Handler";
+        this._eventHandlers[eventName] = this[handlerName].bind(this);
+        this._rootObj.addEventListener(eventName, this._eventHandlers[eventName]);
+      }.bind(this));
+
     this._dispatcher.register(this, [
       "getWindowData",
       "showFeedbackForm"
     ]);
   };
 
   ConversationAppStore.prototype = _.extend({
     getInitialStoreState: function() {
       return {
+        chatWindowDetached: false,
         // How often to display the form. Convert seconds to ms.
         feedbackPeriod: this._mozLoop.getLoopPref("feedback.periodSec") * 1000,
         // Date when the feedback form was last presented. Convert to ms.
         feedbackTimestamp: this._mozLoop
                                .getLoopPref("feedback.dateLastSeenSec") * 1000,
         showFeedbackForm: false
       };
     },
@@ -57,17 +73,17 @@ loop.store.ConversationAppStore = (funct
     },
 
     /**
      * Updates store states and trigger a "change" event.
      *
      * @param {Object} state The new store state.
      */
     setStoreState: function(state) {
-      this._storeState = state;
+      this._storeState = _.extend({}, this._storeState, state);
       this.trigger("change");
     },
 
     /**
      * Sets store state which will result in the feedback form rendered.
      * Saves a timestamp of when the feedback was last rendered.
      */
     showFeedbackForm: function() {
@@ -94,14 +110,72 @@ loop.store.ConversationAppStore = (funct
         this.setStoreState({windowType: "failed"});
         return;
       }
 
       this.setStoreState({windowType: windowData.type});
 
       this._dispatcher.dispatch(new loop.shared.actions.SetupWindowData(_.extend({
         windowId: actionData.windowId}, windowData)));
+    },
+
+    /**
+     * Event handler; invoked when the 'unload' event is dispatched from the
+     * window object.
+     * It will dispatch a 'WindowUnload' action that other stores may listen to
+     * and will remove all event handlers attached to the window object.
+     */
+    unloadHandler: function() {
+      this._dispatcher.dispatch(new loop.shared.actions.WindowUnload());
+
+      // Unregister event handlers.
+      var eventNames = Object.getOwnPropertyNames(this._eventHandlers);
+      eventNames.forEach(function(eventName) {
+        this._rootObj.removeEventListener(eventName, this._eventHandlers[eventName]);
+      }.bind(this));
+      this._eventHandlers = null;
+    },
+
+    /**
+     * Event handler; invoked when the 'LoopHangupNow' event is dispatched from
+     * the window object.
+     * It'll attempt to gracefully disconnect from an active session, or close
+     * the window when no session is currently active.
+     */
+    LoopHangupNowHandler: function() {
+      switch (this.getStoreState().windowType) {
+        case "incoming":
+        case "outgoing":
+          this._dispatcher.dispatch(new loop.shared.actions.HangupCall());
+          break;
+        case "room":
+          if (this._activeRoomStore.getStoreState().used) {
+            this._dispatcher.dispatch(new loop.shared.actions.LeaveRoom());
+          } else {
+            loop.shared.mixins.WindowCloseMixin.closeWindow();
+          }
+          break;
+        default:
+          loop.shared.mixins.WindowCloseMixin.closeWindow();
+          break;
+      }
+    },
+
+    /**
+     * Event handler; invoked when the 'socialFrameAttached' event is dispatched
+     * from the window object.
+     */
+    socialFrameAttachedHandler: function() {
+      this.setStoreState({ chatWindowDetached: false });
+    },
+
+    /**
+     * Event handler; invoked when the 'socialFrameDetached' event is dispatched
+     * from the window object.
+     */
+    socialFrameDetachedHandler: function() {
+      this.setStoreState({ chatWindowDetached: true });
     }
   }, Backbone.Events);
 
   return ConversationAppStore;
 
 })();
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -574,16 +574,17 @@ loop.conversationViews = (function(mozL1
   var OngoingConversationView = React.createClass({displayName: "OngoingConversationView",
     mixins: [
       sharedMixins.MediaSetupMixin
     ],
 
     propTypes: {
       // local
       audio: React.PropTypes.object,
+      chatWindowDetached: React.PropTypes.bool.isRequired,
       // We pass conversationStore here rather than use the mixin, to allow
       // easy configurability for the ui-showcase.
       conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       // This is used from the props rather than the state to make it easier for
       // the ui-showcase.
@@ -710,24 +711,25 @@ loop.conversationViews = (function(mozL1
             matchMedia: this.state.matchMedia || window.matchMedia.bind(window), 
             remotePosterUrl: this.props.remotePosterUrl, 
             remoteSrcMediaElement: this.state.remoteSrcMediaElement, 
             renderRemoteVideo: this.shouldRenderRemoteVideo(), 
             screenShareMediaElement: this.state.screenShareMediaElement, 
             screenSharePosterUrl: null, 
             showContextRoomName: false, 
             useDesktopPaths: true}, 
-            React.createElement(loop.shared.views.ConversationToolbar, {
+            React.createElement(sharedViews.ConversationToolbar, {
               audio: this.props.audio, 
               dispatcher: this.props.dispatcher, 
               hangup: this.hangup, 
               mozLoop: this.props.mozLoop, 
               publishStream: this.publishStream, 
               settingsMenuItems: settingsMenuItems, 
               show: true, 
+              showHangup: this.props.chatWindowDetached, 
               video: this.props.video})
           )
         )
       );
     }
   });
 
   /**
@@ -738,16 +740,17 @@ loop.conversationViews = (function(mozL1
     mixins: [
       sharedMixins.AudioMixin,
       sharedMixins.DocumentTitleMixin,
       loop.store.StoreMixin("conversationStore"),
       Backbone.Events
     ],
 
     propTypes: {
+      chatWindowDetached: React.PropTypes.bool.isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       mozLoop: React.PropTypes.object.isRequired,
       onCallTerminated: React.PropTypes.func.isRequired
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
@@ -817,16 +820,17 @@ loop.conversationViews = (function(mozL1
           return (React.createElement(DirectCallFailureView, {
             dispatcher: this.props.dispatcher, 
             mozLoop: this.props.mozLoop, 
             outgoing: this.state.outgoing}));
         }
         case CALL_STATES.ONGOING: {
           return (React.createElement(OngoingConversationView, {
             audio: { enabled: !this.state.audioMuted, visible: true}, 
+            chatWindowDetached: this.props.chatWindowDetached, 
             conversationStore: this.getStore(), 
             dispatcher: this.props.dispatcher, 
             mediaConnected: this.state.mediaConnected, 
             mozLoop: this.props.mozLoop, 
             remoteSrcMediaElement: this.state.remoteSrcMediaElement, 
             remoteVideoEnabled: this.state.remoteVideoEnabled, 
             video: { enabled: !this.state.videoMuted, visible: true}})
           );
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -574,16 +574,17 @@ loop.conversationViews = (function(mozL1
   var OngoingConversationView = React.createClass({
     mixins: [
       sharedMixins.MediaSetupMixin
     ],
 
     propTypes: {
       // local
       audio: React.PropTypes.object,
+      chatWindowDetached: React.PropTypes.bool.isRequired,
       // We pass conversationStore here rather than use the mixin, to allow
       // easy configurability for the ui-showcase.
       conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       // This is used from the props rather than the state to make it easier for
       // the ui-showcase.
@@ -710,24 +711,25 @@ loop.conversationViews = (function(mozL1
             matchMedia={this.state.matchMedia || window.matchMedia.bind(window)}
             remotePosterUrl={this.props.remotePosterUrl}
             remoteSrcMediaElement={this.state.remoteSrcMediaElement}
             renderRemoteVideo={this.shouldRenderRemoteVideo()}
             screenShareMediaElement={this.state.screenShareMediaElement}
             screenSharePosterUrl={null}
             showContextRoomName={false}
             useDesktopPaths={true}>
-            <loop.shared.views.ConversationToolbar
+            <sharedViews.ConversationToolbar
               audio={this.props.audio}
               dispatcher={this.props.dispatcher}
               hangup={this.hangup}
               mozLoop={this.props.mozLoop}
               publishStream={this.publishStream}
               settingsMenuItems={settingsMenuItems}
               show={true}
+              showHangup={this.props.chatWindowDetached}
               video={this.props.video} />
           </sharedViews.MediaLayoutView>
         </div>
       );
     }
   });
 
   /**
@@ -738,16 +740,17 @@ loop.conversationViews = (function(mozL1
     mixins: [
       sharedMixins.AudioMixin,
       sharedMixins.DocumentTitleMixin,
       loop.store.StoreMixin("conversationStore"),
       Backbone.Events
     ],
 
     propTypes: {
+      chatWindowDetached: React.PropTypes.bool.isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       mozLoop: React.PropTypes.object.isRequired,
       onCallTerminated: React.PropTypes.func.isRequired
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
@@ -817,16 +820,17 @@ loop.conversationViews = (function(mozL1
           return (<DirectCallFailureView
             dispatcher={this.props.dispatcher}
             mozLoop={this.props.mozLoop}
             outgoing={this.state.outgoing} />);
         }
         case CALL_STATES.ONGOING: {
           return (<OngoingConversationView
             audio={{ enabled: !this.state.audioMuted, visible: true }}
+            chatWindowDetached={this.props.chatWindowDetached}
             conversationStore={this.getStore()}
             dispatcher={this.props.dispatcher}
             mediaConnected={this.state.mediaConnected}
             mozLoop={this.props.mozLoop}
             remoteSrcMediaElement={this.state.remoteSrcMediaElement}
             remoteVideoEnabled={this.state.remoteVideoEnabled}
             video={{ enabled: !this.state.videoMuted, visible: true }} />
           );
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -724,29 +724,29 @@ loop.panel = (function(_, mozL10n) {
       });
 
       return (
         React.createElement("ul", {className: dropdownClasses}, 
           React.createElement("li", {
             className: "dropdown-menu-item", 
             onClick: this.props.handleCopyButtonClick, 
             ref: "copyButton"}, 
-            mozL10n.get("copy_url_button2")
+            mozL10n.get("copy_link_menuitem")
           ), 
           React.createElement("li", {
             className: "dropdown-menu-item", 
             onClick: this.props.handleEmailButtonClick, 
             ref: "emailButton"}, 
-            mozL10n.get("email_link_button")
+            mozL10n.get("email_link_menuitem")
           ), 
           React.createElement("li", {
             className: "dropdown-menu-item", 
             onClick: this.props.handleDeleteButtonClick, 
             ref: "deleteButton"}, 
-            mozL10n.get("rooms_list_delete_tooltip")
+            mozL10n.get("delete_conversation_menuitem")
           )
         )
       );
     }
   });
 
   /**
    * User profile prop can be either an object or null as per mozLoopAPI
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -724,29 +724,29 @@ loop.panel = (function(_, mozL10n) {
       });
 
       return (
         <ul className={dropdownClasses}>
           <li
             className="dropdown-menu-item"
             onClick={this.props.handleCopyButtonClick}
             ref="copyButton">
-            {mozL10n.get("copy_url_button2")}
+            {mozL10n.get("copy_link_menuitem")}
           </li>
           <li
             className="dropdown-menu-item"
             onClick={this.props.handleEmailButtonClick}
             ref="emailButton">
-            {mozL10n.get("email_link_button")}
+            {mozL10n.get("email_link_menuitem")}
           </li>
           <li
             className="dropdown-menu-item"
             onClick={this.props.handleDeleteButtonClick}
             ref="deleteButton">
-            {mozL10n.get("rooms_list_delete_tooltip")}
+            {mozL10n.get("delete_conversation_menuitem")}
           </li>
         </ul>
       );
     }
   });
 
   /**
    * User profile prop can be either an object or null as per mozLoopAPI
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -186,16 +186,20 @@ loop.roomViews = (function(mozL10n) {
       );
     }
   });
 
   /**
    * Desktop room invitation view (overlay).
    */
   var DesktopRoomInvitationView = React.createClass({displayName: "DesktopRoomInvitationView",
+    statics: {
+      TRIGGERED_RESET_DELAY: 2000
+    },
+
     mixins: [sharedMixins.DropdownMenuMixin(".room-invitation-overlay")],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       error: React.PropTypes.object,
       mozLoop: React.PropTypes.object.isRequired,
       onAddContextClick: React.PropTypes.func,
       onEditContextClose: React.PropTypes.func,
@@ -231,87 +235,81 @@ loop.roomViews = (function(mozL10n) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
         roomUrl: this.props.roomData.roomUrl,
         from: "conversation"
       }));
 
       this.setState({copiedUrl: true});
+      setTimeout(this.resetTriggeredButtons, this.constructor.TRIGGERED_RESET_DELAY);
+    },
+
+    /**
+     * Reset state of triggered buttons if necessary
+     */
+    resetTriggeredButtons: function() {
+      if (this.state.copiedUrl) {
+        this.setState({copiedUrl: false});
+      }
     },
 
     handleShareButtonClick: function(event) {
       event.preventDefault();
 
       var providers = this.props.socialShareProviders;
       // If there are no providers available currently, save a click by dispatching
       // the 'AddSocialShareProvider' right away.
       if (!providers || !providers.length) {
         this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());
         return;
       }
 
       this.toggleDropdownMenu();
     },
 
-    handleAddContextClick: function(event) {
-      event.preventDefault();
-
-      if (this.props.onAddContextClick) {
-        this.props.onAddContextClick();
-      }
-    },
-
     handleEditContextClose: function() {
       if (this.props.onEditContextClose) {
         this.props.onEditContextClose();
       }
     },
 
     render: function() {
       if (!this.props.show) {
         return null;
       }
 
-      var canAddContext = this.props.mozLoop.getLoopPref("contextInConversations.enabled") &&
-        // Don't show the link when we're showing the edit form already:
-        !this.props.showEditContext &&
-        // Don't show the link when there's already context data available:
-        !(this.props.roomData.roomContextUrls || this.props.roomData.roomDescription);
-
       var cx = React.addons.classSet;
       return (
         React.createElement("div", {className: "room-invitation-overlay"}, 
           React.createElement("div", {className: "room-invitation-content"}, 
             React.createElement("p", {className: cx({hide: this.props.showEditContext})}, 
-              mozL10n.get("invite_header_text")
-            ), 
-            React.createElement("a", {className: cx({hide: !canAddContext, "room-invitation-addcontext": true}), 
-               onClick: this.handleAddContextClick}, 
-              mozL10n.get("context_add_some_label")
+              mozL10n.get("invite_header_text2")
             )
           ), 
           React.createElement("div", {className: cx({
             "btn-group": true,
             "call-action-group": true,
             hide: this.props.showEditContext
           })}, 
-            React.createElement("button", {className: "btn btn-info btn-email", 
-                    onClick: this.handleEmailButtonClick}, 
-              mozL10n.get("email_link_button")
+            React.createElement("div", {className: cx({
+                "btn-copy": true,
+                "invite-button": true,
+                "triggered": this.state.copiedUrl
+              }), 
+              onClick: this.handleCopyButtonClick}, 
+              React.createElement("img", {src: "loop/shared/img/svg/glyph-link-16x16.svg"}), 
+              React.createElement("p", null, mozL10n.get(this.state.copiedUrl ?
+                "invite_copied_link_button" : "invite_copy_link_button"))
             ), 
-            React.createElement("button", {className: "btn btn-info btn-copy", 
-                    onClick: this.handleCopyButtonClick}, 
-              this.state.copiedUrl ? mozL10n.get("copied_url_button") :
-                                      mozL10n.get("copy_url_button2")
-            ), 
-            React.createElement("button", {className: "btn btn-info btn-share", 
-                    onClick: this.handleShareButtonClick, 
-                    ref: "anchor"}, 
-              mozL10n.get("share_button3")
+            React.createElement("div", {className: "btn-email invite-button", 
+              onClick: this.handleEmailButtonClick, 
+              onMouseOver: this.resetTriggeredButtons}, 
+              React.createElement("img", {src: "loop/shared/img/svg/glyph-email-16x16.svg"}), 
+              React.createElement("p", null, mozL10n.get("invite_email_link_button"))
             )
           ), 
           React.createElement(SocialShareDropdown, {
             dispatcher: this.props.dispatcher, 
             ref: "menu", 
             roomUrl: this.props.roomData.roomUrl, 
             show: this.state.showMenu, 
             socialShareProviders: this.props.socialShareProviders}), 
@@ -545,16 +543,17 @@ loop.roomViews = (function(mozL10n) {
       ActiveRoomStoreMixin,
       sharedMixins.DocumentTitleMixin,
       sharedMixins.MediaSetupMixin,
       sharedMixins.RoomsAudioMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
+      chatWindowDetached: React.PropTypes.bool.isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       mozLoop: React.PropTypes.object.isRequired,
       onCallTerminated: React.PropTypes.func.isRequired,
       remotePosterUrl: React.PropTypes.string,
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
@@ -776,16 +775,17 @@ loop.roomViews = (function(mozL10n) {
                   audio: {enabled: !this.state.audioMuted, visible: true}, 
                   dispatcher: this.props.dispatcher, 
                   hangup: this.leaveRoom, 
                   mozLoop: this.props.mozLoop, 
                   publishStream: this.publishStream, 
                   screenShare: screenShareData, 
                   settingsMenuItems: settingsMenuItems, 
                   show: !shouldRenderEditContextView, 
+                  showHangup: this.props.chatWindowDetached, 
                   video: {enabled: !this.state.videoMuted, visible: true}}), 
                 React.createElement(DesktopRoomInvitationView, {
                   dispatcher: this.props.dispatcher, 
                   error: this.state.error, 
                   mozLoop: this.props.mozLoop, 
                   onAddContextClick: this.handleAddContextClick, 
                   onEditContextClose: this.handleEditContextClose, 
                   roomData: roomData, 
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -186,16 +186,20 @@ loop.roomViews = (function(mozL10n) {
       );
     }
   });
 
   /**
    * Desktop room invitation view (overlay).
    */
   var DesktopRoomInvitationView = React.createClass({
+    statics: {
+      TRIGGERED_RESET_DELAY: 2000
+    },
+
     mixins: [sharedMixins.DropdownMenuMixin(".room-invitation-overlay")],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       error: React.PropTypes.object,
       mozLoop: React.PropTypes.object.isRequired,
       onAddContextClick: React.PropTypes.func,
       onEditContextClose: React.PropTypes.func,
@@ -231,88 +235,82 @@ loop.roomViews = (function(mozL10n) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
         roomUrl: this.props.roomData.roomUrl,
         from: "conversation"
       }));
 
       this.setState({copiedUrl: true});
+      setTimeout(this.resetTriggeredButtons, this.constructor.TRIGGERED_RESET_DELAY);
+    },
+
+    /**
+     * Reset state of triggered buttons if necessary
+     */
+    resetTriggeredButtons: function() {
+      if (this.state.copiedUrl) {
+        this.setState({copiedUrl: false});
+      }
     },
 
     handleShareButtonClick: function(event) {
       event.preventDefault();
 
       var providers = this.props.socialShareProviders;
       // If there are no providers available currently, save a click by dispatching
       // the 'AddSocialShareProvider' right away.
       if (!providers || !providers.length) {
         this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());
         return;
       }
 
       this.toggleDropdownMenu();
     },
 
-    handleAddContextClick: function(event) {
-      event.preventDefault();
-
-      if (this.props.onAddContextClick) {
-        this.props.onAddContextClick();
-      }
-    },
-
     handleEditContextClose: function() {
       if (this.props.onEditContextClose) {
         this.props.onEditContextClose();
       }
     },
 
     render: function() {
       if (!this.props.show) {
         return null;
       }
 
-      var canAddContext = this.props.mozLoop.getLoopPref("contextInConversations.enabled") &&
-        // Don't show the link when we're showing the edit form already:
-        !this.props.showEditContext &&
-        // Don't show the link when there's already context data available:
-        !(this.props.roomData.roomContextUrls || this.props.roomData.roomDescription);
-
       var cx = React.addons.classSet;
       return (
         <div className="room-invitation-overlay">
           <div className="room-invitation-content">
             <p className={cx({hide: this.props.showEditContext})}>
-              {mozL10n.get("invite_header_text")}
+              {mozL10n.get("invite_header_text2")}
             </p>
-            <a className={cx({hide: !canAddContext, "room-invitation-addcontext": true})}
-               onClick={this.handleAddContextClick}>
-              {mozL10n.get("context_add_some_label")}
-            </a>
           </div>
           <div className={cx({
             "btn-group": true,
             "call-action-group": true,
             hide: this.props.showEditContext
           })}>
-            <button className="btn btn-info btn-email"
-                    onClick={this.handleEmailButtonClick}>
-              {mozL10n.get("email_link_button")}
-            </button>
-            <button className="btn btn-info btn-copy"
-                    onClick={this.handleCopyButtonClick}>
-              {this.state.copiedUrl ? mozL10n.get("copied_url_button") :
-                                      mozL10n.get("copy_url_button2")}
-            </button>
-            <button className="btn btn-info btn-share"
-                    onClick={this.handleShareButtonClick}
-                    ref="anchor">
-              {mozL10n.get("share_button3")}
-            </button>
+            <div className={cx({
+                "btn-copy": true,
+                "invite-button": true,
+                "triggered": this.state.copiedUrl
+              })}
+              onClick={this.handleCopyButtonClick}>
+              <img src="loop/shared/img/svg/glyph-link-16x16.svg" />
+              <p>{mozL10n.get(this.state.copiedUrl ?
+                "invite_copied_link_button" : "invite_copy_link_button")}</p>
+            </div>
+            <div className="btn-email invite-button"
+              onClick={this.handleEmailButtonClick}
+              onMouseOver={this.resetTriggeredButtons}>
+              <img src="loop/shared/img/svg/glyph-email-16x16.svg" />
+              <p>{mozL10n.get("invite_email_link_button")}</p>
+            </div>
           </div>
           <SocialShareDropdown
             dispatcher={this.props.dispatcher}
             ref="menu"
             roomUrl={this.props.roomData.roomUrl}
             show={this.state.showMenu}
             socialShareProviders={this.props.socialShareProviders} />
           <DesktopRoomEditContextView
@@ -545,16 +543,17 @@ loop.roomViews = (function(mozL10n) {
       ActiveRoomStoreMixin,
       sharedMixins.DocumentTitleMixin,
       sharedMixins.MediaSetupMixin,
       sharedMixins.RoomsAudioMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
+      chatWindowDetached: React.PropTypes.bool.isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       mozLoop: React.PropTypes.object.isRequired,
       onCallTerminated: React.PropTypes.func.isRequired,
       remotePosterUrl: React.PropTypes.string,
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
@@ -776,16 +775,17 @@ loop.roomViews = (function(mozL10n) {
                   audio={{enabled: !this.state.audioMuted, visible: true}}
                   dispatcher={this.props.dispatcher}
                   hangup={this.leaveRoom}
                   mozLoop={this.props.mozLoop}
                   publishStream={this.publishStream}
                   screenShare={screenShareData}
                   settingsMenuItems={settingsMenuItems}
                   show={!shouldRenderEditContextView}
+                  showHangup={this.props.chatWindowDetached}
                   video={{enabled: !this.state.videoMuted, visible: true}} />
                 <DesktopRoomInvitationView
                   dispatcher={this.props.dispatcher}
                   error={this.state.error}
                   mozLoop={this.props.mozLoop}
                   onAddContextClick={this.handleAddContextClick}
                   onEditContextClose={this.handleEditContextClose}
                   roomData={roomData}
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -263,16 +263,51 @@ html[dir="rtl"] .conversation-toolbar-bt
   max-width: 100%;
 }
 
 .call-action-group .btn-group-chevron,
 .call-action-group .btn-group {
   width: 100%;
 }
 
+.call-action-group > .invite-button {
+  margin: 0 4px;
+  position: relative;
+}
+
+.call-action-group > .invite-button > img {
+  background-color: #00a9dc;
+  border-radius: 100%;
+  height: 28px;
+  width: 28px;
+}
+
+.call-action-group > .invite-button:hover > img {
+  background-color: #5cccee;
+}
+
+.call-action-group > .invite-button.triggered > img {
+  background-color: #56b397;
+}
+
+.call-action-group > .invite-button > p {
+  display: none;
+  /* Position the text under the button while centering it without impacting the
+   * rest of the layout */
+  left: -10rem;
+  margin: .5rem 0 0;
+  position: absolute;
+  right: -10rem;
+}
+
+.call-action-group > .invite-button.triggered > p,
+.call-action-group > .invite-button:hover > p {
+  display: block;
+}
+
 .direct-call-failure,
 .room-failure {
   /* This flex allows us to not calculate the height of the logo area
      versus the buttons */
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: space-between;
@@ -658,63 +693,40 @@ html[dir="rtl"] .room-conversation-wrapp
   margin-top: 15px;
   width: 20px;
   height: 20px;
   background: transparent url("../img/svg/glyph-help-16x16.svg") no-repeat;
 }
 
 .room-invitation-overlay {
   position: absolute;
-  background: rgba(255, 255, 255, 0.6);
+  background: rgba(255, 255, 255, 0.85);
   top: 0;
   height: 100%;
   right: 0;
   left: 0;
   text-align: center;
-  color: #fff;
+  color: #000;
   z-index: 1010;
   display: flex;
   flex-flow: column nowrap;
   justify-content: flex-start;
   align-items: stretch;
 }
 
 .room-invitation-content {
   flex: 1 1 auto;
   display: flex;
   flex-flow: column nowrap;
   justify-content: center;
   align-items: center;
 }
 
 .room-invitation-overlay .btn-group {
-  padding: 0 0 5rem 0;
-}
-
-.room-invitation-addcontext {
-  color: #0095dd;
-  padding-left: 1.5em;
-  margin-bottom: 1em;
-  background-image: url("../img/icons-10x10.svg#edit-active");
-  background-size: 1em 1em;
-  background-repeat: no-repeat;
-  background-position: left top;
-  font-size: 1em;
-  cursor: pointer;
-}
-
-.room-invitation-addcontext:hover,
-.room-invitation-addcontext:hover:active {
-  text-decoration: underline;
-}
-
-html[dir="rtl"] .room-invitation-addcontext {
-  padding-left: 0;
-  padding-right: 1.5em;
-  background-position: right top;
+  padding: 0 0 10rem;
 }
 
 .share-service-dropdown {
   color: #000;
   text-align: start;
   bottom: auto;
   top: 0;
   overflow: hidden;
@@ -799,16 +811,17 @@ body[platform="win"] .share-service-drop
 }
 
 .room-context > .error-display-area.error {
   margin: 1em 0 .5em 0;
   text-align: center;
   text-shadow: 1px 1px 0 rgba(0,0,0,.3);
 }
 
+.room-invitation-content,
 .room-context-header {
   color: #333;
   font-size: 1.2rem;
   font-weight: bold;
   margin: 1rem auto;
 }
 
 .room-context > form {
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/svg/glyph-email-16x16.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#FFF" d="M12 10.4c0 .2-.1.4-.2.5-.1.1-.3.2-.5.2H4.7c-.2 0-.4-.1-.5-.2-.1-.1-.2-.3-.2-.5V6.9c.1.1.3.3.5.4 1 .7 1.8 1.2 2.2 1.5.1.1.3.2.4.3.1.1.2.1.4.2s.3.1.5.1.3 0 .5-.1.3-.1.4-.2c.1-.1.3-.2.4-.3.5-.4 1.2-.9 2.2-1.5.2-.1.4-.3.5-.4v3.5zm-.2-4.2c-.1.2-.3.4-.5.5-1.1.8-1.8 1.3-2.1 1.5 0 0-.1.1-.2.1-.1.2-.2.2-.3.3-.1 0-.1.1-.2.1-.1.1-.2.1-.3.1h-.4c-.1 0-.2-.1-.3-.1-.1-.1-.2-.1-.2-.1-.1-.1-.2-.1-.3-.2-.1-.1-.1-.1-.1-.2-.3-.1-.7-.4-1.2-.8-.5-.3-.8-.5-.9-.6-.2-.1-.4-.3-.6-.5S4 5.9 4 5.7c0-.2.1-.4.2-.6.1-.2.3-.2.5-.2h6.6c.2 0 .4.1.5.2.1.1.2.3.2.5s-.1.4-.2.6z"/></svg>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/svg/glyph-facebook-16x16.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#FFF" d="M12 11.6c0 .1 0 .2-.1.3s-.2.1-.3.1h-2V8.9h1l.2-1.2H9.5v-.8c0-.2 0-.3.1-.4.1-.1.2-.1.5-.1h.6V5.3h-.9c-.4-.1-.8 0-1.1.3-.3.3-.4.7-.4 1.2v.9h-1v1.2h1V12H4.4c-.1 0-.2 0-.3-.1-.1-.1-.1-.2-.1-.3V4.4c0-.1 0-.2.1-.3.1-.1.2-.1.3-.1h7.1c.1 0 .2 0 .3.1.2.1.2.2.2.3v7.2z"/></svg>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/svg/glyph-link-16x16.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#FFF" d="M12 9.9c0 .4-.1.7-.4 1l-.7.7c-.3.3-.6.4-1 .4s-.7-.1-1-.4l-1-1c-.3-.3-.4-.6-.4-1s.1-.7.4-1l-.4-.5c-.3.3-.6.4-1 .4s-.7-.1-1-.4l-1-1c-.4-.3-.5-.6-.5-1s.1-.7.4-1l.7-.7c.3-.3.6-.4 1-.4s.7.1 1 .4l1 1c.3.3.4.6.4 1s-.1.7-.4 1l.4.4c.3-.3.6-.4 1-.4s.7.1 1 .4l1 1c.4.4.5.7.5 1.1zM7.6 6.4c0-.1 0-.2-.1-.3l-1-1c-.1-.1-.2-.2-.4-.2-.1 0-.2.1-.3.2l-.7.7c-.1.1-.2.2-.2.3 0 .1 0 .2.1.3l1 1c.1.1.2.1.3.1.1 0 .3-.1.4-.2l-.1-.1v.1s0-.1-.1-.1l-.1-.1V7c0-.1 0-.2.1-.3s.2-.1.3-.1h.1s.1 0 .1.1l.1.1.1.1.1.1c.3-.3.3-.4.3-.6zm3.5 3.5c0-.1 0-.2-.1-.3l-1-1c-.2-.2-.3-.2-.4-.2-.1 0-.3.1-.4.2l.1.1.1.1s0 .1.1.1l.1.1v.1c0 .1 0 .2-.1.3s-.3.2-.4.2H9s-.1 0-.1-.1l-.1-.1-.1-.1c-.1 0-.1-.1-.2-.1-.1.1-.1.2-.1.4 0 .1 0 .2.1.3l1 1c.1.1.2.1.3.1.1 0 .2 0 .3-.1l.7-.7c.2-.1.3-.2.3-.3z"/></svg>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/svg/glyph-user-16x16.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#FFF" d="M6.5 4.3c.4-.5.9-.7 1.5-.7s1.1.2 1.5.6.6.9.6 1.5-.2 1.1-.6 1.5c-.4.6-.9.8-1.5.8s-1.1-.2-1.5-.6c-.5-.5-.7-1-.7-1.6 0-.6.2-1.1.7-1.5zm5.1 7.7c-.3.3-.6.4-1.1.4h-5c-.5 0-.8-.1-1.1-.4-.3-.3-.4-.7-.4-1.1v-.6s0-.4.1-.6c0-.2.1-.4.2-.6.1-.2.1-.4.2-.6.1-.2.2-.3.4-.5.1-.1.2-.2.4-.2s.4-.2.7-.2c0 0 .1 0 .2.1s.3.2.4.3l.6.3s.5.1.8.1c.3 0 .5 0 .8-.1l.6-.3c.2-.1.3-.2.4-.3.1 0 .2-.1.2-.1.2 0 .4 0 .6.1.2.1.4.2.5.3.1.1.2.3.4.5s.2.4.2.6c.1.2.1.4.2.6 0 .2.1.4.1.6v.6c0 .4-.1.8-.4 1.1z"/></svg>
\ No newline at end of file
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -371,17 +371,18 @@ loop.shared.views = (function(_, mozL10n
    */
   var ConversationToolbar = React.createClass({displayName: "ConversationToolbar",
     getDefaultProps: function() {
       return {
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true},
         screenShare: {state: SCREEN_SHARE_STATES.INACTIVE, visible: false},
         settingsMenuItems: null,
-        enableHangup: true
+        enableHangup: true,
+        showHangup: true
       };
     },
 
     getInitialState: function() {
       return {
         idle: false
       };
     },
@@ -392,16 +393,17 @@ loop.shared.views = (function(_, mozL10n
       enableHangup: React.PropTypes.bool,
       hangup: React.PropTypes.func.isRequired,
       hangupButtonLabel: React.PropTypes.string,
       mozLoop: React.PropTypes.object,
       publishStream: React.PropTypes.func.isRequired,
       screenShare: React.PropTypes.object,
       settingsMenuItems: React.PropTypes.array,
       show: React.PropTypes.bool.isRequired,
+      showHangup: React.PropTypes.bool,
       video: React.PropTypes.object.isRequired
     },
 
     handleClickHangup: function() {
       this.props.hangup();
     },
 
     handleToggleVideo: function() {
@@ -488,24 +490,27 @@ loop.shared.views = (function(_, mozL10n
         "idle": this.state.idle
       });
       var mediaButtonGroupCssClasses = cx({
         "conversation-toolbar-media-btn-group-box": true,
         "hide": (!this.props.video.visible && !this.props.audio.visible)
       });
       return (
         React.createElement("ul", {className: conversationToolbarCssClasses}, 
-          React.createElement("li", {className: "conversation-toolbar-btn-box btn-hangup-entry"}, 
-            React.createElement("button", {className: "btn btn-hangup", 
-                    disabled: !this.props.enableHangup, 
-                    onClick: this.handleClickHangup, 
-                    title: mozL10n.get("hangup_button_title")}, 
-              this._getHangupButtonLabel()
-            )
-          ), 
+          
+            this.props.showHangup ?
+            React.createElement("li", {className: "conversation-toolbar-btn-box btn-hangup-entry"}, 
+              React.createElement("button", {className: "btn btn-hangup", 
+                      disabled: !this.props.enableHangup, 
+                      onClick: this.handleClickHangup, 
+                      title: mozL10n.get("hangup_button_title")}, 
+                this._getHangupButtonLabel()
+              )
+            ) : null, 
+          
           React.createElement("li", {className: "conversation-toolbar-btn-box"}, 
             React.createElement("div", {className: mediaButtonGroupCssClasses}, 
                 React.createElement(MediaControlButton, {action: this.handleToggleVideo, 
                                     enabled: this.props.video.enabled, 
                                     scope: "local", type: "video", 
                                     visible: this.props.video.visible}), 
                 React.createElement(MediaControlButton, {action: this.handleToggleAudio, 
                                     enabled: this.props.audio.enabled, 
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -371,17 +371,18 @@ loop.shared.views = (function(_, mozL10n
    */
   var ConversationToolbar = React.createClass({
     getDefaultProps: function() {
       return {
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true},
         screenShare: {state: SCREEN_SHARE_STATES.INACTIVE, visible: false},
         settingsMenuItems: null,
-        enableHangup: true
+        enableHangup: true,
+        showHangup: true
       };
     },
 
     getInitialState: function() {
       return {
         idle: false
       };
     },
@@ -392,16 +393,17 @@ loop.shared.views = (function(_, mozL10n
       enableHangup: React.PropTypes.bool,
       hangup: React.PropTypes.func.isRequired,
       hangupButtonLabel: React.PropTypes.string,
       mozLoop: React.PropTypes.object,
       publishStream: React.PropTypes.func.isRequired,
       screenShare: React.PropTypes.object,
       settingsMenuItems: React.PropTypes.array,
       show: React.PropTypes.bool.isRequired,
+      showHangup: React.PropTypes.bool,
       video: React.PropTypes.object.isRequired
     },
 
     handleClickHangup: function() {
       this.props.hangup();
     },
 
     handleToggleVideo: function() {
@@ -488,24 +490,27 @@ loop.shared.views = (function(_, mozL10n
         "idle": this.state.idle
       });
       var mediaButtonGroupCssClasses = cx({
         "conversation-toolbar-media-btn-group-box": true,
         "hide": (!this.props.video.visible && !this.props.audio.visible)
       });
       return (
         <ul className={conversationToolbarCssClasses}>
-          <li className="conversation-toolbar-btn-box btn-hangup-entry">
-            <button className="btn btn-hangup"
-                    disabled={!this.props.enableHangup}
-                    onClick={this.handleClickHangup}
-                    title={mozL10n.get("hangup_button_title")}>
-              {this._getHangupButtonLabel()}
-            </button>
-          </li>
+          {
+            this.props.showHangup ?
+            <li className="conversation-toolbar-btn-box btn-hangup-entry">
+              <button className="btn btn-hangup"
+                      disabled={!this.props.enableHangup}
+                      onClick={this.handleClickHangup}
+                      title={mozL10n.get("hangup_button_title")}>
+                {this._getHangupButtonLabel()}
+              </button>
+            </li> : null
+          }
           <li className="conversation-toolbar-btn-box">
             <div className={mediaButtonGroupCssClasses}>
                 <MediaControlButton action={this.handleToggleVideo}
                                     enabled={this.props.video.enabled}
                                     scope="local" type="video"
                                     visible={this.props.video.visible}/>
                 <MediaControlButton action={this.handleToggleAudio}
                                     enabled={this.props.audio.enabled}
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -50,17 +50,21 @@ browser.jar:
   content/browser/loop/shared/img/hangup-inverse-14x14.png      (content/shared/img/hangup-inverse-14x14.png)
   content/browser/loop/shared/img/hangup-inverse-14x14@2x.png   (content/shared/img/hangup-inverse-14x14@2x.png)
   content/browser/loop/shared/img/mute-inverse-14x14.png        (content/shared/img/mute-inverse-14x14.png)
   content/browser/loop/shared/img/mute-inverse-14x14@2x.png     (content/shared/img/mute-inverse-14x14@2x.png)
   content/browser/loop/shared/img/video-inverse-14x14.png       (content/shared/img/video-inverse-14x14.png)
   content/browser/loop/shared/img/video-inverse-14x14@2x.png    (content/shared/img/video-inverse-14x14@2x.png)
   content/browser/loop/shared/img/dropdown-inverse.png          (content/shared/img/dropdown-inverse.png)
   content/browser/loop/shared/img/dropdown-inverse@2x.png       (content/shared/img/dropdown-inverse@2x.png)
+  content/browser/loop/shared/img/svg/glyph-email-16x16.svg     (content/shared/img/svg/glyph-email-16x16.svg)
+  content/browser/loop/shared/img/svg/glyph-facebook-16x16.svg  (content/shared/img/svg/glyph-facebook-16x16.svg)
   content/browser/loop/shared/img/svg/glyph-help-16x16.svg      (content/shared/img/svg/glyph-help-16x16.svg)
+  content/browser/loop/shared/img/svg/glyph-link-16x16.svg      (content/shared/img/svg/glyph-link-16x16.svg)
+  content/browser/loop/shared/img/svg/glyph-user-16x16.svg      (content/shared/img/svg/glyph-user-16x16.svg)
   content/browser/loop/shared/img/svg/exit.svg                  (content/shared/img/svg/exit.svg)
   content/browser/loop/shared/img/svg/audio.svg                 (content/shared/img/svg/audio.svg)
   content/browser/loop/shared/img/svg/audio-hover.svg           (content/shared/img/svg/audio-hover.svg)
   content/browser/loop/shared/img/svg/audio-mute.svg            (content/shared/img/svg/audio-mute.svg)
   content/browser/loop/shared/img/svg/audio-mute-hover.svg      (content/shared/img/svg/audio-mute-hover.svg)
   content/browser/loop/shared/img/svg/video.svg                 (content/shared/img/svg/video.svg)
   content/browser/loop/shared/img/svg/video-hover.svg           (content/shared/img/svg/video-hover.svg)
   content/browser/loop/shared/img/svg/video-mute.svg            (content/shared/img/svg/video-mute.svg)
--- a/browser/components/loop/modules/MozLoopPushHandler.jsm
+++ b/browser/components/loop/modules/MozLoopPushHandler.jsm
@@ -382,23 +382,19 @@ var MozLoopPushHandler = {
     * that are found in the work queue at that point.
     *
     * @param {Object} options Set of configuration options. Currently,
     *                 the only option is mocketWebSocket which will be
     *                 used for testing.
     */
   initialize: function(options = {}) {
     consoleLog.info("PushHandler: initialize options = ", options);
-    if (Services.io.offline) {
-      consoleLog.warn("PushHandler: IO offline");
-      return false;
-    }
 
     if (this._initDone) {
-      return true;
+      return;
     }
 
     this._initDone = true;
     this._retryManager = new RetryManager(this._startRetryDelay_ms,
                                           this._maxRetryDelay_ms);
     // Send an empty json payload as a ping.
     // Close the websocket and re-open if a timeout occurs.
     this._pingMonitor = new PingMonitor(() => this._pushSocket.send({}),
@@ -406,17 +402,16 @@ var MozLoopPushHandler = {
                                         this._pingInterval_ms,
                                         this._pingTimeout_ms);
 
     if ("mockWebSocket" in options) {
       this._mockWebSocket = options.mockWebSocket;
     }
 
     this._openSocket();
-    return true;
   },
 
   /**
    * Reset and clear PushServer connection.
    * Returns MozLoopPushHandler to pre-initialized state.
    */
   shutdown: function() {
     consoleLog.info("PushHandler: shutdown");
--- a/browser/components/loop/modules/MozLoopService.jsm
+++ b/browser/components/loop/modules/MozLoopService.jsm
@@ -83,16 +83,26 @@ const ROOM_DELETE = {
 const ROOM_CONTEXT_ADD = {
   ADD_FROM_PANEL: 0,
   ADD_FROM_CONVERSATION: 1
 };
 
 // See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
 const PREF_LOG_LEVEL = "loop.debug.loglevel";
 
+const kChatboxHangupButton = {
+  id: "loop-hangup",
+  visibleWhenUndocked: false,
+  onCommand: function(e, chatbox) {
+    let window = chatbox.content.contentWindow;
+    let event = new window.CustomEvent("LoopHangupNow");
+    window.dispatchEvent(event);
+  }
+};
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm");
 
@@ -900,16 +910,18 @@ var MozLoopServiceInternal = {
     // So I guess the origin is the loop server!?
     let origin = this.loopServerUri;
     let windowId = this.getChatWindowID(conversationWindowData);
 
     gConversationWindowData.set(windowId, conversationWindowData);
 
     let url = this.getChatURL(windowId);
 
+    Chat.registerButton(kChatboxHangupButton);
+
     let callback = chatbox => {
       // We need to use DOMContentLoaded as otherwise the injection will happen
       // in about:blank and then get lost.
       // Sadly we can't use chatbox.promiseChatLoaded() as promise chaining
       // involves event loop spins, which means it might be too late.
       // Have we already done it?
       if (chatbox.contentWindow.navigator.mozLoop) {
         return;
@@ -1009,21 +1021,21 @@ var MozLoopServiceInternal = {
     };
 
     let chatboxInstance = Chat.open(null, origin, "", url, undefined, undefined,
                                     callback);
     if (!chatboxInstance) {
       return null;
     // It's common for unit tests to overload Chat.open.
     } else if (chatboxInstance.setAttribute) {
-      // Set properties that influence visual appeara nce of the chatbox right
+      // Set properties that influence visual appearance of the chatbox right
       // away to circumvent glitches.
-      chatboxInstance.setAttribute("dark", true);
       chatboxInstance.setAttribute("customSize", "loopDefault");
       chatboxInstance.parentNode.setAttribute("customSize", "loopDefault");
+      Chat.loadButtonSet(chatboxInstance, "minimize,swap," + kChatboxHangupButton.id);
     }
     return windowId;
   },
 
   /**
    * Fetch Firefox Accounts (FxA) OAuth parameters from the Loop Server.
    *
    * @return {Promise} resolved with the body of the hawk request for OAuth parameters.
--- a/browser/components/loop/standalone/content/l10n/en-US/loop.properties
+++ b/browser/components/loop/standalone/content/l10n/en-US/loop.properties
@@ -52,19 +52,16 @@ call_progress_ringing_description=Ringing…
 
 help_label=Help
 
 rooms_default_room_name_template=Conversation {{conversationLabel}}
 ## LOCALIZATION_NOTE(rooms_welcome_title): {{conversationName}} will be replaced
 ## by the user specified conversation name.
 rooms_welcome_title=Welcome to {{conversationName}}
 rooms_leave_button_label=Leave
-rooms_list_copy_url_tooltip=Copy Link
-rooms_list_delete_tooltip=Delete conversation
-rooms_list_deleteConfirmation_label=Are you sure?
 rooms_new_room_button_label=Start a conversation
 rooms_only_occupant_label2=You're the only one here.
 rooms_panel_title=Choose a conversation or start a new one
 rooms_room_full_label=There are already two people in this conversation.
 rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
 rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
 rooms_room_joined_label=Someone has joined the conversation!
 rooms_room_join_label=Join the conversation
--- a/browser/components/loop/test/desktop-local/conversationAppStore_test.js
+++ b/browser/components/loop/test/desktop-local/conversationAppStore_test.js
@@ -1,40 +1,79 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 describe("loop.store.ConversationAppStore", function () {
   "use strict";
 
   var expect = chai.expect;
   var sharedActions = loop.shared.actions;
-  var sandbox, dispatcher;
+  var sandbox, activeRoomStore, dispatcher, roomUsed;
 
   beforeEach(function() {
+    roomUsed = false;
+    activeRoomStore = {
+      getStoreState: function() { return { used: roomUsed }; }
+    };
     sandbox = sinon.sandbox.create();
     dispatcher = new loop.Dispatcher();
     sandbox.stub(dispatcher, "dispatch");
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
   describe("#constructor", function() {
+    it("should throw an error if the activeRoomStore is missing", function() {
+      expect(function() {
+        new loop.store.ConversationAppStore({
+          dispatcher: dispatcher,
+          mozLoop: {}
+        });
+      }).to.Throw(/activeRoomStore/);
+    });
+
     it("should throw an error if the dispatcher is missing", function() {
       expect(function() {
-        new loop.store.ConversationAppStore({mozLoop: {}});
+        new loop.store.ConversationAppStore({
+          activeRoomStore: activeRoomStore,
+          mozLoop: {}
+        });
       }).to.Throw(/dispatcher/);
     });
 
     it("should throw an error if mozLoop is missing", function() {
       expect(function() {
-        new loop.store.ConversationAppStore({dispatcher: dispatcher});
+        new loop.store.ConversationAppStore({
+          activeRoomStore: activeRoomStore,
+          dispatcher: dispatcher
+        });
       }).to.Throw(/mozLoop/);
     });
+
+    it("should start listening to events on the window object", function() {
+      var fakeWindow = {
+        addEventListener: sinon.stub()
+      };
+
+      var store = new loop.store.ConversationAppStore({
+        activeRoomStore: activeRoomStore,
+        dispatcher: dispatcher,
+        mozLoop: { getLoopPref: function() {} },
+        rootObject: fakeWindow
+      });
+
+      var eventNames = Object.getOwnPropertyNames(store._eventHandlers);
+      sinon.assert.callCount(fakeWindow.addEventListener, eventNames.length);
+      eventNames.forEach(function(eventName) {
+        sinon.assert.calledWith(fakeWindow.addEventListener, eventName,
+          store._eventHandlers[eventName]);
+      });
+    });
   });
 
   describe("#getWindowData", function() {
     var fakeWindowData, fakeGetWindowData, fakeMozLoop, store, getLoopPrefStub;
     var setLoopPrefStub;
 
     beforeEach(function() {
       fakeWindowData = {
@@ -56,31 +95,30 @@ describe("loop.store.ConversationAppStor
           }
           return null;
         },
         getLoopPref: getLoopPrefStub,
         setLoopPref: setLoopPrefStub
       };
 
       store = new loop.store.ConversationAppStore({
+        activeRoomStore: activeRoomStore,
         dispatcher: dispatcher,
         mozLoop: fakeMozLoop
       });
     });
 
     afterEach(function() {
       sandbox.restore();
     });
 
     it("should fetch the window type from the mozLoop API", function() {
       store.getWindowData(new sharedActions.GetWindowData(fakeGetWindowData));
 
-      expect(store.getStoreState()).eql({
-        windowType: "incoming"
-      });
+      expect(store.getStoreState().windowType).eql("incoming");
     });
 
     it("should have the feedback period in initial state", function() {
       getLoopPrefStub.returns(42);
 
       // Expect ms.
       expect(store.getInitialStoreState().feedbackPeriod).to.eql(42 * 1000);
     });
@@ -130,9 +168,123 @@ describe("loop.store.ConversationAppStor
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.SetupWindowData(_.extend({
             windowId: fakeGetWindowData.windowId
           }, fakeWindowData)));
       });
   });
+
+  describe("Window object event handlers", function() {
+    var store, fakeWindow;
+
+    beforeEach(function() {
+      fakeWindow = {
+        addEventListener: sinon.stub(),
+        removeEventListener: sinon.stub()
+      };
+
+      store = new loop.store.ConversationAppStore({
+        activeRoomStore: activeRoomStore,
+        dispatcher: dispatcher,
+        mozLoop: { getLoopPref: function() {} },
+        rootObject: fakeWindow
+      });
+    });
+
+    describe("#unloadHandler", function() {
+      it("should dispatch a 'WindowUnload' action when invoked", function() {
+        store.unloadHandler();
+
+        sinon.assert.calledOnce(dispatcher.dispatch);
+        sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.WindowUnload());
+      });
+
+      it("should remove all registered event handlers from the window object", function() {
+        var eventHandlers = store._eventHandlers;
+        var eventNames = Object.getOwnPropertyNames(eventHandlers);
+
+        store.unloadHandler();
+
+        sinon.assert.callCount(fakeWindow.removeEventListener, eventNames.length);
+        expect(store._eventHandlers).to.eql(null);
+        eventNames.forEach(function(eventName) {
+          sinon.assert.calledWith(fakeWindow.removeEventListener, eventName,
+            eventHandlers[eventName]);
+        });
+      });
+    });
+
+    describe("#LoopHangupNowHandler", function() {
+      beforeEach(function() {
+        sandbox.stub(loop.shared.mixins.WindowCloseMixin, "closeWindow");
+      });
+
+      it("should dispatch the correct action for windowType 'incoming'", function() {
+        store.setStoreState({ windowType: "incoming" });
+
+        store.LoopHangupNowHandler();
+
+        sinon.assert.calledOnce(dispatcher.dispatch);
+        sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.HangupCall());
+        sinon.assert.notCalled(loop.shared.mixins.WindowCloseMixin.closeWindow);
+      });
+
+      it("should dispatch the correct action for windowType 'outgoing'", function() {
+        store.setStoreState({ windowType: "outgoing" });
+
+        store.LoopHangupNowHandler();
+
+        sinon.assert.calledOnce(dispatcher.dispatch);
+        sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.HangupCall());
+        sinon.assert.notCalled(loop.shared.mixins.WindowCloseMixin.closeWindow);
+      });
+
+      it("should dispatch the correct action when a room was used", function() {
+        store.setStoreState({ windowType: "room" });
+        roomUsed = true;
+
+        store.LoopHangupNowHandler();
+
+        sinon.assert.calledOnce(dispatcher.dispatch);
+        sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.LeaveRoom());
+        sinon.assert.notCalled(loop.shared.mixins.WindowCloseMixin.closeWindow);
+      });
+
+      it("should close the window when a room was not used", function() {
+        store.setStoreState({ windowType: "room" });
+
+        store.LoopHangupNowHandler();
+
+        sinon.assert.notCalled(dispatcher.dispatch);
+        sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);
+      });
+
+      it("should close the window for all other window types", function() {
+        store.setStoreState({ windowType: "foobar" });
+
+        store.LoopHangupNowHandler();
+
+        sinon.assert.notCalled(dispatcher.dispatch);
+        sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);
+      });
+    });
+
+    describe("#socialFrameAttachedHandler", function() {
+      it("should update the store correctly to reflect the attached state", function() {
+        store.setStoreState({ chatWindowDetached: true });
+
+        store.socialFrameAttachedHandler();
+
+        expect(store.getStoreState().chatWindowDetached).to.eql(false);
+      });
+    });
+
+    describe("#socialFrameDetachedHandler", function() {
+      it("should update the store correctly to reflect the detached state", function() {
+        store.socialFrameDetachedHandler();
+
+        expect(store.getStoreState().chatWindowDetached).to.eql(true);
+      });
+    });
+  });
 });
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -523,16 +523,17 @@ describe("loop.conversationViews", funct
 
       expect(extraMessage.textContent).eql("generic_failure_with_reason2");
     });
   });
 
   describe("OngoingConversationView", function() {
     function mountTestComponent(extraProps) {
       var props = _.extend({
+        chatWindowDetached: false,
         conversationStore: conversationStore,
         dispatcher: dispatcher,
         mozLoop: {},
         matchMedia: window.matchMedia
       }, extraProps);
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.OngoingConversationView, props));
     }
@@ -568,28 +569,16 @@ describe("loop.conversationViews", funct
         video: {
           enabled: true
         }
       });
 
       expect(view.getDOMNode().querySelector(".local video")).not.eql(null);
     });
 
-    it("should dispatch a hangupCall action when the hangup button is pressed",
-      function() {
-        view = mountTestComponent();
-
-        var hangupBtn = view.getDOMNode().querySelector(".btn-hangup");
-
-        React.addons.TestUtils.Simulate.click(hangupBtn);
-
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("name", "hangupCall"));
-      });
-
     it("should dispatch a setMute action when the audio mute button is pressed",
       function() {
         view = mountTestComponent({
           audio: {enabled: false}
         });
 
         var muteBtn = view.getDOMNode().querySelector(".btn-mute-audio");
 
@@ -643,16 +632,17 @@ describe("loop.conversationViews", funct
   });
 
   describe("CallControllerView", function() {
     var onCallTerminatedStub;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.CallControllerView, {
+          chatWindowDetached: false,
           dispatcher: dispatcher,
           mozLoop: fakeMozLoop,
           onCallTerminated: onCallTerminatedStub
         }));
     }
 
     beforeEach(function() {
       onCallTerminatedStub = sandbox.stub();
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -172,16 +172,17 @@ describe("loop.conversation", function()
         mozLoop: {},
         sdkDriver: {}
       });
       roomStore = new loop.store.RoomStore(dispatcher, {
         mozLoop: navigator.mozLoop,
         activeRoomStore: activeRoomStore
       });
       conversationAppStore = new loop.store.ConversationAppStore({
+        activeRoomStore: activeRoomStore,
         dispatcher: dispatcher,
         mozLoop: navigator.mozLoop
       });
 
       loop.store.StoreMixin.register({
         conversationAppStore: conversationAppStore,
         conversationStore: conversationStore
       });
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -9,17 +9,17 @@ describe("loop.roomViews", function () {
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var ROOM_STATES = loop.store.ROOM_STATES;
   var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
   var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
 
   var sandbox, dispatcher, roomStore, activeRoomStore, view;
-  var fakeWindow, fakeMozLoop, fakeContextURL;
+  var clock, fakeWindow, fakeMozLoop, fakeContextURL;
   var favicon = "data:image/x-icon;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
 
     dispatcher = new loop.Dispatcher();
 
     fakeMozLoop = {
@@ -43,16 +43,18 @@ describe("loop.roomViews", function () {
           }
         }),
         update: sinon.stub().callsArgWith(2, null)
       },
       telemetryAddValue: sinon.stub(),
       setLoopPref: sandbox.stub()
     };
 
+    clock = sandbox.useFakeTimers();
+
     fakeWindow = {
       close: sinon.stub(),
       document: {},
       navigator: {
         mozLoop: fakeMozLoop
       },
       addEventListener: function() {},
       removeEventListener: function() {},
@@ -87,16 +89,17 @@ describe("loop.roomViews", function () {
       location: "http://invalid.com",
       thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
     };
     sandbox.stub(dispatcher, "dispatch");
   });
 
   afterEach(function() {
     sandbox.restore();
+    clock.restore();
     loop.shared.mixins.setRootObject(window);
     view = null;
   });
 
   describe("ActiveRoomStoreMixin", function() {
     it("should merge initial state", function() {
       var TestView = React.createClass({
         mixins: [loop.roomViews.ActiveRoomStoreMixin],
@@ -245,92 +248,60 @@ describe("loop.roomViews", function () {
     describe("Copy Button", function() {
       beforeEach(function() {
         view = mountTestComponent({
           roomData: { roomUrl: "http://invalid" }
         });
       });
 
       it("should dispatch a CopyRoomUrl action when the copy button is pressed", function() {
-          var copyBtn = view.getDOMNode().querySelector(".btn-copy");
-
-          React.addons.TestUtils.Simulate.click(copyBtn);
+        var copyBtn = view.getDOMNode().querySelector(".btn-copy");
+        React.addons.TestUtils.Simulate.click(copyBtn);
 
-          sinon.assert.calledOnce(dispatcher.dispatch);
-          sinon.assert.calledWith(dispatcher.dispatch, new sharedActions.CopyRoomUrl({
-            roomUrl: "http://invalid",
-            from: "conversation"
-          }));
-        });
+        sinon.assert.calledOnce(dispatcher.dispatch);
+        sinon.assert.calledWith(dispatcher.dispatch, new sharedActions.CopyRoomUrl({
+          roomUrl: "http://invalid",
+          from: "conversation"
+        }));
+      });
 
       it("should change the text when the url has been copied", function() {
-          var copyBtn = view.getDOMNode().querySelector(".btn-copy");
-
-          React.addons.TestUtils.Simulate.click(copyBtn);
+        var copyBtn = view.getDOMNode().querySelector(".btn-copy");
+        React.addons.TestUtils.Simulate.click(copyBtn);
 
-          // copied_url_button is the l10n string.
-          expect(copyBtn.textContent).eql("copied_url_button");
+        expect(copyBtn.textContent).eql("invite_copied_link_button");
       });
-    });
 
-    describe("Share button", function() {
-      it("should dispatch a AddSocialShareProvider action when the share button is clicked", function() {
-        view = mountTestComponent();
-
-        var shareBtn = view.getDOMNode().querySelector(".btn-share");
+      it("should keep the text for a while after the url has been copied", function() {
+        var copyBtn = view.getDOMNode().querySelector(".btn-copy");
+        React.addons.TestUtils.Simulate.click(copyBtn);
+        clock.tick(loop.roomViews.DesktopRoomInvitationView.TRIGGERED_RESET_DELAY / 2);
 
-        React.addons.TestUtils.Simulate.click(shareBtn);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWith(dispatcher.dispatch,
-          new sharedActions.AddSocialShareProvider());
+        expect(copyBtn.textContent).eql("invite_copied_link_button");
       });
 
-      it("should toggle the share dropdown when the share button is clicked", function() {
-        view = mountTestComponent({
-          socialShareProviders: [{
-            name: "foo",
-            origin: "https://foo",
-            iconURL: "http://example.com/foo.png"
-          }]
-        });
+      it("should reset the text a bit after the url has been copied", function() {
+        var copyBtn = view.getDOMNode().querySelector(".btn-copy");
+        React.addons.TestUtils.Simulate.click(copyBtn);
+        clock.tick(loop.roomViews.DesktopRoomInvitationView.TRIGGERED_RESET_DELAY);
+
+        expect(copyBtn.textContent).eql("invite_copy_link_button");
+      });
 
-        var shareBtn = view.getDOMNode().querySelector(".btn-share");
+      it("should reset the text after the url has been copied then mouse over another button", function() {
+        var copyBtn = view.getDOMNode().querySelector(".btn-copy");
+        React.addons.TestUtils.Simulate.click(copyBtn);
+        var emailBtn = view.getDOMNode().querySelector(".btn-email");
+        React.addons.TestUtils.Simulate.mouseOver(emailBtn);
 
-        React.addons.TestUtils.Simulate.click(shareBtn);
-
-        expect(view.state.showMenu).to.eql(true);
-        expect(view.refs.menu.props.show).to.eql(true);
+        expect(copyBtn.textContent).eql("invite_copy_link_button");
       });
     });
 
     describe("Edit Context", function() {
-      it("should show the 'Add some context' link", function() {
-        view = mountTestComponent();
-
-        expect(view.getDOMNode().querySelector(
-          ".room-invitation-addcontext")).to.not.eql(null);
-      });
-
-      it("should call a callback when the link is clicked", function() {
-        var onAddContextClick = sinon.stub();
-        view = mountTestComponent({
-          onAddContextClick: onAddContextClick
-        });
-
-        var node = view.getDOMNode();
-        expect(node.querySelector(".room-context")).to.eql(null);
-
-        var addLink = node.querySelector(".room-invitation-addcontext");
-
-        React.addons.TestUtils.Simulate.click(addLink);
-
-        sinon.assert.calledOnce(onAddContextClick);
-      });
-
       it("should show the edit context view", function() {
         view = mountTestComponent({
           showEditContext: true
         });
 
         expect(view.getDOMNode().querySelector(".room-context")).to.not.eql(null);
       });
     });
@@ -346,16 +317,17 @@ describe("loop.roomViews", function () {
         }
         return "test";
       };
       onCallTerminatedStub = sandbox.stub();
     });
 
     function mountTestComponent(props) {
       props = _.extend({
+        chatWindowDetached: false,
         dispatcher: dispatcher,
         roomStore: roomStore,
         mozLoop: fakeMozLoop,
         onCallTerminated: onCallTerminatedStub
       }, props);
       return TestUtils.renderIntoDocument(
         React.createElement(loop.roomViews.DesktopRoomConversationView, props));
     }
@@ -424,42 +396,16 @@ describe("loop.roomViews", function () {
       var muteBtn = view.getDOMNode().querySelector(".btn-mute-video");
 
       React.addons.TestUtils.Simulate.click(muteBtn);
 
       sinon.assert.calledWithMatch(dispatcher.dispatch,
         sinon.match.hasOwn("name", "setMute"));
     });
 
-    it("should dispatch a `LeaveRoom` action when the hangup button is pressed and the room has been used", function() {
-      view = mountTestComponent();
-
-      view.setState({used: true});
-
-      var hangupBtn = view.getDOMNode().querySelector(".btn-hangup");
-
-      React.addons.TestUtils.Simulate.click(hangupBtn);
-
-      sinon.assert.calledOnce(dispatcher.dispatch);
-      sinon.assert.calledWithExactly(dispatcher.dispatch,
-        new sharedActions.LeaveRoom());
-    });
-
-    it("should close the window when the hangup button is pressed and the room has not been used", function() {
-      view = mountTestComponent();
-
-      view.setState({used: false});
-
-      var hangupBtn = view.getDOMNode().querySelector(".btn-hangup");
-
-      React.addons.TestUtils.Simulate.click(hangupBtn);
-
-      sinon.assert.calledOnce(fakeWindow.close);
-    });
-
     describe("#componentWillUpdate", function() {
       function expectActionDispatched(component) {
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           sinon.match.instanceOf(sharedActions.SetupStreamElements));
       }
 
       it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state is entered", function() {
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -498,27 +498,37 @@ describe("loop.shared.views", function()
         hangup: hangup,
         publishStream: publishStream
       });
 
       expect(comp.getDOMNode().querySelector("button.btn-hangup").textContent)
             .eql("foo");
     });
 
-    it("should accept a enableHangup optional prop", function() {
+    it("should accept an enableHangup optional prop", function() {
       var comp = mountTestComponent({
         enableHangup: false,
         hangup: hangup,
         publishStream: publishStream
       });
 
       expect(comp.getDOMNode().querySelector("button.btn-hangup").disabled)
             .eql(true);
     });
 
+    it("should accept a showHangup optional prop", function() {
+      var comp = mountTestComponent({
+        showHangup: false,
+        hangup: hangup,
+        publishStream: publishStream
+      });
+
+      expect(comp.getDOMNode().querySelector(".btn-hangup-entry")).to.eql(null);
+    });
+
     it("should hangup when hangup button is clicked", function() {
       var comp = mountTestComponent({
         hangup: hangup,
         publishStream: publishStream,
         audio: {enabled: true}
       });
 
       TestUtils.Simulate.click(
--- a/browser/components/loop/test/xpcshell/test_looppush_initialize.js
+++ b/browser/components/loop/test/xpcshell/test_looppush_initialize.js
@@ -2,40 +2,33 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 var dummyCallback = () => {};
 var mockWebSocket = new MockWebSocketChannel();
 var pushServerRequestCount = 0;
 
-add_test(function test_initalize_offline() {
-  Services.io.offline = true;
-  do_check_false(MozLoopPushHandler.initialize());
-  Services.io.offline = false;
-  run_next_test();
-});
-
 add_test(function test_initalize_missing_chanid() {
   Assert.throws(() => MozLoopPushHandler.register(null, dummyCallback, dummyCallback));
   run_next_test();
 });
 
 add_test(function test_initalize_missing_regcallback() {
   Assert.throws(() => MozLoopPushHandler.register("chan-1", null, dummyCallback));
   run_next_test();
 });
 
 add_test(function test_initalize_missing_notifycallback() {
   Assert.throws(() => MozLoopPushHandler.register("chan-1", dummyCallback, null));
   run_next_test();
 });
 
 add_test(function test_initalize_websocket() {
-  do_check_true(MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket}));
+  MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket});
   MozLoopPushHandler.register(
     "chan-1",
     function(err, url, id) {
       Assert.equal(err, null, "err should be null to indicate success");
       Assert.equal(url, kEndPointUrl, "Should return push server application URL");
       Assert.equal(id, "chan-1", "Should have channel id = chan-1");
       Assert.equal(mockWebSocket.uri.prePath, kServerPushUrl,
                    "Should have the url from preferences");
@@ -187,17 +180,17 @@ add_test(function test_retry_pushurl() {
       response.processAsync();
       response.finish();
 
       run_next_test();
       break;
     }
   });
 
-  do_check_true(MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket}));
+  MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket});
 });
 
 function run_test() {
   setupFakeLoopServer();
 
   loopServer.registerPathHandler("/push-server-config", (request, response) => {
     response.setStatusLine(null, 200, "OK");
     response.write(JSON.stringify({pushServerURI: kServerPushUrl}));
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -1217,16 +1217,17 @@
             React.createElement(FramedExample, {dashed: true, 
                            height: 394, 
                            onContentsRendered: conversationStores[0].forcedUpdate, 
                            summary: "Desktop ongoing conversation window", 
                            width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(OngoingConversationView, {
                   audio: { enabled: true, visible: true}, 
+                  chatWindowDetached: false, 
                   conversationStore: conversationStores[0], 
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mediaConnected: true, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   remoteVideoEnabled: true, 
                   video: { enabled: true, visible: true}})
               )
@@ -1235,16 +1236,17 @@
             React.createElement(FramedExample, {dashed: true, 
                            height: 400, 
                            onContentsRendered: conversationStores[1].forcedUpdate, 
                            summary: "Desktop ongoing conversation window (medium)", 
                            width: 600}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(OngoingConversationView, {
                   audio: { enabled: true, visible: true}, 
+                  chatWindowDetached: false, 
                   conversationStore: conversationStores[1], 
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mediaConnected: true, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   remoteVideoEnabled: true, 
                   video: { enabled: true, visible: true}})
               )
@@ -1252,16 +1254,17 @@
 
             React.createElement(FramedExample, {height: 600, 
                            onContentsRendered: conversationStores[2].forcedUpdate, 
                            summary: "Desktop ongoing conversation window (large)", 
                            width: 800}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(OngoingConversationView, {
                   audio: { enabled: true, visible: true}, 
+                  chatWindowDetached: false, 
                   conversationStore: conversationStores[2], 
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mediaConnected: true, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   remoteVideoEnabled: true, 
                   video: { enabled: true, visible: true}})
               )
@@ -1270,16 +1273,17 @@
             React.createElement(FramedExample, {dashed: true, 
                            height: 394, 
                            onContentsRendered: conversationStores[3].forcedUpdate, 
                            summary: "Desktop ongoing conversation window - local face mute", 
                            width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(OngoingConversationView, {
                   audio: { enabled: true, visible: true}, 
+                  chatWindowDetached: false, 
                   conversationStore: conversationStores[3], 
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mediaConnected: true, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   remoteVideoEnabled: true, 
                   video: { enabled: false, visible: true}})
               )
@@ -1288,16 +1292,17 @@
             React.createElement(FramedExample, {dashed: true, 
                            height: 394, 
                            onContentsRendered: conversationStores[4].forcedUpdate, 
                            summary: "Desktop ongoing conversation window - remote face mute", 
                            width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(OngoingConversationView, {
                   audio: { enabled: true, visible: true}, 
+                  chatWindowDetached: false, 
                   conversationStore: conversationStores[4], 
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mediaConnected: true, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   remoteVideoEnabled: false, 
                   video: { enabled: true, visible: true}})
               )
@@ -1383,16 +1388,17 @@
 
           React.createElement(Section, {name: "DesktopRoomConversationView"}, 
             React.createElement(FramedExample, {height: 398, 
                            onContentsRendered: invitationRoomStore.activeRoomStore.forcedUpdate, 
                            summary: "Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)", 
                            width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
+                  chatWindowDetached: false, 
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
                   onCallTerminated: function(){}, 
                   roomState: ROOM_STATES.INIT, 
                   roomStore: invitationRoomStore})
               )
             ), 
@@ -1418,16 +1424,17 @@
                            height: 394, 
                            onContentsRendered: desktopRoomStoreLoading.activeRoomStore.forcedUpdate, 
                            summary: "Desktop room conversation (loading)", 
                            width: 298}, 
               /* Hide scrollbars here. Rotating loading div overflows and causes
                scrollbars to appear */
               React.createElement("div", {className: "fx-embedded overflow-hidden"}, 
                 React.createElement(DesktopRoomConversationView, {
+                  chatWindowDetached: false, 
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
                   onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                   roomStore: desktopRoomStoreLoading})
               )
@@ -1435,16 +1442,17 @@
 
             React.createElement(FramedExample, {dashed: true, 
                            height: 394, 
                            onContentsRendered: roomStore.activeRoomStore.forcedUpdate, 
                            summary: "Desktop room conversation", 
                            width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
+                  chatWindowDetached: false, 
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
                   onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                   roomStore: roomStore})
               )
@@ -1452,16 +1460,17 @@
 
             React.createElement(FramedExample, {dashed: true, 
                            height: 482, 
                            onContentsRendered: desktopRoomStoreMedium.activeRoomStore.forcedUpdate, 
                            summary: "Desktop room conversation (medium)", 
                            width: 602}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
+                  chatWindowDetached: false, 
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
                   onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                   roomStore: desktopRoomStoreMedium})
               )
@@ -1469,16 +1478,17 @@
 
             React.createElement(FramedExample, {dashed: true, 
                            height: 485, 
                            onContentsRendered: desktopRoomStoreLarge.activeRoomStore.forcedUpdate, 
                            summary: "Desktop room conversation (large)", 
                            width: 646}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
+                  chatWindowDetached: false, 
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
                   onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                   roomStore: desktopRoomStoreLarge})
               )
@@ -1486,31 +1496,33 @@
 
             React.createElement(FramedExample, {dashed: true, 
                            height: 394, 
                            onContentsRendered: desktopLocalFaceMuteRoomStore.activeRoomStore.forcedUpdate, 
                            summary: "Desktop room conversation local face-mute", 
                            width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
+                  chatWindowDetached: false, 
                   dispatcher: dispatcher, 
                   mozLoop: navigator.mozLoop, 
                   onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomStore: desktopLocalFaceMuteRoomStore})
               )
             ), 
 
             React.createElement(FramedExample, {dashed: true, 
                            height: 394, 
                            onContentsRendered: desktopRemoteFaceMuteRoomStore.activeRoomStore.forcedUpdate, 
                            summary: "Desktop room conversation remote face-mute", 
                            width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
+                  chatWindowDetached: false, 
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
                   onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomStore: desktopRemoteFaceMuteRoomStore})
               )
             )
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -1217,16 +1217,17 @@
             <FramedExample dashed={true}
                            height={394}
                            onContentsRendered={conversationStores[0].forcedUpdate}
                            summary="Desktop ongoing conversation window"
                            width={298}>
               <div className="fx-embedded">
                 <OngoingConversationView
                   audio={{ enabled: true, visible: true }}
+                  chatWindowDetached={false}
                   conversationStore={conversationStores[0]}
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mediaConnected={true}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   remoteVideoEnabled={true}
                   video={{ enabled: true, visible: true }} />
               </div>
@@ -1235,16 +1236,17 @@
             <FramedExample dashed={true}
                            height={400}
                            onContentsRendered={conversationStores[1].forcedUpdate}
                            summary="Desktop ongoing conversation window (medium)"
                            width={600}>
               <div className="fx-embedded">
                 <OngoingConversationView
                   audio={{ enabled: true, visible: true }}
+                  chatWindowDetached={false}
                   conversationStore={conversationStores[1]}
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mediaConnected={true}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   remoteVideoEnabled={true}
                   video={{ enabled: true, visible: true }} />
               </div>
@@ -1252,16 +1254,17 @@
 
             <FramedExample height={600}
                            onContentsRendered={conversationStores[2].forcedUpdate}
                            summary="Desktop ongoing conversation window (large)"
                            width={800}>
               <div className="fx-embedded">
                 <OngoingConversationView
                   audio={{ enabled: true, visible: true }}
+                  chatWindowDetached={false}
                   conversationStore={conversationStores[2]}
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mediaConnected={true}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   remoteVideoEnabled={true}
                   video={{ enabled: true, visible: true }} />
               </div>
@@ -1270,16 +1273,17 @@
             <FramedExample dashed={true}
                            height={394}
                            onContentsRendered={conversationStores[3].forcedUpdate}
                            summary="Desktop ongoing conversation window - local face mute"
                            width={298}>
               <div className="fx-embedded">
                 <OngoingConversationView
                   audio={{ enabled: true, visible: true }}
+                  chatWindowDetached={false}
                   conversationStore={conversationStores[3]}
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mediaConnected={true}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   remoteVideoEnabled={true}
                   video={{ enabled: false, visible: true }} />
               </div>
@@ -1288,16 +1292,17 @@
             <FramedExample dashed={true}
                            height={394}
                            onContentsRendered={conversationStores[4].forcedUpdate}
                            summary="Desktop ongoing conversation window - remote face mute"
                            width={298} >
               <div className="fx-embedded">
                 <OngoingConversationView
                   audio={{ enabled: true, visible: true }}
+                  chatWindowDetached={false}
                   conversationStore={conversationStores[4]}
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mediaConnected={true}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   remoteVideoEnabled={false}
                   video={{ enabled: true, visible: true }} />
               </div>
@@ -1383,16 +1388,17 @@
 
           <Section name="DesktopRoomConversationView">
             <FramedExample height={398}
                            onContentsRendered={invitationRoomStore.activeRoomStore.forcedUpdate}
                            summary="Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)"
                            width={298}>
               <div className="fx-embedded">
                 <DesktopRoomConversationView
+                  chatWindowDetached={false}
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
                   onCallTerminated={function(){}}
                   roomState={ROOM_STATES.INIT}
                   roomStore={invitationRoomStore} />
               </div>
             </FramedExample>
@@ -1418,16 +1424,17 @@
                            height={394}
                            onContentsRendered={desktopRoomStoreLoading.activeRoomStore.forcedUpdate}
                            summary="Desktop room conversation (loading)"
                            width={298}>
               {/* Hide scrollbars here. Rotating loading div overflows and causes
                scrollbars to appear */}
               <div className="fx-embedded overflow-hidden">
                 <DesktopRoomConversationView
+                  chatWindowDetached={false}
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
                   onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomState={ROOM_STATES.HAS_PARTICIPANTS}
                   roomStore={desktopRoomStoreLoading} />
               </div>
@@ -1435,16 +1442,17 @@
 
             <FramedExample dashed={true}
                            height={394}
                            onContentsRendered={roomStore.activeRoomStore.forcedUpdate}
                            summary="Desktop room conversation"
                            width={298}>
               <div className="fx-embedded">
                 <DesktopRoomConversationView
+                  chatWindowDetached={false}
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
                   onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomState={ROOM_STATES.HAS_PARTICIPANTS}
                   roomStore={roomStore} />
               </div>
@@ -1452,16 +1460,17 @@
 
             <FramedExample dashed={true}
                            height={482}
                            onContentsRendered={desktopRoomStoreMedium.activeRoomStore.forcedUpdate}
                            summary="Desktop room conversation (medium)"
                            width={602}>
               <div className="fx-embedded">
                 <DesktopRoomConversationView
+                  chatWindowDetached={false}
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
                   onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomState={ROOM_STATES.HAS_PARTICIPANTS}
                   roomStore={desktopRoomStoreMedium} />
               </div>
@@ -1469,16 +1478,17 @@
 
             <FramedExample dashed={true}
                            height={485}
                            onContentsRendered={desktopRoomStoreLarge.activeRoomStore.forcedUpdate}
                            summary="Desktop room conversation (large)"
                            width={646}>
               <div className="fx-embedded">
                 <DesktopRoomConversationView
+                  chatWindowDetached={false}
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
                   onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomState={ROOM_STATES.HAS_PARTICIPANTS}
                   roomStore={desktopRoomStoreLarge} />
               </div>
@@ -1486,31 +1496,33 @@
 
             <FramedExample dashed={true}
                            height={394}
                            onContentsRendered={desktopLocalFaceMuteRoomStore.activeRoomStore.forcedUpdate}
                            summary="Desktop room conversation local face-mute"
                            width={298}>
               <div className="fx-embedded">
                 <DesktopRoomConversationView
+                  chatWindowDetached={false}
                   dispatcher={dispatcher}
                   mozLoop={navigator.mozLoop}
                   onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomStore={desktopLocalFaceMuteRoomStore} />
               </div>
             </FramedExample>
 
             <FramedExample dashed={true}
                            height={394}
                            onContentsRendered={desktopRemoteFaceMuteRoomStore.activeRoomStore.forcedUpdate}
                            summary="Desktop room conversation remote face-mute"
                            width={298} >
               <div className="fx-embedded">
                 <DesktopRoomConversationView
+                  chatWindowDetached={false}
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
                   onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomStore={desktopRemoteFaceMuteRoomStore} />
               </div>
             </FramedExample>
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -13,17 +13,16 @@ support-files =
 [browser_bug1018066_resetScrollPosition.js]
 [browser_bug1020245_openPreferences_to_paneContent.js]
 [browser_change_app_handler.js]
 skip-if = os != "win" # This test tests the windows-specific app selection dialog, so can't run on non-Windows
 [browser_chunk_permissions.js]
 [browser_connection.js]
 [browser_connection_bug388287.js]
 [browser_cookies_exceptions.js]
-skip-if = os == "linux" # See bug 1209521 for re-enabling on Linux
 [browser_healthreport.js]
 skip-if = !healthreport || (os == 'linux' && debug)
 [browser_permissions.js]
 [browser_proxy_backup.js]
 [browser_privacypane_1.js]
 [browser_privacypane_3.js]
 [browser_privacypane_4.js]
 [browser_privacypane_5.js]
--- a/browser/components/preferences/in-content/tests/browser_cookies_exceptions.js
+++ b/browser/components/preferences/in-content/tests/browser_cookies_exceptions.js
@@ -1,13 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function test() {
   waitForExplicitFinish();
+  requestLongerTimeout(2);
   testRunner.runTests();
 }
 
 var testRunner = {
 
   tests:
     [
       {
--- a/browser/components/search/test/browser_contextmenu.js
+++ b/browser/components/search/test/browser_contextmenu.js
@@ -62,17 +62,17 @@ function test() {
       is(searchItem.disabled, false, "Check that search context menu item is enabled");
       doOnloadOnce(checkSearchURL);
       searchItem.click();
       contextMenu.hidePopup();
     }
 
     function checkSearchURL(event) {
       is(event.originalTarget.URL,
-         "http://mochi.test:8888/browser/browser/components/search/test/?test=test+search&ie=utf-8&client=app&channel=contextsearch",
+         "http://mochi.test:8888/browser/browser/components/search/test/?test=test+search&ie=utf-8&channel=contextsearch",
          "Checking context menu search URL");
       // Remove the tab opened by the search
       gBrowser.removeCurrentTab();
       ss.removeEngine(ss.currentEngine);
     }
 
     var selectionListener = {
       notifySelectionChanged: function(doc, sel, reason) {
--- a/browser/components/search/test/testEngine_mozsearch.xml
+++ b/browser/components/search/test/testEngine_mozsearch.xml
@@ -2,14 +2,13 @@
   <ShortName>Foo</ShortName>
   <Description>Foo Search</Description>
   <InputEncoding>utf-8</InputEncoding>
   <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
   <Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/?suggestions&amp;locale={moz:locale}&amp;test={searchTerms}"/>
   <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/">
     <Param name="test" value="{searchTerms}"/>
     <Param name="ie" value="utf-8"/>
-    <MozParam name="client" condition="defaultEngine" trueValue="app-default" falseValue="app"/>
     <MozParam name="channel" condition="purpose" purpose="keyword" value="keywordsearch"/>
     <MozParam name="channel" condition="purpose" purpose="contextmenu" value="contextsearch"/>
   </Url>
   <SearchForm>http://mochi.test:8888/browser/browser/components/search/test/</SearchForm>
 </SearchPlugin>
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -771,12 +771,30 @@ e10s.postActivationInfobar.message = You
 e10s.postActivationInfobar.learnMore.label = Learn More
 e10s.postActivationInfobar.learnMore.accesskey = L
 e10s.accessibilityNotice.mainMessage = Multi-process does not yet support accessibility features. Multi-process will be disabled if you restart %S. Would you like to restart?
 e10s.accessibilityNotice.disableAndRestart.label = Disable and Restart
 e10s.accessibilityNotice.disableAndRestart.accesskey = R
 e10s.accessibilityNotice.dontDisable.label = Don't Disable
 e10s.accessibilityNotice.dontDisable.accesskey = D
 
+# LOCALIZATION NOTE (usercontext.personal.label,
+#                    usercontext.work.label,
+#                    usercontext.shopping.label,
+#                    usercontext.banking.label):
+# These strings specify the four default contexts included in support of the
+# Contextual Identity / Containers project. Each context is meant to represent
+# the context that the user is in when interacting with the site. Different
+# contexts will store cookies and other information from those sites in
+# different, isolated locations. You can enable the feature by typing
+# about:config in the URL bar and changing privacy.userContext.enabled to true.
+# Once enabled, you can open a new tab in a specific context by clicking
+# File > New Container Tab > (1 of 4 contexts). Once opened, you will see these
+# strings on the right-hand side of the URL bar.
+usercontext.personal.label = Personal
+usercontext.work.label = Work
+usercontext.shopping.label = Shopping
+usercontext.banking.label = Banking
+
 muteTab.label = Mute Tab
 muteTab.accesskey = M
 unmuteTab.label = Unmute Tab
 unmuteTab.accesskey = M
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -25,31 +25,24 @@ sign_in_again_button=Sign In
 ## will be replaced by the super short brandname.
 sign_in_again_use_as_guest_button2=Use {{clientSuperShortname}} as a Guest
 
 first_time_experience_button_label=Get Started
 ## LOCALIZATION_NOTE(first_time_experience_subheading): Message inviting the
 ## user to create his or her first conversation.
 first_time_experience_subheading=Join the conversation
 
-invite_header_text=Invite someone to join you.
 invite_header_text2=Invite a friend to join you
-## LOCALIZATION_NOTE(invite_facebook_button, invite_facebook_triggered,
-## invite_contacts_button, invite_contacts_triggered, invite_copy_button,
-## invite_copy_triggered, invite_email_button, invite_email_triggered): These
-## button/triggered pairs are labels under an iconic button that switch to the
-## triggered text when clicked/activated.
-invite_facebook_button=share on Facebook
-invite_facebook_triggered=shared!
-invite_contacts_button=share with contacts
-invite_contacts_triggered=shared!
-invite_copy_button=copy link
-invite_copy_triggered=copied!
-invite_email_button=email link
-invite_email_triggered=emailed!
+## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
+## invite_email_link_button, invite_facebook_button2): These labels appear under
+## an iconic button for the invite view.
+invite_copy_link_button=Copy Link
+invite_copied_link_button=Copied!
+invite_email_link_button=Email Link
+invite_facebook_button2=Share on Facebook
 
 # Status text
 display_name_guest=Guest
 display_name_dnd_status=Do Not Disturb
 display_name_available_status=Available
 
 # Error bars
 ## LOCALIZATION NOTE(unable_retrieve_url,session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
@@ -80,18 +73,22 @@ share_email_body_context2=Join me for a video conversation. Click the Firefox Hello link to connect now: {{callUrl}}\n\nLet’s talk about this during our conversation: {{title}}
 share_email_footer=\n\n________\nJoin and create video conversations free with Firefox Hello. Connect easily over video with anyone, anywhere. No downloads or registration. Learn more at http://www.firefox.com/hello
 ## LOCALIZATION NOTE (share_tweeet): In this item, don't translate the part
 ## between {{..}}. Please keep the text below 117 characters to make sure it fits
 ## in a tweet.
 share_tweet=Join me for a video conversation on {{clientShortname2}}!
 
 share_button3=Share Link
 share_add_service_button=Add a Service
-copy_url_button2=Copy Link
-copied_url_button=Copied!
+
+## LOCALIZATION NOTE (copy_link_menuitem, email_link_menuitem, delete_conversation_menuitem):
+## These menu items are displayed from a panel's context menu for a conversation.
+copy_link_menuitem=Copy Link
+email_link_menuitem=Email Link
+delete_conversation_menuitem=Delete conversation
 
 panel_footer_signin_or_signup_link=Sign In or Sign Up
 
 settings_menu_item_account=Account
 settings_menu_item_settings=Settings
 settings_menu_item_signout=Sign Out
 settings_menu_item_signin=Sign In
 settings_menu_button_tooltip=Settings
@@ -286,20 +283,19 @@ tos_failure_message={{clientShortname}} 
 
 ## LOCALIZATION NOTE (contact_offline_title): Title which is displayed when the
 ## contact is offline.
 contact_offline_title=This person is not online
 ## LOCALIZATION NOTE (call_timeout_notification_text): Title which is displayed
 ## when the call didn't go through.
 call_timeout_notification_text=Your call did not go through.
 
-## LOCALIZATION NOTE (retry_call_button, cancel_button, email_link_button):
+## LOCALIZATION NOTE (retry_call_button, cancel_button):
 ## These buttons are displayed when a call has failed.
 retry_call_button=Retry
-email_link_button=Email Link
 cancel_button=Cancel
 rejoin_button=Rejoin Conversation
 
 cannot_start_call_session_not_ready=Can't start call, session is not ready.
 network_disconnected=The network connection terminated abruptly.
 connection_error_see_console_notification=Call failed; see console for details.
 no_media_failure_message=No camera or microphone found.
 
@@ -329,22 +325,19 @@ feedback_request_button=Leave Feedback
 
 help_label=Help
 tour_label=Tour
 
 ## LOCALIZATION NOTE(rooms_default_room_name_template): {{conversationLabel}}
 ## will be replaced by a number. For example "Conversation 1" or "Conversation 12".
 rooms_default_room_name_template=Conversation {{conversationLabel}}
 rooms_leave_button_label=Leave
-rooms_list_copy_url_tooltip=Copy Link
 ## LOCALIZATION NOTE (rooms_list_recent_conversations): String is in all caps
 ## for emphasis reasons, it is a heading. Proceed as appropriate for locale.
 rooms_list_recent_conversations=RECENT CONVERSATIONS
-rooms_list_delete_tooltip=Delete conversation
-rooms_list_deleteConfirmation_label=Are you sure?
 rooms_change_failed_label=Conversation cannot be updated
 rooms_new_room_button_label=Start a conversation
 rooms_panel_title=Choose a conversation or start a new one
 rooms_room_full_label=There are already two people in this conversation.
 rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
 rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
 rooms_room_joined_label=Someone has joined the conversation!
 rooms_room_join_label=Join the conversation
@@ -365,27 +358,19 @@ infobar_menuitem_dontshowagain_accesskey
 # section.
 context_inroom_header=Let's Talk About…
 # LOCALIZATION NOTE (context_inroom_label2): this string is followed by the
 # title and domain of the website you are having a conversation about, displayed on a
 # separate line. If this structure doesn't work for your locale, you might want
 # to consider this as a stand-alone title. See example screenshot:
 # https://bug1115342.bugzilla.mozilla.org/attachment.cgi?id=8563677
 context_inroom_label2=Let's Talk About:
-## LOCALIZATION_NOTE (context_edit_activate_label): {{title}} will be replaced
-## by the title of the active tab, also known as the title of an HTML document.
-## The quotes around the title are intentional.
-context_edit_activate_label=Talk about "{{title}}"
 context_edit_name_placeholder=Conversation Name
 context_edit_comments_placeholder=Comments
-context_add_some_label=Add some context
-context_show_tooltip=Show Context
 context_cancel_label=Cancel
 context_done_label=Done
-context_link_modified=This link was modified.
-context_learn_more_link_label=Learn more.
 conversation_settings_menu_edit_context=Edit Context
 conversation_settings_menu_hide_context=Hide Context
 
 
 # Text chat strings
 
 chat_textbox_placeholder=Type here…
--- a/browser/modules/Chat.jsm
+++ b/browser/modules/Chat.jsm
@@ -1,25 +1,30 @@
 /* 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"];
+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"]);
+let 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
@@ -200,9 +205,105 @@ var Chat = {
     }
     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 = [for (button of buttonSet.split(",")) 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/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1832,49 +1832,16 @@ toolbarbutton.chevron > .toolbarbutton-i
 }
 
 .social-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
 }
 
 %include ../shared/social/chat.inc.css
 
-.chat-titlebar {
-  background-color: #d9d9d9;
-  background-image: linear-gradient(@toolbarHighlight@, transparent);
-}
-
-.chat-titlebar[selected] {
-  background-color: #f0f0f0;
-}
-
-.chatbar-button {
-  -moz-appearance: none;
-  background-color: #d9d9d9;
-  background-image: linear-gradient(@toolbarHighlight@, transparent);
-}
-
-.chatbar-button > .toolbarbutton-icon {
-  -moz-margin-end: 0;
-}
-
-.chatbar-button:hover,
-.chatbar-button[open="true"] {
-  background-color: #f0f0f0;
-}
-
-.chatbar-button[activity] {
-  background-image: radial-gradient(circle farthest-corner at center 3px, rgb(233,242,252) 3%, rgba(172,206,255,0.75) 40%, rgba(87,151,201,0.5) 80%, transparent);
-}
-
-chatbox {
-  border-top-left-radius: 2.5px;
-  border-top-right-radius: 2.5px;
-}
-
 /* 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/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -51,16 +51,17 @@ browser.jar:
   skin/classic/browser/reload-stop-go@2x.png
   skin/classic/browser/searchbar.css
   skin/classic/browser/Security-broken.png
   skin/classic/browser/setDesktopBackground.css
   skin/classic/browser/slowStartup-16.png
   skin/classic/browser/Toolbar.png
   skin/classic/browser/Toolbar-inverted.png
   skin/classic/browser/Toolbar-small.png
+  skin/classic/browser/webRTC-indicator.css
   skin/classic/browser/loop/menuPanel.png             (loop/menuPanel.png)
   skin/classic/browser/loop/menuPanel@2x.png          (loop/menuPanel@2x.png)
   skin/classic/browser/loop/toolbar.png               (loop/toolbar.png)
   skin/classic/browser/loop/toolbar@2x.png            (loop/toolbar@2x.png)
   skin/classic/browser/loop/toolbar-inverted.png      (loop/toolbar-inverted.png)
   skin/classic/browser/loop/toolbar-inverted@2x.png   (loop/toolbar-inverted@2x.png)
 * skin/classic/browser/controlcenter/panel.css        (controlcenter/panel.css)
   skin/classic/browser/customizableui/background-noise-toolbar.png  (customizableui/background-noise-toolbar.png)
rename from browser/themes/shared/webrtc/indicator.css
rename to browser/themes/linux/webRTC-indicator.css
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -3433,53 +3433,16 @@ notification[value="loop-sharing-notific
   border-top-left-radius: inherit;
   border-top-right-radius: inherit;
 }
 
 /* === end of social toolbar provider menu === */
 
 %include ../shared/social/chat.inc.css
 
-.chat-titlebar {
-  background-color: #d9d9d9;
-  background-image: linear-gradient(rgba(255,255,255,.43), transparent);
-}
-
-.chat-titlebar[selected] {
-  background-color: #f0f0f0;
-}
-
-.chatbar-button {
-  background-color: #d9d9d9;
-  background-image: linear-gradient(rgba(255,255,255,.43), transparent);
-  border-top-left-radius: @toolbarbuttonCornerRadius@;
-  border-top-right-radius: @toolbarbuttonCornerRadius@;
-}
-
-.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%);
-}
-
-chatbox {
-  border-top-left-radius: @toolbarbuttonCornerRadius@;
-  border-top-right-radius: @toolbarbuttonCornerRadius@;
-}
-
-window > chatbox {
-  border-top-left-radius: @toolbarbuttonCornerRadius@;
-  border-top-right-radius: @toolbarbuttonCornerRadius@;
-  border-bottom-left-radius: @toolbarbuttonCornerRadius@;
-  border-bottom-right-radius: @toolbarbuttonCornerRadius@;
-}
-
 /* Customization mode */
 
 %include ../shared/customizableui/customizeMode.inc.css
 
 #main-window[customizing] {
   background-color: rgb(178,178,178);
 }
 
--- a/browser/themes/shared/fullscreen/warning.inc.css
+++ b/browser/themes/shared/fullscreen/warning.inc.css
@@ -1,52 +1,51 @@
 %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
 
-#fullscreen-warning {
+html|*#fullscreen-warning {
   align-items: center;
   background: rgba(45, 62, 72, 0.9);
   border: 2px solid #fafafa;
   box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.5);
   border-radius: 8px;
   padding: 24px 16px;
   font: message-box;
 }
 
-#fullscreen-warning::before {
+html|*#fullscreen-warning::before {
   margin: 0;
   width: 24px; height: 24px;
 }
 
-#fullscreen-warning.verifiedIdentity::before,
-#fullscreen-warning.verifiedDomain::before {
+html|*#fullscreen-warning.verifiedIdentity::before,
+html|*#fullscreen-warning.verifiedDomain::before {
   content: url("chrome://browser/skin/fullscreen/secure.svg");
 }
 
-#fullscreen-warning.unknownIdentity::before {
+html|*#fullscreen-warning.unknownIdentity::before {
   content: url("chrome://browser/skin/fullscreen/insecure.svg");
 }
 
-#fullscreen-domain-text,
-#fullscreen-generic-text {
+html|*#fullscreen-domain-text,
+html|*#fullscreen-generic-text {
   font-size: 21px;
   font-weight: lighter;
   color: #fafafa;
   margin: 0 16px;
 }
 
-#fullscreen-domain {
+html|*#fullscreen-domain {
   font-weight: bold;
   margin: 0;
 }
 
-#fullscreen-exit-button {
-  padding: 0 30px;
+html|*#fullscreen-exit-button {
+  padding: 5px 30px;
   font: message-box;
   font-size: 14px;
   font-weight: lighter;
   margin: 0;
-  height: 28px;
   box-sizing: content-box;
 }
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -140,17 +140,16 @@
   skin/classic/browser/webRTC-sharingMicrophone-16.png         (../shared/webrtc/webRTC-sharingMicrophone-16.png)
   skin/classic/browser/webRTC-sharingMicrophone-16@2x.png      (../shared/webrtc/webRTC-sharingMicrophone-16@2x.png)
   skin/classic/browser/webRTC-shareScreen-16.png               (../shared/webrtc/webRTC-shareScreen-16.png)
   skin/classic/browser/webRTC-shareScreen-16@2x.png            (../shared/webrtc/webRTC-shareScreen-16@2x.png)
   skin/classic/browser/webRTC-shareScreen-64.png               (../shared/webrtc/webRTC-shareScreen-64.png)
   skin/classic/browser/webRTC-shareScreen-64@2x.png            (../shared/webrtc/webRTC-shareScreen-64@2x.png)
   skin/classic/browser/webRTC-sharingScreen-16.png             (../shared/webrtc/webRTC-sharingScreen-16.png)
   skin/classic/browser/webRTC-sharingScreen-16@2x.png          (../shared/webrtc/webRTC-sharingScreen-16@2x.png)
-  skin/classic/browser/webRTC-indicator.css                    (../shared/webrtc/indicator.css)
   skin/classic/browser/webRTC-camera-white-16.png              (../shared/webrtc/camera-white-16.png)
   skin/classic/browser/webRTC-microphone-white-16.png          (../shared/webrtc/microphone-white-16.png)
   skin/classic/browser/webRTC-screen-white-16.png              (../shared/webrtc/screen-white-16.png)
   skin/classic/browser/panic-panel/header.png                  (../shared/panic-panel/header.png)
   skin/classic/browser/panic-panel/header@2x.png               (../shared/panic-panel/header@2x.png)
   skin/classic/browser/panic-panel/header-small.png            (../shared/panic-panel/header-small.png)
   skin/classic/browser/panic-panel/header-small@2x.png         (../shared/panic-panel/header-small@2x.png)
   skin/classic/browser/panic-panel/icons.png                   (../shared/panic-panel/icons.png)
--- a/browser/themes/shared/social/chat-icons.svg
+++ b/browser/themes/shared/social/chat-icons.svg
@@ -3,33 +3,47 @@
    - 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: #c1c1c1;
+      fill: #666;
+    }
+    use[id$="-hover"] {
+      fill: #4a4a4a;
     }
     use[id$="-active"] {
-      fill: #c1c1c1;
+      fill: #4a4a4a;
     }
     use[id$="-disabled"] {
-      fill: #c1c1c1;
+      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"/>
-    <polygon id="expand-shape" points="10,0 4.838,0 6.506,1.669 0,8.175 1.825,10 8.331,3.494 10,5.162"/>
-    <rect id="minimize-shape" y="3.6" width="10" height="2.8"/>
+    <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="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"/>
 </svg>
--- a/browser/themes/shared/social/chat.inc.css
+++ b/browser/themes/shared/social/chat.inc.css
@@ -50,78 +50,106 @@
 .chat-toolbarbutton {
   -moz-appearance: none;
   border: none;
   padding: 0 3px;
   margin: 0;
   background: none;
 }
 
-.chat-toolbarbutton:hover {
-  background-color: rgba(255,255,255,.35);
-}
-
-.chat-toolbarbutton:hover:active {
-  background-color: rgba(255,255,255,.5);
-}
-
 .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:-moz-any(:hover,:hover:active) {
+.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:-moz-any(:hover,:hover:active) {
+.chat-minimize-button:hover {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize-hover");
+}
+
+:hover,: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:-moz-any(:hover,:hover:active) {
+.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-loop-hangup {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#exit-white");
+  background-color: #d13f1a;
+  border: 1px solid #d13f1a;
+  border-top-right-radius: 4px;
+  width: 32px;
+  height: 26px;
+  margin-top: -6px;
+  margin-bottom: -5px;
+  -moz-margin-start: 6px;
+  -moz-margin-end: -5px;
+}
+
+.chat-toolbarbutton.chat-loop-hangup:-moz-any(:hover,:hover:active) {
+  background-color: #ef6745;
+  border-color: #ef6745;
+}
+
 .chat-title {
-  font-weight: bold;
-  color: black;
+  color: #666;
   text-shadow: none;
   cursor: inherit;
 }
 
 .chat-titlebar {
-  height: 30px;
-  min-height: 30px;
+  height: 26px;
+  min-height: 26px;
   width: 100%;
   margin: 0;
-  padding: 7px 6px;
-  border: none;
-  border-bottom: 1px solid #ccc;
+  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"] {
@@ -130,50 +158,51 @@ chatbar > chatbox > .chat-titlebar > .ch
 
 .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;
 }
 
-chatbox[dark=true] > .chat-titlebar,
-chatbox[dark=true] > .chat-titlebar[selected] {
-  border-bottom: none;
-  background-color: #000;
-  background-image: none;
-}
-
-chatbox[dark=true] > .chat-titlebar > hbox > .chat-title {
-  font-weight: normal;
-  color: #c1c1c1;
-}
-
 .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;
@@ -199,19 +228,29 @@ chatbox[dark=true] > .chat-titlebar > hb
 }
 
 chatbar {
   -moz-margin-end: 20px;
 }
 
 chatbox {
   -moz-margin-start: 4px;
-  background-color: white;
-  border: 1px solid #ccc;
-  border-bottom: none;
+  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. */
+  -moz-margin-end: 5px;
 }
 
 window > chatbox {
   -moz-margin-start: 0px;
   margin: 0px;
   border: none;
   padding: 0px;
+  border-radius: 4px;
 }
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -392,16 +392,41 @@
   background-image: url(chrome://browser/skin/tabbrowser/tab-background-start.png),
                     url(chrome://browser/skin/tabbrowser/tab-background-middle.png),
                     url(chrome://browser/skin/tabbrowser/tab-background-end.png);
   background-position: left bottom, @tabCurveWidth@ bottom, right bottom;
   background-repeat: no-repeat;
   background-size: @tabCurveWidth@ 100%, calc(100% - (2 * @tabCurveWidth@)) 100%, @tabCurveWidth@ 100%;
 }
 
+/* User Context UI - change tab decoration depending on userContextId.
+   Defaults to gray for unknown usercontextids. */
+.tabbrowser-tab[usercontextid] {
+  background-image: linear-gradient(to right, transparent 20%, #909090 30%, #909090 70%, transparent 80%);
+  background-size: auto 2px;
+  background-repeat: no-repeat;
+}
+
+/* Personal User Context */
+.tabbrowser-tab[usercontextid="1"] {
+  background-image: linear-gradient(to right, transparent 20%, #00a7e0 30%, #00a7e0 70%, transparent 80%);
+}
+/* Work User Context */
+.tabbrowser-tab[usercontextid="2"] {
+  background-image: linear-gradient(to right, transparent 20%, #f89c24 30%, #f89c24 70%, transparent 80%);
+}
+/* Banking User Context */
+.tabbrowser-tab[usercontextid="3"] {
+  background-image: linear-gradient(to right, transparent 20%, #7dc14c 30%, #7dc14c 70%, transparent 80%);
+}
+/* Shopping User Context */
+.tabbrowser-tab[usercontextid="4"] {
+  background-image: linear-gradient(to right, transparent 20%, #ee5195 30%, #ee5195 70%, transparent 80%);
+}
+
 /* Tab pointer-events */
 .tabbrowser-tab {
   pointer-events: none;
 }
 
 .tab-background-middle,
 .tabs-newtab-button,
 .tab-icon-overlay[soundplaying],
--- a/browser/themes/shared/usercontext/usercontext.inc.css
+++ b/browser/themes/shared/usercontext/usercontext.inc.css
@@ -10,8 +10,60 @@
 
 #menu_newUserContextTabBanking {
   list-style-image: url("chrome://browser/skin/usercontext/banking.svg");
 }
 
 #menu_newUserContextTabShopping {
   list-style-image: url("chrome://browser/skin/usercontext/shopping.svg");
 }
+
+/* URL Bar Decoration */
+
+#userContext-indicator {
+  height: 16px;
+  width: 16px;
+}
+
+#userContext-label {
+  margin-inline-end: 3px;
+  color: #909090;
+}
+
+#userContext-icons:not([usercontextid]) {
+  display: none;
+}
+
+#userContext-icons {
+  -moz-box-align: center;
+}
+
+/* Personal User Context */
+#userContext-icons[usercontextid="1"] > #userContext-label {
+  color: #00a7e0;
+}
+#userContext-icons[usercontextid="1"] > #userContext-indicator {
+  list-style-image: url("chrome://browser/skin/usercontext/personal.svg");
+}
+
+/* Work User Context */
+#userContext-icons[usercontextid="2"] > #userContext-label {
+  color: #f89c24;
+}
+#userContext-icons[usercontextid="2"] > #userContext-indicator {
+  list-style-image: url("chrome://browser/skin/usercontext/work.svg");
+}
+
+/* Banking User Context */
+#userContext-icons[usercontextid="3"] > #userContext-label {
+  color: #7dc14c;
+}
+#userContext-icons[usercontextid="3"] > #userContext-indicator {
+  list-style-image: url("chrome://browser/skin/usercontext/banking.svg");
+}
+
+/* Shopping User Context */
+#userContext-icons[usercontextid="4"] > #userContext-label {
+  color: #ee5195;
+}
+#userContext-icons[usercontextid="4"] > #userContext-indicator {
+  list-style-image: url("chrome://browser/skin/usercontext/shopping.svg");
+}
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -2607,49 +2607,16 @@ notification[value="loop-sharing-notific
 }
 
 .social-panel-frame {
   border-radius: inherit;
 }
 
 %include ../shared/social/chat.inc.css
 
-.chat-titlebar {
-  background-color: #c4cfde;
-  background-image: linear-gradient(rgba(255,255,255,.5), transparent);
-}
-
-.chat-titlebar[selected] {
-  background-color: #dae3f0;
-}
-
-.chatbar-button {
-  -moz-appearance: none;
-  background-color: #c4cfde;
-  background-image: linear-gradient(rgba(255,255,255,.5), transparent);
-}
-
-.chatbar-button > .toolbarbutton-icon {
-  -moz-margin-end: 0;
-}
-
-.chatbar-button:hover,
-.chatbar-button[open="true"] {
-  background-color: #dae3f0;
-}
-
-.chatbar-button[activity]:not([open="true"]) {
-  background-image: radial-gradient(circle farthest-corner at center 3px, rgb(255,255,255) 3%, rgba(186,221,251,0.75) 40%, rgba(127,179,255,0.5) 80%, rgba(127,179,255,0.25));
-}
-
-chatbox {
-  border-top-left-radius: 2.5px;
-  border-top-right-radius: 2.5px;
-}
-
 /* 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/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -87,16 +87,17 @@ browser.jar:
   skin/classic/browser/toolbarbutton-dropdown-arrow.png
   skin/classic/browser/toolbarbutton-dropdown-arrow-XPVista7.png
   skin/classic/browser/toolbarbutton-dropdown-arrow-inverted.png
   skin/classic/browser/urlbar-popup-blocked.png
   skin/classic/browser/urlbar-history-dropmarker.png
   skin/classic/browser/urlbar-history-dropmarker@2x.png
   skin/classic/browser/urlbar-history-dropmarker-preWin10.png
   skin/classic/browser/urlbar-history-dropmarker-preWin10@2x.png
+  skin/classic/browser/webRTC-indicator.css
   skin/classic/browser/loop/menuPanel.png                      (loop/menuPanel.png)
   skin/classic/browser/loop/menuPanel@2x.png                   (loop/menuPanel@2x.png)
   skin/classic/browser/loop/menuPanel-aero.png                 (loop/menuPanel-aero.png)
   skin/classic/browser/loop/menuPanel-aero@2x.png              (loop/menuPanel-aero@2x.png)
   skin/classic/browser/loop/toolbar.png                        (loop/toolbar.png)
   skin/classic/browser/loop/toolbar@2x.png                     (loop/toolbar@2x.png)
   skin/classic/browser/loop/toolbar-aero.png                   (loop/toolbar-aero.png)
   skin/classic/browser/loop/toolbar-aero@2x.png                (loop/toolbar-aero@2x.png)
copy from browser/themes/shared/webrtc/indicator.css
copy to browser/themes/windows/webRTC-indicator.css
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -224,17 +224,17 @@ Tools.shaderEditor = {
     return new ShaderEditorPanel(iframeWindow, toolbox);
   }
 };
 
 Tools.canvasDebugger = {
   id: "canvasdebugger",
   ordinal: 6,
   visibilityswitch: "devtools.canvasdebugger.enabled",
-  icon: "chrome://devtools/skin/themes/images/tool-styleeditor.svg",
+  icon: "chrome://devtools/skin/themes/images/tool-canvas.svg",
   invertIconForLightTheme: true,
   url: "chrome://devtools/content/canvasdebugger/canvasdebugger.xul",
   label: l10n("ToolboxCanvasDebugger.label", canvasDebuggerStrings),
   panelLabel: l10n("ToolboxCanvasDebugger.panelLabel", canvasDebuggerStrings),
   tooltip: l10n("ToolboxCanvasDebugger.tooltip", canvasDebuggerStrings),
 
   // Hide the Canvas Debugger in the Add-on Debugger and Browser Toolbox
   // (bug 1047520).
--- a/devtools/client/framework/sidebar.js
+++ b/devtools/client/framework/sidebar.js
@@ -220,16 +220,17 @@ ToolSidebar.prototype = {
     iframe.setAttribute("src", url);
     iframe.tooltip = "aHTMLTooltip";
 
     // Creating the tab and adding it to the tabbox
     let tab = this._panelDoc.createElementNS(XULNS, "tab");
     this._tabbox.tabs.appendChild(tab);
     tab.setAttribute("label", ""); // Avoid showing "undefined" while the tab is loading
     tab.setAttribute("id", this.TAB_ID_PREFIX + id);
+    tab.setAttribute("crop", "end");
 
     // Add the tab to the allTabs menu if exists
     let allTabsItem = this._addItemToAllTabsMenu(id, tab, selected);
 
     let onIFrameLoaded = (event) => {
       let doc = event.target;
       let win = doc.defaultView;
       tab.setAttribute("label", doc.title);
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -293,16 +293,17 @@ devtools.jar:
     skin/themes/images/dock-side@2x.png (themes/images/dock-side@2x.png)
 *   skin/themes/floating-scrollbars.css (themes/floating-scrollbars.css)
     skin/themes/floating-scrollbars-light.css (themes/floating-scrollbars-light.css)
 *   skin/themes/inspector.css (themes/inspector.css)
     skin/themes/images/profiler-stopwatch.svg (themes/images/profiler-stopwatch.svg)
     skin/themes/images/profiler-stopwatch-checked.svg (themes/images/profiler-stopwatch-checked.svg)
     skin/themes/images/tool-options.svg (themes/images/tool-options.svg)
     skin/themes/images/tool-webconsole.svg (themes/images/tool-webconsole.svg)
+    skin/themes/images/tool-canvas.svg (themes/images/tool-canvas.svg)
     skin/themes/images/tool-debugger.svg (themes/images/tool-debugger.svg)
     skin/themes/images/tool-debugger-paused.svg (themes/images/tool-debugger-paused.svg)
     skin/themes/images/tool-inspector.svg (themes/images/tool-inspector.svg)
     skin/themes/images/tool-shadereditor.svg (themes/images/tool-shadereditor.svg)
     skin/themes/images/tool-styleeditor.svg (themes/images/tool-styleeditor.svg)
     skin/themes/images/tool-storage.svg (themes/images/tool-storage.svg)
     skin/themes/images/tool-profiler.svg (themes/images/tool-profiler.svg)
     skin/themes/images/tool-profiler-active.svg (themes/images/tool-profiler-active.svg)
--- a/devtools/client/markupview/markup-view.js
+++ b/devtools/client/markupview/markup-view.js
@@ -1924,17 +1924,17 @@ MarkupContainer.prototype = {
       this.isDragging = true;
 
       this._dragStartY = event.pageY;
       this.markup.indicateDropTarget(this.elt);
 
       // If this is the last child, use the closing <div.tag-line> of parent as indicator
       this.markup.indicateDragTarget(this.elt.nextElementSibling ||
                                      this.markup.getContainer(this.node.parentNode()).closeTagLine);
-    }, this.GRAB_DELAY);
+    }, this.markup.GRAB_DELAY);
   },
 
   /**
    * On mouse up, stop dragging.
    */
   _onMouseUp: Task.async(function*() {
     this._isMouseDown = false;
 
--- a/devtools/client/markupview/test/browser_markupview_dragdrop_isDragging.js
+++ b/devtools/client/markupview/test/browser_markupview_dragdrop_isDragging.js
@@ -22,16 +22,20 @@ add_task(function*() {
     pageX: rect.x,
     pageY: rect.y,
     stopPropagation: function() {},
     preventDefault: function() {}
   });
 
   ok(!el.isDragging, "isDragging should not be set to true immediately");
 
+  info("Waiting for 10ms");
+  yield wait(10);
+  ok(!el.isDragging, "isDragging should not be set to true after a brief wait");
+
   info("Waiting " + (GRAB_DELAY + 1) + "ms");
   yield wait(GRAB_DELAY + 1);
   ok(el.isDragging, "isDragging true after GRAB_DELAY has passed");
 
   let dropCompleted = once(inspector.markup, "drop-completed");
 
   info("Simulating mouseUp on #test");
   el._onMouseUp({
--- a/devtools/client/netmonitor/netmonitor.xul
+++ b/devtools/client/netmonitor/netmonitor.xul
@@ -314,28 +314,35 @@
                        data-key="body"/>
             </vbox>
           </vbox>
           <tabbox id="event-details-pane"
                   class="devtools-sidebar-tabs"
                   handleCtrlTab="false">
             <tabs>
               <tab id="headers-tab"
+                   crop="end"
                    label="&netmonitorUI.tab.headers;"/>
               <tab id="cookies-tab"
+                   crop="end"
                    label="&netmonitorUI.tab.cookies;"/>
               <tab id="params-tab"
+                   crop="end"
                    label="&netmonitorUI.tab.params;"/>
               <tab id="response-tab"
+                   crop="end"
                    label="&netmonitorUI.tab.response;"/>
               <tab id="timings-tab"
+                   crop="end"
                    label="&netmonitorUI.tab.timings;"/>
               <tab id="security-tab"
+                   crop="end"
                    label="&netmonitorUI.tab.security;"/>
               <tab id="preview-tab"
+                   crop="end"
                    label="&netmonitorUI.tab.preview;"/>
             </tabs>
             <tabpanels flex="1">
               <tabpanel id="headers-tabpanel"
                         class="tabpanel-content">
                 <vbox flex="1">
                   <hbox id="headers-summary-url"
                         class="tabpanel-summary-container"
--- a/devtools/client/shared/doorhanger.js
+++ b/devtools/client/shared/doorhanger.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { Ci, Cc } = require("chrome");
 const { Services } = require("resource://gre/modules/Services.jsm");
 const { DOMHelpers } = require("resource:///modules/devtools/client/shared/DOMHelpers.jsm");
 const { Task } = require("resource://gre/modules/Task.jsm");
-const { Promise } = require("promise");
+const { Promise } = require("resource://gre/modules/Promise.jsm");
 const { setTimeout } = require("sdk/timers");
 const { getMostRecentBrowserWindow } = require("sdk/window/utils");
 
 const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const DEV_EDITION_PROMO_URL = "chrome://devtools/content/framework/dev-edition-promo/dev-edition-promo.xul";
 const DEV_EDITION_PROMO_ENABLED_PREF = "devtools.devedition.promo.enabled";
 const DEV_EDITION_PROMO_SHOWN_PREF = "devtools.devedition.promo.shown";
 const DEV_EDITION_PROMO_URL_PREF = "devtools.devedition.promo.url";
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/tool-canvas.svg
@@ -0,0 +1,8 @@
+<!-- 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 height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="whitesmoke">
+    <path opacity="0.2" d="M6.9 7.3h26l-.1 25.9-25.9-.1z"/>
+    <path opacity="0.5" d="M13 8.7H9.1c-.2 0-.4.2-.4.4V13c0 .2.2.4.4.4H13c.2 0 .4-.2.4-.4V9.1c0-.2-.2-.4-.4-.4zM13 17.9H9.1c-.2 0-.4.2-.4.4v3.9c0 .2.2.4.4.4H13c.2 0 .4-.2.4-.4v-3.9c0-.2-.2-.4-.4-.4zM13 27.1H9.1c-.2 0-.4.2-.4.4v3.9c0 .2.2.4.4.4H13c.2 0 .4-.2.4-.4v-3.9c0-.3-.2-.4-.4-.4zM17.6 13.3h-3.9c-.2 0-.4.2-.4.4v3.9c0 .2.2.4.4.4h3.9c.2 0 .4-.2.4-.4v-3.9c0-.2-.2-.4-.4-.4zM17.6 22.5h-3.9c-.2 0-.4.2-.4.4v3.9c0 .2.2.4.4.4h3.9c.2 0 .4-.2.4-.4v-3.9c0-.3-.2-.4-.4-.4zM22.2 8.7h-3.9c-.2 0-.4.2-.4.4V13c0 .2.2.4.4.4h3.9c.2 0 .4-.2.4-.4V9.1c-.1-.2-.2-.4-.4-.4zM22.2 17.9h-3.9c-.2 0-.4.2-.4.4v3.9c0 .2.2.4.4.4h3.9c.2 0 .4-.2.4-.4v-3.9c-.1-.2-.2-.4-.4-.4zM22.2 27.1h-3.9c-.2 0-.4.2-.4.4v3.9c0 .2.2.4.4.4h3.9c.2 0 .4-.2.4-.4v-3.9c0-.3-.2-.4-.4-.4zM26.8 13.3h-3.9c-.2 0-.4.2-.4.4v3.9c0 .2.2.4.4.4h3.9c.2 0 .4-.2.4-.4v-3.9c-.1-.2-.2-.4-.4-.4zM26.8 22.5h-3.9c-.2 0-.4.2-.4.4v3.9c0 .2.2.4.4.4h3.9c.2 0 .4-.2.4-.4v-3.9c-.1-.3-.2-.4-.4-.4zM31.4 8.7h-3.9c-.2 0-.4.2-.4.4V13c0 .2.2.4.4.4h3.9c.2 0 .4-.2.4-.4V9.1c-.1-.2-.2-.4-.4-.4zM31.4 17.9h-3.9c-.2 0-.4.2-.4.4v3.9c0 .2.2.4.4.4h3.9c.2 0 .4-.2.4-.4v-3.9c-.1-.2-.2-.4-.4-.4zM31.4 27.1h-3.9c-.2 0-.4.2-.4.4v3.9c0 .2.2.4.4.4h3.9c.2 0 .4-.2.4-.4v-3.9c-.1-.3-.2-.4-.4-.4z"/>
+    <path d="M34.1 5.3H5.9c-.4 0-.6.3-.6.6V34c0 .4.3.6.6.6h28.2c.4 0 .6-.3.6-.6V5.9c0-.3-.3-.6-.6-.6zm-2.9 25.9H9.3L9.2 9h22.1v22.2z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/images/tool-debugger-paused.svg
+++ b/devtools/client/themes/images/tool-debugger-paused.svg
@@ -1,6 +1,7 @@
 <!-- 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 height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
-  <path fill="#71c054" fill-rule="evenodd" d="m8,1c-3.9,0-7,3.1-7,7 0,3.9 3.1,7 7,7 3.9,0 7-3.1 7-7 0-3.9-3.1-7-7-7zm2,11h-1-5c-.6,0-1-.4-1-1v-6c0-.6 .4-1 1-1h5 1l4,4-4,4z"/>
-</svg>
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="green">
+    <path d="M20 4.1c-8.8 0-15.9 7-15.9 15.9s7 15.9 15.9 15.9 15.9-7 15.9-15.9S28.8 4.1 20 4.1zm0 27.2c-6.3 0-11.3-5-11.3-11.3S13.7 8.7 20 8.7s11.3 5 11.3 11.3-5 11.3-11.3 11.3z"/>
+    <path d="M27.8 19.1l-3.5-3.5c-.3-.3-.6-.4-.9-.4h-9.8c-.3 0-.6.3-.6.6l.1 8.3c0 .3.3.6.6.6h10.1l.6-.3 3.5-3.5c.4-.6.4-1.3-.1-1.8z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/images/tool-debugger.svg
+++ b/devtools/client/themes/images/tool-debugger.svg
@@ -1,10 +1,7 @@
 <!-- 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 width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
-  <g fill="#edf0f1" fill-rule="evenodd">
-    <path d="m8,1c-3.9,0-7,3.1-7,7 0,3.9 3.1,7 7,7 3.9,0 7-3.1 7-7 0-3.9-3.1-7-7-7zm0,12c-2.8,0-5-2.2-5-5 0-2.8 2.2-5 5-5 2.8,0 5,2.2 5,5 0,2.8-2.2,5-5,5z"/>
-    <path d="m6,5c.6,0 1,.4 1,1v4c0,.6-.4,1-1,1-.6,0-1-.4-1-1v-4c0-.6 .4-1 1-1z"/>
-    <path d="m10,5c.6,0 1,.4 1,1v4c0,.6-.4,1-1,1-.6,0-1-.4-1-1v-4c0-.6 .4-1 1-1z"/>
-  </g>
-</svg>
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="whitesmoke">
+    <path d="M20 4.1c-8.8 0-15.9 7-15.9 15.9s7 15.9 15.9 15.9 15.9-7 15.9-15.9S28.8 4.1 20 4.1zm0 27.2c-6.3 0-11.3-5-11.3-11.3S13.7 8.7 20 8.7s11.3 5 11.3 11.3-5 11.3-11.3 11.3z"/>
+    <path d="M15.5 13.2c1.4 0 2.3.9 2.3 2.3v9.1c0 1.4-.9 2.3-2.3 2.3s-2.3-.9-2.3-2.3v-9.1c0-1.4.9-2.3 2.3-2.3zM24.5 13.2c1.4 0 2.3.9 2.3 2.3v9.1c0 1.4-.9 2.3-2.3 2.3s-2.3-.9-2.3-2.3v-9.1c.1-1.4 1-2.3 2.3-2.3z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/images/tool-inspector.svg
+++ b/devtools/client/themes/images/tool-inspector.svg
@@ -1,12 +1,6 @@
 <!-- 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 width="16" xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 16 16">
-  <path fill="#eef0f2" fill-rule="evenodd" d="M2,4v9h11V4H2z M11,11H4V6h7V11z"/>
-  <g opacity=".8">
-    <path opacity=".8" fill="#eef0f2" fill-rule="evenodd" d="M0,8h2v1H0V8z"/>
-    <path opacity=".8" fill="#eef0f2" fill-rule="evenodd" d="M13,8h2v1h-2V8z"/>
-    <path opacity=".8" fill="#eef0f2" fill-rule="evenodd" d="M7,2h1v2H7V2z"/>
-    <path opacity=".8" fill="#eef0f2" fill-rule="evenodd" d="M7,13h1v2H7V13z"/>
-  </g>
-</svg>
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="whitesmoke">
+    <path d="M36.1 18h-2V9.6c0-1.5-1.2-2.6-2.6-2.6h-9V5c0-1.1-.9-2.1-2.1-2.1-1.1 0-2.1.9-2.1 2.1v2H8.5C7 6.9 5.9 8.1 5.9 9.6V18h-2c-1.1 0-2.1.9-2.1 2.1 0 1.1.9 2.1 2.1 2.1h2v8.4c0 1.5 1.2 2.6 2.6 2.6h9.8v2c0 1.1.9 2 2.1 2 1.1 0 2.1-.9 2.1-2v-2h9c1.5 0 2.6-1.2 2.6-2.6v-8.4h2c1.1 0 2.1-.9 2.1-2.1s-1-2.1-2.1-2.1zm-6.6 10.4H10.9v-17h18.6v17z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/images/tool-network.svg
+++ b/devtools/client/themes/images/tool-network.svg
@@ -1,39 +1,9 @@
 <!-- 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 width="17" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17 16">
-  <g fill="#edf0f1" fill-rule="evenodd">
-    <path opacity=".1" d="M2.1,0h12.8C16,0,17,1,17,2.1v10.6c0,1.2-1,2.1-2.1,2.1H2.1c-1.2,0-2.1-1-2.1-2.1V2.1C0,1,1,0,2.1,0z"/>
-    <path d="m2.1,2.1h9.6c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-9.6c-.6,0-1.1-.5-1.1-1.1 .1-.6 .5-1.1 1.1-1.1z"/>
-  </g>
-  <g opacity=".7">
-    <g opacity=".75">
-      <path fill="#edf0f1" fill-rule="evenodd" d="m7.4,5.3h7.4c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-7.4c-.5-.1-1-.5-1-1.1 0-.6 .5-1.1 1-1.1z"/>
-    </g>
-    <g opacity=".85">
-      <path fill="#edf0f1" d="m14.9,5.7c.4,0 .7,.3 .7,.7s-.4,.6-.7,.6h-7.5c-.3,0-.6-.3-.6-.6s.3-.7 .7-.7h7.4m0-.4h-7.5c-.6,0-1.1,.5-1.1,1.1 0,.6 .5,1.1 1.1,1.1h7.4c.6,0 1.1-.5 1.1-1.1 0-.6-.4-1.1-1-1.1z"/>
-    </g>
-  </g>
-  <g opacity=".75">
-    <path fill="#edf0f1" fill-rule="evenodd" d="m5.3,8.5h3.2c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-3.2c-.6,0-1.1-.5-1.1-1.1 .1-.6 .5-1.1 1.1-1.1z"/>
-  </g>
-  <g opacity=".85">
-    <path fill="#edf0f1" d="m8.5,8.9c.4,0 .7,.3 .7,.7 0,.4-.3,.7-.7,.7h-3.2c-.4,0-.7-.3-.7-.7 0-.4 .3-.7 .7-.7h3.2m0-.4h-3.2c-.6,0-1.1,.5-1.1,1.1 0,.6 .5,1.1 1.1,1.1h3.2c.6,0 1.1-.5 1.1-1.1 0-.6-.5-1.1-1.1-1.1z"/>
-  </g>
-  <g opacity=".7">
-    <g opacity=".75">
-      <path fill="#edf0f1" fill-rule="evenodd" d="m4.3,11.7h2.1c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-2.1c-.6,0-1.1-.5-1.1-1.1 0-.6 .5-1.1 1.1-1.1z"/>
-    </g>
-    <g opacity=".85">
-      <path fill="#edf0f1" d="m6.4,12.1c.4,0 .7,.3 .7,.7 0,.4-.3,.7-.7,.7h-2.1c-.4,0-.7-.3-.7-.7 0-.4 .3-.7 .7-.7h2.1m0-.4h-2.1c-.6,0-1.1,.5-1.1,1.1 0,.6 .5,1.1 1.1,1.1h2.1c.6,0 1.1-.5 1.1-1.1-.1-.6-.5-1.1-1.1-1.1z"/>
-    </g>
-  </g>
-  <g opacity=".05" fill="#edf0f1" fill-rule="evenodd">
-    <path d="m7.4,14.3v-13.8c0-.3-.2-.5-.5-.5-.3,0-.5,.2-.5,.5v13.8c0,.3 .2,.5 .5,.5 .3,.1 .5-.2 .5-.5z"/>
-    <path d="m4.2,14.3v-13.8c0-.3-.2-.5-.5-.5-.3,0-.5,.2-.5,.5v13.8c0,.3 .2,.5 .5,.5 .3,.1 .5-.2 .5-.5z"/>
-    <path d="m1,14.6c0-.1 0-.1 0-.2v-13.9c0-.1 0-.1 0-.2-.6,.4-1,1-1,1.8v10.6c0,.8 .4,1.5 1,1.9z"/>
-    <path d="m16,.3c0,.1 0,.1 0,.2v13.8c0,.1 0,.1 0,.2 .6-.4 1-1 1-1.8v-10.6c0-.7-.4-1.4-1-1.8z"/>
-    <path d="m13.8,14.3v-13.8c0-.3-.2-.5-.5-.5-.3,0-.5,.2-.5,.5v13.8c0,.3 .2,.5 .5,.5 .3,.1 .5-.2 .5-.5z"/>
-    <path d="m10.6,14.3v-13.8c0-.3-.2-.5-.5-.5-.3,0-.5,.2-.5,.5v13.8c0,.3 .2,.5 .5,.5 .3,.1 .5-.2 .5-.5z"/>
-  </g>
-</svg>
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="whitesmoke">
+    <path d="M28.2 10.6H4.6c-1 0-1.9-.8-1.9-1.9s.8-1.9 1.9-1.9h23.6c1 0 1.9.8 1.9 1.9s-.8 1.9-1.9 1.9z"/>
+    <path opacity="0.75" d="M35.4 18.1H16.7c-1 0-1.9-.8-1.9-1.9s.8-1.9 1.9-1.9h18.7c1 0 1.9.8 1.9 1.9s-.9 1.9-1.9 1.9z"/>
+    <path d="M21 25.6h-8.7c-1 0-1.9-.8-1.9-1.9s.8-1.9 1.9-1.9H21c1 0 1.9.8 1.9 1.9s-.9 1.9-1.9 1.9z"/>
+    <path opacity="0.75" d="M12.3 33.2H6.4c-1 0-1.9-.8-1.9-1.9 0-1 .8-1.9 1.9-1.9h5.9c1 0 1.9.8 1.9 1.9 0 1.1-.8 1.9-1.9 1.9z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/images/tool-profiler-active.svg
+++ b/devtools/client/themes/images/tool-profiler-active.svg
@@ -1,17 +1,8 @@
 <!-- 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" width="16" height="16" viewBox="0 0 16 16">
-  <g fill="#71c054" fill-rule="evenodd">
-    <path d="m8,1c-3.9,0-7,3.1-7,7s3.1,7 7,7c3.9,0 7-3.1 7-7s-3.1-7-7-7zm-.1,12c-2.8,0-5-2.2-5-5 0-2.8 2.2-5 5-5s5,2.2 5,5c0,2.8-2.2,5-5,5z"/>
-    <path d="m8,6.9c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1-.6,0-1.1-.5-1.1-1.1 0-.6 .5-1.1 1.1-1.1z"/>
-    <path d="m11.3,4.6l-3.9,2.5 1.5,1.4 2.4-3.9z"/>
-    <path opacity=".4" d="m4.6,10c.7,1.2 2,2 3.4,2 1.5,0 2.7-.8 3.4-2h-6.8z"/>
-    <g opacity=".3">
-      <path d="m7.1,5.1l-.6-1.3-.9,.4 .7,1.3c.2-.1 .5-.3 .8-.4z"/>
-      <path d="m9.8,5.6l.7-1.4-.9-.4-.7,1.3c.3,.2 .6,.3 .9,.5z"/>
-      <path d="m10.8,7c.1,.3 .2,.7 .2,1h2v-1h-2.2z"/>
-      <path d="m5,8c0-.3 .1-.7 .2-1h-2.2l-.1,1h2.1z"/>
-    </g>
-  </g>
-</svg>
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="#71c054">
+    <path d="M17.6 18.4c-.7.2-1.3.9-1.5 1.6-.2 1.2.6 2.3 1.7 2.5.8.1 1.5-.2 2-.7l6-6.9-8.2 3.5z"/>
+    <path d="M12.3 24.5c1.6 2.7 4.5 4.5 7.7 4.5 3.4 0 6.1-1.8 7.7-4.5H12.3z"/>
+    <path d="M20 4.3c-8.8 0-15.9 7-15.9 15.7 0 8.8 7 15.7 15.9 15.7 8.8 0 15.9-7 15.9-15.7 0-8.8-7.1-15.7-15.9-15.7zm-.2 26.9c-6 0-10.7-4.4-11.3-10.1H9.9c1.4 0 2.3-.9 2.3-2.2 0-1.3-.9-2.2-2.3-2.2h-1c.6-2.1 1.9-3.9 3.5-5.3l.5.8c.6 1.2 1.9 1.6 3.1.9s1.6-1.8.9-3l-.5-.9c1-.3 2.1-.5 3.3-.5 1.3 0 2.5.2 3.7.6l-.4 1c-.6 1.2-.3 2.4.9 3s2.4.3 3.1-.9l.4-.7c1.5 1.3 2.6 3 3.2 5h-.9c-1.4 0-2.3.9-2.3 2.2 0 1.3.9 2.2 2.3 2.2H31.1c-.6 5.8-5.4 10.1-11.3 10.1z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/images/tool-profiler.svg
+++ b/devtools/client/themes/images/tool-profiler.svg
@@ -1,17 +1,8 @@
 <!-- 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" width="16" height="16" viewBox="0 0 16 16">
-  <g fill="#edf0f1" fill-rule="evenodd">
-    <path d="m8,1c-3.9,0-7,3.1-7,7s3.1,7 7,7c3.9,0 7-3.1 7-7s-3.1-7-7-7zm-.1,12c-2.8,0-5-2.2-5-5 0-2.8 2.2-5 5-5s5,2.2 5,5c0,2.8-2.2,5-5,5z"/>
-    <path d="m8,6.9c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1-.6,0-1.1-.5-1.1-1.1 0-.6 .5-1.1 1.1-1.1z"/>
-    <path d="m11.3,4.6l-3.9,2.5 1.5,1.4 2.4-3.9z"/>
-    <path opacity=".4" d="m4.6,10c.7,1.2 2,2 3.4,2 1.5,0 2.7-.8 3.4-2h-6.8z"/>
-    <g opacity=".3">
-      <path d="m7.1,5.1l-.6-1.3-.9,.4 .7,1.3c.2-.1 .5-.3 .8-.4z"/>
-      <path d="m9.8,5.6l.7-1.4-.9-.4-.7,1.3c.3,.2 .6,.3 .9,.5z"/>
-      <path d="m10.8,7c.1,.3 .2,.7 .2,1h2v-1h-2.2z"/>
-      <path d="m5,8c0-.3 .1-.7 .2-1h-2.2l-.1,1h2.1z"/>
-    </g>
-  </g>
-</svg>
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="whitesmoke">
+    <path d="M17.6 18.4c-.7.2-1.3.9-1.5 1.6-.2 1.2.6 2.3 1.7 2.5.8.1 1.5-.2 2-.7l6-6.9-8.2 3.5z"/>
+    <path opacity="0.5" d="M12.3 24.5c1.6 2.7 4.5 4.5 7.7 4.5 3.4 0 6.1-1.8 7.7-4.5H12.3z"/>
+    <path d="M20 4.3c-8.8 0-15.9 7-15.9 15.7 0 8.8 7 15.7 15.9 15.7 8.8 0 15.9-7 15.9-15.7 0-8.8-7.1-15.7-15.9-15.7zm-.2 26.9c-6 0-10.7-4.4-11.3-10.1H9.9c1.4 0 2.3-.9 2.3-2.2 0-1.3-.9-2.2-2.3-2.2h-1c.6-2.1 1.9-3.9 3.5-5.3l.5.8c.6 1.2 1.9 1.6 3.1.9s1.6-1.8.9-3l-.5-.9c1-.3 2.1-.5 3.3-.5 1.3 0 2.5.2 3.7.6l-.4 1c-.6 1.2-.3 2.4.9 3s2.4.3 3.1-.9l.4-.7c1.5 1.3 2.6 3 3.2 5h-.9c-1.4 0-2.3.9-2.3 2.2 0 1.3.9 2.2 2.3 2.2H31.1c-.6 5.8-5.4 10.1-11.3 10.1z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/images/tool-scratchpad.svg
+++ b/devtools/client/themes/images/tool-scratchpad.svg
@@ -1,9 +1,6 @@
 <!-- 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 height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
-  <g fill="#edf0f1" fill-rule="evenodd">
-    <path opacity=".3" d="m1.1,6.3c0-.3 .3-.6 .6-.6h4.6c.3,0 .6,.3 .6,.6s-.3,.6-.6,.6h-4.6c-.3,0-.6-.3-.6-.6zm12,1.7h-10.2c-.4,0-.6,.3-.6,.6 0,.3 .3,.6 .6,.6h10.3c.3,0 .6-.3 .6-.6-.1-.3-.3-.6-.7-.6zm-5.7,3.4c.3,0 .6-.3 .6-.6 0-.3-.3-.6-.6-.6h-4.5c-.3,0-.6,.3-.6,.6 0,.3 .3,.6 .6,.6h4.5zm2.3,1.2h-8c-.3,0-.6,.3-.6,.6 0,.3 .3,.6 .6,.6h8c.3,0 .6-.3 .6-.6 0-.4-.3-.6-.6-.6z"/>
-    <path d="m14.3,2.3h-.6v1.1c0,.6-.5,1.1-1.1,1.1-.6,0-1.1-.5-1.1-1.1v-.5c0,.3 .3,.6 .6,.6 .3,0 .6-.3 .6-.6v-.6-1.7c0-.1 0-.2-.1-.3-.1-.1-.1-.1-.2-.2-.2-.1-.3-.1-.4-.1-.3,0-.6,.3-.6,.6v1.7h-1.1v1.1c0,.6-.5,1.1-1.1,1.1-.7,.1-1.2-.4-1.2-1.1v-.5c0,.3 .3,.6 .6,.6 .3,0 .6-.3 .6-.6v-.6-1.7c0-.1 0-.2-.1-.3-.1-.1-.2-.2-.3-.2-.1-.1-.1-.1-.2-.1-.3,0-.6,.3-.6,.6v1.7h-1.1v1.1c0,.6-.5,1.1-1.1,1.1-.6,0-1.1-.5-1.1-1.1v-.5c0,.3 .3,.6 .6,.6 .3,0 .6-.3 .6-.6v-.6-1.7c0-.1 0-.2-.1-.3-.3-.1-.3-.2-.4-.2-.1-.1-.2-.1-.3-.1-.3,0-.5,.3-.5,.6v1.7h-1.2v1.1c0,.6-.5,1.1-1.1,1.1-.6,0-1.1-.5-1.1-1.1v-.5c0,.3 .3,.6 .6,.6 .3,0 .6-.3 .6-.6v-.6-1.7c0-.1 0-.2-.1-.3-.2-.1-.2-.2-.3-.2-.1-.1-.2-.1-.3-.1-.3,0-.6,.3-.6,.6v1.7h-.5c-.3,0-.6,.2-.6,.6v12.6c0,.2 .3,.5 .6,.5h13.7c.3,0 .6-.3 .6-.6v-12.5c0-.4-.3-.6-.6-.6zm-12.6,3.4h4.6c.3,0 .6,.3 .6,.6s-.3,.6-.6,.6h-4.6c-.3,0-.6-.3-.6-.6s.3-.6 .6-.6zm8,8h-8c-.3,0-.6-.3-.6-.6 0-.3 .3-.6 .6-.6h8c.3,0 .6,.3 .6,.6 0,.4-.3,.6-.6,.6zm-7.4-2.8c0-.3 .3-.6 .6-.6h4.6c.3,0 .6,.3 .6,.6 0,.3-.3,.6-.6,.6h-4.6c-.4-.1-.6-.3-.6-.6zm10.8-1.8h-10.2c-.3,0-.6-.3-.6-.6 0-.2 .2-.5 .6-.5h10.3c.3,0 .6,.3 .6,.6-.1,.3-.3,.5-.7,.5z"/>
-  </g>
-</svg>
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="whitesmoke">
+    <path d="M33.9 7.3h-1.2v2.2c0 1.2-1 2.2-2.2 2.2-1.2 0-2.2-1-2.2-2.2v-.1c0 .6.6 1.1 1.2 1.1.7 0 1.2-.5 1.2-1.1v-5c0-.6-.6-1.1-1.2-1.1-.7 0-1.2.5-1.2 1.1v2.9h-5.4v2.2c0 1.2-1 2.2-2.2 2.2-1.4.2-2.4-.8-2.4-2.2v-.1c0 .6.6 1.1 1.2 1.1.7 0 1.2-.5 1.2-1.1v-5c0-.6-.6-1.1-1.2-1.1-.7 0-1.2.5-1.2 1.1v2.9H13v2.2c0 1.2-1 2.2-2.2 2.2-1.2 0-2.2-1-2.2-2.1.1.5.6.9 1.2.9.7 0 1.2-.5 1.2-1.1v-5c0-.6-.6-1.1-1.2-1.1s-1.3.5-1.3 1.1v2.9H6.1c-.6 0-1.2.4-1.2 1.2v25.6c0 .4.6 1 1.2 1H34c.6 0 1.2-.6 1.2-1.2V8.5c0-.8-.7-1.2-1.3-1.2zm-25 8.8h15.2c.7 0 1.2.6 1.2 1.2 0 .7-.6 1.2-1.2 1.2H8.9c-.7 0-1.2-.6-1.2-1.2s.5-1.2 1.2-1.2zm16.6 14.6H9.1c-.7 0-1.2-.6-1.2-1.2 0-.7.6-1.2 1.2-1.2h16.4c.7 0 1.2.6 1.2 1.2 0 .7-.5 1.2-1.2 1.2zm5.6-6H11.5c-.7 0-1.2-.6-1.2-1.2 0-.7.6-1.2 1.2-1.2h19.6c.7 0 1.2.6 1.2 1.2s-.5 1.2-1.2 1.2z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/images/tool-shadereditor.svg
+++ b/devtools/client/themes/images/tool-shadereditor.svg
@@ -1,26 +1,9 @@
 <!-- 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" width="16" height="16" viewBox="0 0 16 16">
-  <rect x="2" y="2" width="12" height="12" fill="#edf0f1" fill-opacity=".1"/>
-  <polygon points="2,2 14,14 2,14" fill="#edf0f1" fill-opacity=".35"/>
-  <rect x="2.5" y="2.5" width="11" height="11" fill="none" stroke="#edf0f1" stroke-width="1"/>
-  <g fill="#edf0f1" fill-opacity=".65">
-    <polygon points="3,3 5,3 5,5"/>
-    <rect x="11" y="3" width="2" height="2"/>
-    <rect x="7" y="3" width="2" height="2"/>
-    <polygon points="5,5 7,5 7,7"/>
-    <rect x="9" y="5" width="2" height="2"/>
-    <polygon points="7,7 9,7 9,9"/>
-    <rect x="11" y="7" width="2" height="2"/>
-    <polygon points="9,9 11,9 11,11"/>
-    <polygon points="11,11 13,11 13,13"/>
-  </g>
-  <line x1="3" y1="3" x2="13" y2="13" stroke="#edf0f1" stroke-width="1"/>
-  <g fill="#edf0f1">
-    <circle cx="2" cy="2" r="1"/>
-    <circle cx="14" cy="2" r="1"/>
-    <circle cx="2" cy="14" r="1"/>
-    <circle cx="14" cy="14" r="1"/>
-  </g>
-</svg>
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="whitesmoke">
+    <path opacity="0.2" d="M7.3 7.7l25.1 25.1-25.1-.1z"/>
+    <path d="M27.8 25.7l-13-13c-.7-.7-1.8-.7-2.5 0s-.7 1.8 0 2.5l12.9 12.9c.3.3.8.5 1.2.5.5 0 .9-.2 1.2-.5.9-.6.9-1.7.2-2.4z"/>
+    <path opacity="0.5" d="M27.5 12.5h-3.8c-.2 0-.3.2-.3.3v3.8c0 .2.2.3.3.3h3.8c.2 0 .3-.2.3-.3v-3.8c.1-.2-.1-.3-.3-.3zm.2 8.9h-3.8c-.1 0-.2.1-.3.1l4.3 4.3c.1-.1.1-.2.1-.3v-3.8c0-.2-.1-.3-.3-.3zm-4.5-4.6h-3.8c-.1 0-.2.1-.3.1l4.3 4.3c.1-.1.1-.2.1-.3v-3.8c0-.1-.1-.3-.3-.3zm-4.5-4.4h-3.8c-.1 0-.2.1-.3.1l4.3 4.3c.1-.1.1-.2.1-.3v-3.8c0-.1-.1-.3-.3-.3z"/>
+    <path d="M34.2 31.8V8.1c1.3 0 2.4-1.1 2.4-2.4s-1.1-2.4-2.4-2.4c-1.3 0-2.4 1.1-2.4 2.4H8.2c0-1.3-1.1-2.4-2.4-2.4-1.3 0-2.4 1.1-2.4 2.4 0 1.2.9 2.2 2 2.3v23.8c-1.1.2-2 1.2-2 2.3 0 1.3 1.1 2.4 2.4 2.4 1.2 0 2.2-.9 2.3-2h23.8c.2 1.1 1.2 2 2.3 2 1.3 0 2.4-1.1 2.4-2.4 0-1.2-1.1-2.3-2.4-2.3zm-4-1.2H9.5V9.8h20.7v20.8z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/images/tool-storage.svg
+++ b/devtools/client/themes/images/tool-storage.svg
@@ -1,10 +1,8 @@
 <!-- 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 width="16" xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 16 16">
-  <g fill="#edf0f1">
-    <path d="m1.3,12.5v-2.4c0,0 0,2.5 6.7,2.5 6.7,0 6.7-2.5 6.7-2.5v2.4c0,0 0,2.7-6.8,2.7-6.6,0-6.6-2.7-6.6-2.7z"/>
-    <path d="m14.7,3.4c0-1.4-3-2.5-6.7-2.5s-6.7,1.1-6.7,2.5c0,.2 0,.3 .1,.5-.1-.3-.1-.4-.1-.4v1.5c0,0 0,2.7 6.7,2.7 6.7,0 6.8-2.7 6.8-2.7v-1.6c0,.1 0,.2-.1,.5-0-.2-0-.3-0-.5z"/>
-    <path d="m1.3,8.7v-2.4c0,0 0,2.5 6.7,2.5 6.7,0 6.7-2.5 6.7-2.5v2.4c0,0 0,2.7-6.8,2.7-6.6-0-6.6-2.7-6.6-2.7z"/>
-  </g>
-</svg>
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="whitesmoke">
+    <path d="M19.8 17.2c-14.1 0-13.5-4.4-13.5-5.3v-2c0-4 8-5 13.5-5 5.6 0 13.8 1.1 13.8 5v2c0 .9.3 5.3-13.8 5.3z"/>
+    <path d="M19.8 21.6c-14.4 0-13.5-4.9-13.5-4.9V21c0 .9-.6 5.5 13.3 5.5 14.3 0 14-4.6 14-5.5v-4c.1 0 .8 4.6-13.8 4.6z"/>
+    <path d="M19.8 30.4c-14.4 0-13.5-4.9-13.5-4.9v4.2c0 .9-.6 5.5 13.3 5.5 14.3 0 14-4.6 14-5.5v-3.9c.1 0 .8 4.6-13.8 4.6z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/images/tool-styleeditor.svg
+++ b/devtools/client/themes/images/tool-styleeditor.svg
@@ -1,9 +1,6 @@
 <!-- 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" width="17" height="16" viewBox="0 0 17 16">
-  <g fill="#edf0f1" fill-rule="evenodd">
-    <path d="m10,11.1 0,2.3-7.8,0 0-11.2 5.6-0 1.1,1.1 1.7-1.6-1.7-1.7-8.9,0 0,15.6 12.2,0 0-6.7z"/>
-    <path d="M6.7,7.8L14.5,0l2.2,2.2L8.9,10l-3.3,1.1L6.7,7.8z"/>
-  </g>
-</svg>
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="whitesmoke">
+    <path d="M11 18.7c.3-.5.4-1.1.4-1.9v-5-.6c0-.2.1-.3.2-.4.1-.1.2-.2.4-.3.1-.1.4-.1.9-.1h.9c1.2 0 2.1-1 2.1-2.3 0-1.2-.9-2.2-2-2.3H11.1c-.7 0-1.3.1-1.8.3-.5 0-1 .3-1.4.8-.4.4-.8.9-1 1.6-.2.6-.3 1.4-.3 2.2v5.4c0 .4-.1.6-.3.8-.2.2-.5.4-.8.5-.3.1-.5.2-.8.2-.4 0 0 .1-.2.1h-.1c-1.1.1-1.9 1.1-1.9 2.2 0 1.2.8 2.1 1.9 2.2.2.1-.1 0 .3.1.3 0 .5.1.8.3.3.1.6.3.8.5.2.2.3.5.3.8v5.4c0 .9.1 1.6.4 2.2.2.6.6 1.2 1 1.6.4.4.9.7 1.5.9.6.2 1.2.3 1.8.3h3.1c1.2 0 2.1-1 2.1-2.3 0-1.2-.9-2.3-2.1-2.3h-1.3c-.5 0-.8-.1-.9-.1-.2-.1-.3-.2-.4-.3-.1-.1-.2-.3-.2-.4v-5.6c0-.8-.1-1.4-.4-1.9-.3-.5-.6-.9-.9-1.1-.3-.1-.4-.2-.5-.2.1-.1.2-.1.3-.2.4-.3.7-.7.9-1.1zM29 18.7c-.3-.5-.4-1.1-.4-1.9v-5-.6c0-.2-.1-.3-.2-.4-.1-.1-.2-.2-.4-.3-.1-.1-.4-.1-.9-.1h-.8-.1c-1.2 0-2.1-1-2.1-2.3 0-1.2.9-2.2 2-2.3H28.9c.7 0 1.3.1 1.8.3.6.2 1.1.5 1.5.9.4.4.8.9 1 1.6.2.6.3 1.4.3 2.2v5.4c0 .4.1.6.3.8.2.2.5.4.8.5.3.1.5.2.8.2.4 0 0 .1.2.1h.1c1.1.1 1.9 1.1 1.9 2.2 0 1.2-.8 2.1-1.9 2.2-.2.1.1 0-.3.1-.3 0-.5.1-.8.3-.3.1-.6.3-.8.5-.2.2-.3.5-.3.8v5.4c0 .9-.1 1.6-.4 2.2-.2.6-.6 1.2-1 1.6-.4.4-.9.7-1.5.9-.6.2-1.2.3-1.8.3h-3-.1c-1.2 0-2.1-1-2.1-2.3 0-1.2.9-2.3 2.1-2.3H27c.5 0 .8-.1.9-.1.2-.1.3-.2.4-.3.1-.1.2-.3.2-.4v-5.6c0-.8.1-1.4.4-1.9.3-.5.6-.9.9-1.1.1-.1.2-.1.3-.2-.1-.1-.2-.1-.3-.2-.3-.4-.6-.8-.8-1.2z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/images/tool-webaudio.svg
+++ b/devtools/client/themes/images/tool-webaudio.svg
@@ -1,6 +1,6 @@
 <!-- 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" width="16px" height="16px" viewBox="-6.167 -16.135 100 100">
-  <path fill="none" stroke="#edf0f1" stroke-width="8" stroke-linecap="round" stroke-miterlimit="10" d="M86.666,33.864  c-0.797,5.297-3.467,32.799-10.518,32.866c-7.086,0.066-9.973-27.596-10.9-32.866C64.322,28.597,61.436,0.933,54.35,1  c-7.105,0.068-9.644,27.561-10.517,32.864c-0.874,5.305-3.412,32.799-10.517,32.866c-7.087,0.066-9.974-27.596-10.899-32.866  C21.49,28.597,18.604,0.933,11.517,1C4.466,1.067,1.796,28.569,1,33.864"/>
-</svg>
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="whitesmoke">
+    <path d="M32.4 34.7c-3.9 0-5.1-6.2-6.1-13.2-.1-.5-.1-.9-.2-1.1-.1-.3-.1-.7-.2-1.3-.7-5-1.4-7.4-1.8-8.6-.5 1.2-1.1 3.7-1.7 8.6-.1.6-.1 1-.2 1.3 0 .3-.1.7-.2 1.2-.9 6.9-2.1 13.2-6 13.2s-5.1-6.2-6.1-13.2c-.1-.5-.1-.9-.2-1.1-.1-.3-.1-.7-.2-1.3-.7-5-1.4-7.4-1.8-8.6-.5 1.2-1.1 3.7-1.8 8.9-.1.4-.1.8-.1 1-.2 1.2-1.3 2-2.4 1.8-1.2-.2-2-1.3-1.8-2.4 0-.2.1-.5.1-.9C2.7 10.2 4 5.3 7.6 5.3c3.9 0 5.1 6.2 6.1 13.2.1.5.1.9.2 1.2 0 .3.1.7.2 1.3.7 5 1.4 7.4 1.8 8.7.5-1.2 1.1-3.7 1.7-8.6.1-.6.1-1 .2-1.3 0-.3.1-.7.2-1.2.9-6.9 2.1-13.2 6-13.2s5.1 6.2 6.1 13.2c.1.5.1.9.2 1.2 0 .3.1.7.2 1.3.7 5 1.4 7.4 1.8 8.6.5-1.2 1.1-3.7 1.8-8.9.1-.4.1-.8.1-1 .2-1.2 1.3-2 2.4-1.8 1.2.2 2 1.3 1.8 2.4 0 .2-.1.5-.1.9-1 8.5-2.3 13.4-5.9 13.4z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/images/tool-webconsole.svg
+++ b/devtools/client/themes/images/tool-webconsole.svg
@@ -1,6 +1,6 @@
 <!-- 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" width="16" height="16" viewBox="0 0 16 16">
-  <path fill="#edf0f1" fill-rule="evenodd" d="M2,2h5.4l6.5,6.5L7.4,15H2l6.5-6.5L2,2z"/>
-</svg>
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="whitesmoke">
+    <path d="M16.3 15.4L7.7 6.9c-.3-.3-.7-.5-1.1-.5H4.3c-.7 0-1.2.4-1.5 1-.2.6-.1 1.3.4 1.8l7.4 7.4-7.1 7.1c-.5.5-.6 1.2-.4 1.8.3.6.8 1 1.5 1h2.3c.4 0 .8-.2 1.1-.5l8.3-8.3c.6-.6.6-1.6 0-2.3zM34.9 33.6H9.2c-1.3 0-2.4-1.1-2.4-2.4 0-1.3 1.1-2.4 2.4-2.4h25.7c1.3 0 2.4 1.1 2.4 2.4 0 1.3-1.1 2.4-2.4 2.4z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -469,17 +469,17 @@ a {
 
 .message[open] .stacktrace {
   display: block;
 }
 
 .message .theme-twisty {
   display: inline-block;
   vertical-align: middle;
-  margin: 0 3px 0 0;
+  margin: 3px 3px 0 0;
 }
 
 .stacktrace li {
   display: flex;
   margin: 0;
 }
 
 .stacktrace .function {
--- a/devtools/shared/gcli/commands/media.js
+++ b/devtools/shared/gcli/commands/media.js
@@ -1,50 +1,56 @@
 /* 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";
 
+const {Ci} = require("chrome");
 const l10n = require("gcli/l10n");
 
+function getContentViewer(context) {
+  let {window} = context.environment;
+  return window.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDocShell)
+               .contentViewer;
+}
+
 exports.items = [
   {
     name: "media",
     description: l10n.lookup("mediaDesc")
   },
   {
     item: "command",
-    runAt: "client",
+    runAt: "server",
     name: "media emulate",
     description: l10n.lookup("mediaEmulateDesc"),
     manual: l10n.lookup("mediaEmulateManual"),
     params: [
       {
         name: "type",
         description: l10n.lookup("mediaEmulateType"),
         type: {
-           name: "selection",
-           data: [
-             "braille", "embossed", "handheld", "print", "projection",
-             "screen", "speech", "tty", "tv"
-           ]
+          name: "selection",
+          data: [
+            "braille", "embossed", "handheld", "print", "projection",
+            "screen", "speech", "tty", "tv"
+          ]
         }
       }
     ],
     exec: function(args, context) {
-      let markupDocumentViewer = context.environment.chromeWindow
-                                        .gBrowser.markupDocumentViewer;
-      markupDocumentViewer.emulateMedium(args.type);
+      let contentViewer = getContentViewer(context);
+      contentViewer.emulateMedium(args.type);
     }
   },
   {
     item: "command",
-    runAt: "client",
+    runAt: "server",
     name: "media reset",
     description: l10n.lookup("mediaResetDesc"),
     exec: function(args, context) {
-      let markupDocumentViewer = context.environment.chromeWindow
-                                        .gBrowser.markupDocumentViewer;
-      markupDocumentViewer.stopEmulatingMedium();
+      let contentViewer = getContentViewer(context);
+      contentViewer.stopEmulatingMedium();
     }
   }
 ];
--- a/gfx/layers/apz/src/APZCTreeManager.cpp
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -1,42 +1,42 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 #include "APZCTreeManager.h"
 #include "AsyncPanZoomController.h"
 #include "Compositor.h"                 // for Compositor
-#include "gfxPrefs.h"                   // for gfxPrefs
 #include "HitTestingTreeNode.h"         // for HitTestingTreeNode
 #include "InputBlockState.h"            // for InputBlockState
 #include "InputData.h"                  // for InputData, etc
 #include "Layers.h"                     // for Layer, etc
 #include "mozilla/dom/Touch.h"          // for Touch
-#include "mozilla/gfx/Logging.h"        // for gfx::TreeLog
 #include "mozilla/gfx/Point.h"          // for Point
 #include "mozilla/layers/APZThreadUtils.h"  // for AssertOnCompositorThread, etc
 #include "mozilla/layers/AsyncCompositionManager.h" // for ViewTransform
 #include "mozilla/layers/CompositorParent.h" // for CompositorParent, etc
 #include "mozilla/layers/LayerMetricsWrapper.h"
 #include "mozilla/MouseEvents.h"
 #include "mozilla/mozalloc.h"           // for operator new
 #include "mozilla/TouchEvents.h"
 #include "mozilla/Preferences.h"        // for Preferences
 #include "mozilla/EventStateManager.h"  // for WheelPrefs
 #include "nsDebug.h"                    // for NS_WARNING
 #include "nsPoint.h"                    // for nsIntPoint
 #include "nsThreadUtils.h"              // for NS_IsMainThread
+#include "mozilla/gfx/Logging.h"        // for gfx::TreeLog
+#include "UnitTransforms.h"             // for ViewAs
+#include "gfxPrefs.h"                   // for gfxPrefs
 #include "OverscrollHandoffState.h"     // for OverscrollHandoffState
 #include "TaskThrottler.h"              // for TaskThrottler
 #include "TreeTraversal.h"              // for generic tree traveral algorithms
 #include "LayersLogging.h"              // for Stringify
 #include "Units.h"                      // for ParentlayerPixel
-#include "UnitTransforms.h"             // for ViewAs
 
 #define ENABLE_APZCTM_LOGGING 0
 // #define ENABLE_APZCTM_LOGGING 1
 
 #if ENABLE_APZCTM_LOGGING
 #  define APZCTM_LOG(...) printf_stderr("APZCTM: " __VA_ARGS__)
 #else
 #  define APZCTM_LOG(...)
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -58,17 +58,16 @@
 #include "nsDebug.h"                    // for NS_WARNING
 #include "nsIDOMWindowUtils.h"          // for nsIDOMWindowUtils
 #include "nsMathUtils.h"                // for NS_hypot
 #include "nsPoint.h"                    // for nsIntPoint
 #include "nsStyleConsts.h"
 #include "nsStyleStruct.h"              // for nsTimingFunction
 #include "nsTArray.h"                   // for nsTArray, nsTArray_Impl, etc
 #include "nsThreadUtils.h"              // for NS_IsMainThread
-#include "prsystem.h"                   // for PR_GetPhysicalMemorySize
 #include "SharedMemoryBasic.h"          // for SharedMemoryBasic
 #include "WheelScrollAnimation.h"
 
 // #define APZC_ENABLE_RENDERTRACE
 
 #define ENABLE_APZC_LOGGING 0
 // #define ENABLE_APZC_LOGGING 1
 
@@ -325,47 +324,32 @@ using mozilla::gfx::PointTyped;
  * less and get faster, more predictable paint times. When panning slowly we
  * can afford to paint more even though it's slower.
  *
  * \li\b apz.x_stationary_size_multiplier
  * \li\b apz.y_stationary_size_multiplier
  * The multiplier we apply to the displayport size if it is not skating (see
  * documentation for the skate size multipliers above).
  *
- * \li\b apz.x_skate_highmem_adjust
- * \li\b apz.y_skate_highmem_adjust
- * On high memory systems, we adjust the displayport during skating
- * to be larger so we can reduce checkerboarding.
- *
  * \li\b apz.zoom_animation_duration_ms
  * This controls how long the zoom-to-rect animation takes.\n
  * Units: ms
  */
 
 /**
  * Computed time function used for sampling frames of a zoom to animation.
  */
 StaticAutoPtr<ComputedTimingFunction> gZoomAnimationFunction;
 
 /**
  * Computed time function used for curving up velocity when it gets high.
  */
 StaticAutoPtr<ComputedTimingFunction> gVelocityCurveFunction;
 
 /**
- * Returns true if this is a high memory system and we can use
- * extra memory for a larger displayport to reduce checkerboarding.
- */
-static bool gIsHighMemSystem = false;
-static bool IsHighMemSystem()
-{
-  return gIsHighMemSystem;
-}
-
-/**
  * Maximum zoom amount, always used, even if a page asks for higher.
  */
 static const CSSToParentLayerScale MAX_ZOOM(8.0f);
 
 /**
  * Minimum zoom amount, always used, even if a page asks for lower.
  */
 static const CSSToParentLayerScale MIN_ZOOM(0.125f);
@@ -813,20 +797,16 @@ AsyncPanZoomController::InitializeGlobal
   ClearOnShutdown(&gZoomAnimationFunction);
   gVelocityCurveFunction = new ComputedTimingFunction();
   gVelocityCurveFunction->Init(
     nsTimingFunction(gfxPrefs::APZCurveFunctionX1(),
                      gfxPrefs::APZCurveFunctionY2(),
                      gfxPrefs::APZCurveFunctionX2(),
                      gfxPrefs::APZCurveFunctionY2()));
   ClearOnShutdown(&gVelocityCurveFunction);
-
-  uint64_t sysmem = PR_GetPhysicalMemorySize();
-  uint64_t threshold = 1LL << 32; // 4 GB in bytes
-  gIsHighMemSystem = sysmem >= threshold;
 }
 
 AsyncPanZoomController::AsyncPanZoomController(uint64_t aLayersId,
                                                APZCTreeManager* aTreeManager,
                                                const nsRefPtr<InputQueue>& aInputQueue,
                                                GeckoContentController* aGeckoContentController,
                                                TaskThrottler* aPaintThrottler,
                                                GestureBehavior aGestures)
@@ -2443,21 +2423,16 @@ CalculateDisplayPortSize(const CSSSize& 
 {
   float xMultiplier = fabsf(aVelocity.x) < gfxPrefs::APZMinSkateSpeed()
                         ? gfxPrefs::APZXStationarySizeMultiplier()
                         : gfxPrefs::APZXSkateSizeMultiplier();
   float yMultiplier = fabsf(aVelocity.y) < gfxPrefs::APZMinSkateSpeed()
                         ? gfxPrefs::APZYStationarySizeMultiplier()
                         : gfxPrefs::APZYSkateSizeMultiplier();
 
-  if (IsHighMemSystem()) {
-    xMultiplier += gfxPrefs::APZXSkateHighMemAdjust();
-    yMultiplier += gfxPrefs::APZYSkateHighMemAdjust();
-  }
-
   // Ensure that it is at least as large as the visible area inflated by the
   // danger zone. If this is not the case then the "AboutToCheckerboard"
   // function in TiledContentClient.cpp will return true even in the stable
   // state.
   float xSize = std::max(aCompositionSize.width * xMultiplier,
                          aCompositionSize.width + (2 * gfxPrefs::APZDangerZoneX()));
   float ySize = std::max(aCompositionSize.height * yMultiplier,
                          aCompositionSize.height + (2 * gfxPrefs::APZDangerZoneY()));
--- a/gfx/thebes/gfxPrefs.h
+++ b/gfx/thebes/gfxPrefs.h
@@ -180,20 +180,18 @@ private:
   DECL_GFX_PREF(Live, "apz.printtree",                         APZPrintTree, bool, false);
   DECL_GFX_PREF(Live, "apz.smooth_scroll_repaint_interval",    APZSmoothScrollRepaintInterval, int32_t, 75);
   DECL_GFX_PREF(Live, "apz.test.logging_enabled",              APZTestLoggingEnabled, bool, false);
   DECL_GFX_PREF(Live, "apz.touch_start_tolerance",             APZTouchStartTolerance, float, 1.0f/4.5f);
   DECL_GFX_PREF(Live, "apz.use_paint_duration",                APZUsePaintDuration, bool, true);
   DECL_GFX_PREF(Live, "apz.velocity_bias",                     APZVelocityBias, float, 1.0f);
   DECL_GFX_PREF(Live, "apz.velocity_relevance_time_ms",        APZVelocityRelevanceTime, uint32_t, 150);
   DECL_GFX_PREF(Live, "apz.x_skate_size_multiplier",           APZXSkateSizeMultiplier, float, 1.5f);
-  DECL_GFX_PREF(Live, "apz.x_skate_highmem_adjust",            APZXSkateHighMemAdjust, float, 0.0f);
   DECL_GFX_PREF(Live, "apz.x_stationary_size_multiplier",      APZXStationarySizeMultiplier, float, 3.0f);
   DECL_GFX_PREF(Live, "apz.y_skate_size_multiplier",           APZYSkateSizeMultiplier, float, 2.5f);
-  DECL_GFX_PREF(Live, "apz.y_skate_highmem_adjust",            APZYSkateHighMemAdjust, float, 0.0f);
   DECL_GFX_PREF(Live, "apz.y_stationary_size_multiplier",      APZYStationarySizeMultiplier, float, 3.5f);
   DECL_GFX_PREF(Live, "apz.zoom_animation_duration_ms",        APZZoomAnimationDuration, int32_t, 250);
 
   DECL_GFX_PREF(Live, "browser.ui.zoom.force-user-scalable",   ForceUserScalable, bool, false);
   DECL_GFX_PREF(Live, "browser.viewport.desktopWidth",         DesktopViewportWidth, int32_t, 980);
 
   DECL_GFX_PREF(Live, "dom.meta-viewport.enabled",             MetaViewportEnabled, bool, false);
   DECL_GFX_PREF(Once, "dom.vr.enabled",                        VREnabled, bool, false);
--- a/media/libcubeb/src/audiotrack_definitions.h
+++ b/media/libcubeb/src/audiotrack_definitions.h
@@ -47,35 +47,26 @@ enum event_type {
   EVENT_UNDERRUN = 1,
   EVENT_LOOP_END = 2,
   EVENT_MARKER = 3,
   EVENT_NEW_POS = 4,
   EVENT_BUFFER_END = 5
 };
 
 /**
- * From https://android.googlesource.com/platform/frameworks/base/+/android-2.2.3_r2.1/include/media/AudioSystem.h
- * and 
- * https://android.googlesource.com/platform/system/core/+/android-4.2.2_r1/include/system/audio.h
+ * From https://android.googlesource.com/platform/system/core/+/android-4.2.2_r1/include/system/audio.h
  */
 
 #define AUDIO_STREAM_TYPE_MUSIC 3
 
 enum {
   AUDIO_CHANNEL_OUT_FRONT_LEFT_ICS  = 0x1,
   AUDIO_CHANNEL_OUT_FRONT_RIGHT_ICS = 0x2,
   AUDIO_CHANNEL_OUT_MONO_ICS     = AUDIO_CHANNEL_OUT_FRONT_LEFT_ICS,
   AUDIO_CHANNEL_OUT_STEREO_ICS   = (AUDIO_CHANNEL_OUT_FRONT_LEFT_ICS | AUDIO_CHANNEL_OUT_FRONT_RIGHT_ICS)
 } AudioTrack_ChannelMapping_ICS;
 
-enum {
-  AUDIO_CHANNEL_OUT_FRONT_LEFT_Froyo = 0x4,
-  AUDIO_CHANNEL_OUT_FRONT_RIGHT_Froyo = 0x8,
-  AUDIO_CHANNEL_OUT_MONO_Froyo = AUDIO_CHANNEL_OUT_FRONT_LEFT_Froyo,
-  AUDIO_CHANNEL_OUT_STEREO_Froyo = (AUDIO_CHANNEL_OUT_FRONT_LEFT_Froyo | AUDIO_CHANNEL_OUT_FRONT_RIGHT_Froyo)
-} AudioTrack_ChannelMapping_Froyo;
-
 typedef enum {
   AUDIO_FORMAT_PCM = 0x00000000,
   AUDIO_FORMAT_PCM_SUB_16_BIT = 0x1,
   AUDIO_FORMAT_PCM_16_BIT = (AUDIO_FORMAT_PCM | AUDIO_FORMAT_PCM_SUB_16_BIT),
 } AudioTrack_SampleType;
 
--- a/media/libcubeb/src/cubeb_audiotrack.c
+++ b/media/libcubeb/src/cubeb_audiotrack.c
@@ -51,29 +51,23 @@ void audiotrack_stream_destroy(cubeb_str
 
 struct AudioTrack {
   /* only available on ICS and later. The second int paramter is in fact of type audio_stream_type_t. */
   /* static */ status_t (*get_min_frame_count)(int* frame_count, int stream_type, uint32_t rate);
   /* if we have a recent ctor, but can't find the above symbol, we
    * can get the minimum frame count with this signature, and we are
    * running gingerbread. */
   /* static */ status_t (*get_min_frame_count_gingerbread)(int* frame_count, int stream_type, uint32_t rate);
-  /* if this symbol is not availble, and the next one is, we know
-   * we are on a Froyo (Android 2.2) device. */
   void* (*ctor)(void* instance, int, unsigned int, int, int, int, unsigned int, void (*)(int, void*, void*), void*, int, int);
-  void* (*ctor_froyo)(void* instance, int, unsigned int, int, int, int, unsigned int, void (*)(int, void*, void*), void*, int);
   void* (*dtor)(void* instance);
   void (*start)(void* instance);
   void (*pause)(void* instance);
   uint32_t (*latency)(void* instance);
   status_t (*check)(void* instance);
   status_t (*get_position)(void* instance, uint32_t* position);
-  /* only used on froyo. */
-  /* static */ int (*get_output_frame_count)(int* frame_count, int stream);
-  /* static */ int (*get_output_latency)(uint32_t* latency, int stream);
   /* static */ int (*get_output_samplingrate)(int* samplerate, int stream);
   status_t (*set_marker_position)(void* instance, unsigned int);
   status_t (*set_volume)(void* instance, float left, float right);
 };
 
 struct cubeb {
   struct cubeb_ops const * ops;
   void * library;
@@ -133,64 +127,28 @@ audiotrack_refill(int event, void* user,
     assert(0 && "We don't support the setPositionUpdatePeriod feature of audiotrack.");
     break;
   case EVENT_BUFFER_END:
     assert(0 && "Should not happen.");
     break;
   }
 }
 
-/* We are running on froyo if we found the right AudioTrack constructor */
-static int
-audiotrack_version_is_froyo(cubeb * ctx)
-{
-  return ctx->klass.ctor_froyo != NULL;
-}
-
 /* We are running on gingerbread if we found the gingerbread signature for
  * getMinFrameCount */
 static int
 audiotrack_version_is_gingerbread(cubeb * ctx)
 {
   return ctx->klass.get_min_frame_count_gingerbread != NULL;
 }
 
 int
 audiotrack_get_min_frame_count(cubeb * ctx, cubeb_stream_params * params, int * min_frame_count)
 {
   status_t status;
-  /* Recent Android have a getMinFrameCount method. On Froyo, we have to compute it by hand. */
-  if (audiotrack_version_is_froyo(ctx)) {
-    int samplerate, frame_count, latency, min_buffer_count;
-    status = ctx->klass.get_output_frame_count(&frame_count, params->stream_type);
-    if (status) {
-      ALOG("error getting the output frame count.");
-      return CUBEB_ERROR;
-    }
-    status = ctx->klass.get_output_latency((uint32_t*)&latency, params->stream_type);
-    if (status) {
-      ALOG("error getting the output frame count.");
-      return CUBEB_ERROR;
-    }
-    status = ctx->klass.get_output_samplingrate(&samplerate, params->stream_type);
-    if (status) {
-      ALOG("error getting the output frame count.");
-      return CUBEB_ERROR;
-    }
-
-    /* Those numbers were found reading the Android source. It is the minimum
-     * numbers that will be accepted by the AudioTrack class, hence yielding the
-     * best latency possible.
-     * See https://android.googlesource.com/platform/frameworks/base/+/android-2.2.3_r2.1/media/libmedia/AudioTrack.cpp
-     * around line 181 for Android 2.2 */
-    min_buffer_count = latency / ((1000 * frame_count) / samplerate);
-    min_buffer_count = min_buffer_count < 2 ? min_buffer_count : 2;
-    *min_frame_count = (frame_count * params->rate * min_buffer_count) / samplerate;
-    return CUBEB_OK;
-  }
   /* Recent Android have a getMinFrameCount method. */
   if (!audiotrack_version_is_gingerbread(ctx)) {
     status = ctx->klass.get_min_frame_count(min_frame_count, params->stream_type, params->rate);
   } else {
     status = ctx->klass.get_min_frame_count_gingerbread(min_frame_count, params->stream_type, params->rate);
   }
   if (status != 0) {
     ALOG("error getting the min frame count");
@@ -217,54 +175,43 @@ audiotrack_init(cubeb ** context, char c
    * using only the name of the library. */
   ctx->library = dlopen("libmedia.so", RTLD_LAZY);
   if (!ctx->library) {
     ALOG("dlopen error: %s.", dlerror());
     free(ctx);
     return CUBEB_ERROR;
   }
 
-  /* Recent Android first, then Froyo. */
+  /* Recent Android first, then Gingerbread. */
   DLSYM_DLERROR("_ZN7android10AudioTrackC1EijiiijPFviPvS1_ES1_ii", ctx->klass.ctor, ctx->library);
-  if (!ctx->klass.ctor) {
-    DLSYM_DLERROR("_ZN7android10AudioTrackC1EijiiijPFviPvS1_ES1_i", ctx->klass.ctor_froyo, ctx->library);
-    assert(ctx->klass.ctor_froyo);
-  }
   DLSYM_DLERROR("_ZN7android10AudioTrackD1Ev", ctx->klass.dtor, ctx->library);
 
   DLSYM_DLERROR("_ZNK7android10AudioTrack7latencyEv", ctx->klass.latency, ctx->library);
   DLSYM_DLERROR("_ZNK7android10AudioTrack9initCheckEv", ctx->klass.check, ctx->library);
 
   DLSYM_DLERROR("_ZN7android11AudioSystem21getOutputSamplingRateEPii", ctx->klass.get_output_samplingrate, ctx->library);
 
-  /* |getMinFrameCount| is not available on Froyo, and is available on
-   * gingerbread and ICS with a different signature. */
-  if (audiotrack_version_is_froyo(ctx)) {
-    DLSYM_DLERROR("_ZN7android11AudioSystem19getOutputFrameCountEPii", ctx->klass.get_output_frame_count, ctx->library);
-    DLSYM_DLERROR("_ZN7android11AudioSystem16getOutputLatencyEPji", ctx->klass.get_output_latency, ctx->library);
-  } else {
-    DLSYM_DLERROR("_ZN7android10AudioTrack16getMinFrameCountEPi19audio_stream_type_tj", ctx->klass.get_min_frame_count, ctx->library);
-    if (!ctx->klass.get_min_frame_count) {
-      DLSYM_DLERROR("_ZN7android10AudioTrack16getMinFrameCountEPiij", ctx->klass.get_min_frame_count_gingerbread, ctx->library);
-    }
+  /* |getMinFrameCount| is available on gingerbread and ICS with different signatures. */
+  DLSYM_DLERROR("_ZN7android10AudioTrack16getMinFrameCountEPi19audio_stream_type_tj", ctx->klass.get_min_frame_count, ctx->library);
+  if (!ctx->klass.get_min_frame_count) {
+    DLSYM_DLERROR("_ZN7android10AudioTrack16getMinFrameCountEPiij", ctx->klass.get_min_frame_count_gingerbread, ctx->library);
   }
 
   DLSYM_DLERROR("_ZN7android10AudioTrack5startEv", ctx->klass.start, ctx->library);
   DLSYM_DLERROR("_ZN7android10AudioTrack5pauseEv", ctx->klass.pause, ctx->library);
   DLSYM_DLERROR("_ZN7android10AudioTrack11getPositionEPj", ctx->klass.get_position, ctx->library);
   DLSYM_DLERROR("_ZN7android10AudioTrack17setMarkerPositionEj", ctx->klass.set_marker_position, ctx->library);
   DLSYM_DLERROR("_ZN7android10AudioTrack9setVolumeEff", ctx->klass.set_volume, ctx->library);
 
   /* check that we have a combination of symbol that makes sense */
   c = &ctx->klass;
-  if(!((c->ctor || c->ctor_froyo) && /* at least on ctor. */
+  if(!(c->ctor &&
        c->dtor && c->latency && c->check &&
        /* at least one way to get the minimum frame count to request. */
-       ((c->get_output_frame_count && c->get_output_latency && c->get_output_samplingrate) ||
-        c->get_min_frame_count ||
+       (c->get_min_frame_count ||
         c->get_min_frame_count_gingerbread) &&
        c->start && c->pause && c->get_position && c->set_marker_position)) {
     ALOG("Could not find all the symbols we need.");
     audiotrack_destroy(ctx);
     return CUBEB_ERROR;
   }
 
   ctx->ops = &audiotrack_ops;
@@ -361,46 +308,25 @@ audiotrack_stream_init(cubeb * ctx, cube
   stm->user_ptr = user_ptr;
   stm->params = stream_params;
 
   stm->instance = calloc(SIZE_AUDIOTRACK_INSTANCE, 1);
   (*(uint32_t*)((intptr_t)stm->instance + SIZE_AUDIOTRACK_INSTANCE - 4)) = 0xbaadbaad;
   assert(stm->instance && "cubeb: EOM");
 
   /* gingerbread uses old channel layout enum */
-  if (audiotrack_version_is_froyo(ctx) || audiotrack_version_is_gingerbread(ctx)) {
+  if (audiotrack_version_is_gingerbread(ctx)) {
     channels = stm->params.channels == 2 ? AUDIO_CHANNEL_OUT_STEREO_Legacy : AUDIO_CHANNEL_OUT_MONO_Legacy;
   } else {
     channels = stm->params.channels == 2 ? AUDIO_CHANNEL_OUT_STEREO_ICS : AUDIO_CHANNEL_OUT_MONO_ICS;
   }
 
-  if (audiotrack_version_is_froyo(ctx)) {
-    ctx->klass.ctor_froyo(stm->instance,
-                          stm->params.stream_type,
-                          stm->params.rate,
-                          AUDIO_FORMAT_PCM_16_BIT,
-                          channels,
-                          min_frame_count,
-                          0,
-                          audiotrack_refill,
-                          stm,
-                          0);
-  } else {
-    ctx->klass.ctor(stm->instance,
-                    stm->params.stream_type,
-                    stm->params.rate,
-                    AUDIO_FORMAT_PCM_16_BIT,
-                    channels,
-                    min_frame_count,
-                    0,
-                    audiotrack_refill,
-                    stm,
-                    0,
-                    0);
-  }
+  ctx->klass.ctor(stm->instance, stm->params.stream_type, stm->params.rate,
+                  AUDIO_FORMAT_PCM_16_BIT, channels, min_frame_count, 0,
+                  audiotrack_refill, stm, 0, 0);
 
   assert((*(uint32_t*)((intptr_t)stm->instance + SIZE_AUDIOTRACK_INSTANCE - 4)) == 0xbaadbaad);
 
   if (ctx->klass.check(stm->instance)) {
     ALOG("stream not initialized properly.");
     audiotrack_stream_destroy(stm);
     return CUBEB_ERROR;
   }
--- a/media/omx-plugin/OmxPlugin.cpp
+++ b/media/omx-plugin/OmxPlugin.cpp
@@ -21,31 +21,22 @@
 #include "mozilla/Types.h"
 #include "MPAPI.h"
 
 #include "android/log.h"
 
 #define MAX_DECODER_NAME_LEN 256
 #define AVC_MIME_TYPE "video/avc"
 
-#if !defined(MOZ_ANDROID_FROYO)
 #define DEFAULT_STAGEFRIGHT_FLAGS OMXCodec::kClientNeedsFramebuffer
-#else
-#define DEFAULT_STAGEFRIGHT_FLAGS 0
-#endif
 
 #undef LOG
 #define LOG(args...)  __android_log_print(ANDROID_LOG_INFO, "OmxPlugin" , ## args)
 
-#if defined(MOZ_ANDROID_FROYO) || defined(MOZ_ANDROID_GB)
-// Android versions 2.x.x have common API differences
-#define MOZ_ANDROID_V2_X_X
-#endif
-
-#if !defined(MOZ_ANDROID_V2_X_X) && !defined(MOZ_ANDROID_HC)
+#if !defined(MOZ_ANDROID_GB) && !defined(MOZ_ANDROID_HC)
 #define MOZ_ANDROID_V4_OR_ABOVE
 #endif
 
 #if defined(MOZ_ANDROID_V4_OR_ABOVE)
 #include <I420ColorConverter.h>
 #endif
 
 using namespace MPAPI;
@@ -237,36 +228,32 @@ static sp<IOMX> GetOMX() {
 }
 #endif
 
 static uint32_t
 GetDefaultStagefrightFlags(PluginHost *aPluginHost)
 {
   uint32_t flags = DEFAULT_STAGEFRIGHT_FLAGS;
 
-#if !defined(MOZ_ANDROID_FROYO)
-
   char hardware[256] = "";
   aPluginHost->GetSystemInfoString("hardware", hardware, sizeof(hardware));
 
   if (!strcmp("qcom", hardware) ||
       !strncmp("mt", hardware, 2)) {
     // Qualcomm's OMXCodec implementation interprets this flag to mean that we
     // only want a thumbnail and therefore only need one frame. After the first
     // frame it returns EOS.
     // Some MediaTek chipsets have also been found to do the same.
     // All other OMXCodec implementations seen so far interpret this flag
     // sanely; some do not return full framebuffers unless this flag is passed.
     flags &= ~OMXCodec::kClientNeedsFramebuffer;
   }
 
   LOG("Hardware %s; using default flags %#x\n", hardware, flags);
 
-#endif
-
   return flags;
 }
 
 static uint32_t GetVideoCreationFlags(PluginHost* aPluginHost)
 {
 #ifdef MOZ_WIDGET_GONK
   // Flag value of zero means return a hardware or software decoder
   // depending on what the device supports.
@@ -276,17 +263,17 @@ static uint32_t GetVideoCreationFlags(Pl
   // CreationFlags flags. This is useful for A/B testing hardware and software
   // decoders for performance and bugs. The interesting flag values are:
   //  0 = Let Stagefright choose hardware or software decoding (default)
   //  8 = Force software decoding
   // 16 = Force hardware decoding
   int32_t flags = 0;
   aPluginHost->GetIntPref("media.stagefright.omxcodec.flags", &flags);
   if (flags != 0) {
-#if !defined(MOZ_ANDROID_V2_X_X)
+#if !defined(MOZ_ANDROID_GB)
     LOG("media.stagefright.omxcodec.flags=%d", flags);
     if ((flags & OMXCodec::kHardwareCodecsOnly) != 0) {
       LOG("FORCE HARDWARE DECODING");
     } else if ((flags & OMXCodec::kSoftwareCodecsOnly) != 0) {
       LOG("FORCE SOFTWARE DECODING");
     }
 #endif
   }
@@ -441,17 +428,17 @@ static sp<MediaSource> CreateVideoSource
       LOG("Unknown video color format: %#x", videoColorFormat);
     } else {
       LOG("Video color format not found");
     }
 
     // Throw away the videoSource and try again with new flags.
     LOG("Falling back to software decoder");
     videoSource.clear();
-#if defined(MOZ_ANDROID_V2_X_X)
+#if defined(MOZ_ANDROID_GB)
     flags = DEFAULT_STAGEFRIGHT_FLAGS | OMXCodec::kPreferSoftwareCodecs;
 #else
     flags = DEFAULT_STAGEFRIGHT_FLAGS | OMXCodec::kSoftwareCodecsOnly;
 #endif
   }
 
   MOZ_ASSERT(flags != DEFAULT_STAGEFRIGHT_FLAGS);
   return OMXCodec::Create(aOmx, aVideoTrack->getFormat(), false, aVideoTrack,
@@ -516,21 +503,16 @@ bool OmxDecoder::Init()
   sp<IOMX> omx = GetOMX();
 #else
   sp<IOMX> omx = sClientInstance.get()->interface();
 #endif
 
   sp<MediaSource> videoTrack;
   sp<MediaSource> videoSource;
   if (videoTrackIndex != -1 && (videoTrack = extractor->getTrack(videoTrackIndex)) != nullptr) {
-#if defined(MOZ_ANDROID_FROYO)
-    // Allow up to 720P video.
-    sp<MetaData> meta = extractor->getTrackMetaData(videoTrackIndex);
-    meta->setInt32(kKeyMaxInputSize, (1280 * 720 * 3) / 2);
-#endif
     videoSource = CreateVideoSource(mPluginHost, omx, videoTrack);
     if (videoSource == nullptr) {
       LOG("OMXCodec failed to initialize video decoder for \"%s\"", videoMime);
       return false;
     }
     status_t status = videoSource->start();
     if (status != OK) {
       LOG("videoSource->start() failed with status %#x", status);
@@ -618,17 +600,17 @@ bool OmxDecoder::Init()
 
 bool OmxDecoder::SetVideoFormat() {
   sp<MetaData> format = mVideoSource->getFormat();
 
   // Stagefright's kKeyWidth and kKeyHeight are what MPAPI calls stride and
   // slice height. Stagefright only seems to use its kKeyStride and
   // kKeySliceHeight to initialize camera video formats.
 
-#if defined(DEBUG) && !defined(MOZ_ANDROID_FROYO)
+#if defined(DEBUG)
   int32_t unexpected;
   if (format->findInt32(kKeyStride, &unexpected))
     LOG("Expected kKeyWidth, but found kKeyStride %d", unexpected);
   if (format->findInt32(kKeySliceHeight, &unexpected))
     LOG("Expected kKeyHeight, but found kKeySliceHeight %d", unexpected);
 #endif // DEBUG
 
   const char *componentName;
@@ -646,48 +628,44 @@ bool OmxDecoder::SetVideoFormat() {
   }
 
   if (mVideoSliceHeight <= 0) {
     LOG("slice height %d must be positive", mVideoSliceHeight);
     return false;
   }
 
   // Gingerbread does not support the kKeyCropRect key
-#if !defined(MOZ_ANDROID_V2_X_X)
+#if !defined(MOZ_ANDROID_GB)
   if (!format->findRect(kKeyCropRect, &mVideoCropLeft, &mVideoCropTop,
                                       &mVideoCropRight, &mVideoCropBottom)) {
 #endif
     mVideoCropLeft = 0;
     mVideoCropTop = 0;
     mVideoCropRight = mVideoStride - 1;
     mVideoCropBottom = mVideoSliceHeight - 1;
     LOG("crop rect not available, assuming no cropping");
-#if !defined(MOZ_ANDROID_V2_X_X)
+#if !defined(MOZ_ANDROID_GB)
   }
 #endif
 
   if (mVideoCropLeft < 0 || mVideoCropLeft >= mVideoCropRight || mVideoCropRight >= mVideoStride ||
       mVideoCropTop < 0 || mVideoCropTop >= mVideoCropBottom || mVideoCropBottom >= mVideoSliceHeight) {
     LOG("invalid crop rect %d,%d-%d,%d", mVideoCropLeft, mVideoCropTop, mVideoCropRight, mVideoCropBottom);
     return false;
   }
 
   mVideoWidth = mVideoCropRight - mVideoCropLeft + 1;
   mVideoHeight = mVideoCropBottom - mVideoCropTop + 1;
   MOZ_ASSERT(mVideoWidth > 0 && mVideoWidth <= mVideoStride);
   MOZ_ASSERT(mVideoHeight > 0 && mVideoHeight <= mVideoSliceHeight);
 
-#if !defined(MOZ_ANDROID_FROYO)
   if (!format->findInt32(kKeyRotation, &mVideoRotation)) {
-#endif
     mVideoRotation = 0;
-#if !defined(MOZ_ANDROID_FROYO)
     LOG("rotation not available, assuming 0");
   }
-#endif
 
   if (mVideoRotation != 0 && mVideoRotation != 90 &&
       mVideoRotation != 180 && mVideoRotation != 270) {
     LOG("invalid rotation %d, assuming 0", mVideoRotation);
   }
 
   LOG("width: %d height: %d component: %s format: %#x stride: %d sliceHeight: %d rotation: %d crop: %d,%d-%d,%d",
       mVideoWidth, mVideoHeight, componentName, mVideoColorFormat,
@@ -837,17 +815,17 @@ bool OmxDecoder::ToVideoFrame_ColorConve
   void *buffer = (*aBufferCallback)(mVideoWidth, mVideoHeight, MPAPI::RGB565);
 
   if (!buffer) {
     return false;
   }
 
   aFrame->mSize = mVideoWidth * mVideoHeight * 2;
 
-#if defined(MOZ_ANDROID_V2_X_X)
+#if defined(MOZ_ANDROID_GB)
   mColorConverter->convert(mVideoWidth, mVideoHeight,
                            aData, 0 /* srcSkip */,
                            buffer, mVideoWidth * 2);
 #else
   mColorConverter->convert(aData, mVideoStride, mVideoSliceHeight,
                            mVideoCropLeft, mVideoCropTop,
                            mVideoCropLeft + mVideoWidth - 1,
                            mVideoCropTop + mVideoHeight - 1,
@@ -890,19 +868,16 @@ bool OmxDecoder::ToVideoFrame_I420ColorC
   return result == OK;
 #else
   return false;
 #endif
 }
 
 bool OmxDecoder::ToVideoFrame(VideoFrame *aFrame, int64_t aTimeUs, void *aData, size_t aSize, bool aKeyFrame, BufferCallback *aBufferCallback) {
   switch (mVideoColorFormat) {
-// Froyo support is best handled with the android color conversion code. I
-// get corrupted videos when using our own routines below.
-#if !defined(MOZ_ANDROID_FROYO)
   case OMX_COLOR_FormatYUV420Planar: // e.g. Asus Transformer, Stagefright's software decoder
     ToVideoFrame_YUV420Planar(aFrame, aTimeUs, aData, aSize, aKeyFrame);
     break;
   case OMX_COLOR_FormatCbYCrY: // e.g. Droid 1
     ToVideoFrame_CbYCrY(aFrame, aTimeUs, aData, aSize, aKeyFrame);
     break;
   case OMX_COLOR_FormatYUV420SemiPlanar: // e.g. Galaxy S III
     ToVideoFrame_YUV420SemiPlanar(aFrame, aTimeUs, aData, aSize, aKeyFrame);
@@ -914,17 +889,16 @@ bool OmxDecoder::ToVideoFrame(VideoFrame
     ToVideoFrame_YVU420PackedSemiPlanar32m4ka(aFrame, aTimeUs, aData, aSize, aKeyFrame);
     break;
   case OMX_TI_COLOR_FormatYUV420PackedSemiPlanar: // e.g. Galaxy Nexus
     ToVideoFrame_YUV420PackedSemiPlanar(aFrame, aTimeUs, aData, aSize, aKeyFrame);
     break;
   case OMX_COLOR_Format16bitRGB565:
     return ToVideoFrame_RGB565(aFrame, aTimeUs, aData, aSize, aKeyFrame, aBufferCallback);
     break;
-#endif
   default:
     if (!ToVideoFrame_ColorConverter(aFrame, aTimeUs, aData, aSize, aKeyFrame, aBufferCallback) &&
         !ToVideoFrame_I420ColorConverter(aFrame, aTimeUs, aData, aSize, aKeyFrame, aBufferCallback)) {
       LOG("Unknown video color format: %#x", mVideoColorFormat);
       return false;
     }
   }
   return true;
--- a/media/omx-plugin/lib/gb/libstagefright/libstagefright.cpp
+++ b/media/omx-plugin/lib/gb/libstagefright/libstagefright.cpp
@@ -76,21 +76,17 @@ MOZ_EXPORT bool MetaData::findCString(ui
 {
   return false;
 }
  
 MOZ_EXPORT MediaSource::ReadOptions::ReadOptions()
 {
 }
 
-MOZ_EXPORT void MediaSource::ReadOptions::setSeekTo(int64_t time_us
-#if !defined(MOZ_ANDROID_FROYO)
-, SeekMode mode
-#endif
-)
+MOZ_EXPORT void MediaSource::ReadOptions::setSeekTo(int64_t time_us, SeekMode mode)
 {
 }
 
 MOZ_EXPORT sp<DataSource> DataSource::CreateFromURI(
           const char *uri,
           const KeyedVector<String8, String8> *headers) {
   return 0;
 }
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -3304,17 +3304,17 @@ public class BrowserApp extends GeckoApp
         MenuUtils.safeSetVisible(aMenu, R.id.new_private_tab, privateTabVisible);
 
         // Disable PDF generation (save and print) for about:home and xul pages.
         boolean allowPDF = (!(isAboutHome(tab) ||
                                tab.getContentType().equals("application/vnd.mozilla.xul+xml") ||
                                tab.getContentType().startsWith("video/")));
         saveAsPDF.setEnabled(allowPDF);
         print.setEnabled(allowPDF);
-        print.setVisible(Versions.feature19Plus && AppConstants.NIGHTLY_BUILD);
+        print.setVisible(Versions.feature19Plus);
 
         // Disable find in page for about:home, since it won't work on Java content.
         findInPage.setEnabled(!isAboutHome(tab));
 
         charEncoding.setVisible(GeckoPreferences.getCharEncodingState());
 
         if (mProfile.inGuestMode()) {
             exitGuestMode.setVisible(true);
--- a/mobile/android/base/GeckoView.java
+++ b/mobile/android/base/GeckoView.java
@@ -217,17 +217,17 @@ public class GeckoView extends LayerView
             GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, Window.class,
                     "open", window, metrics.widthPixels, metrics.heightPixels);
         }
     }
 
     @Override
     public void onDetachedFromWindow()
     {
-        super.onAttachedToWindow();
+        super.onDetachedFromWindow();
         window.disposeNative();
     }
 
     /**
     * Add a Browser to the GeckoView container.
     * @param url The URL resource to load into the new Browser.
     */
     public Browser addBrowser(String url) {
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -213,17 +213,17 @@
 <!ENTITY pref_header_devtools "Developer tools">
 
 <!ENTITY pref_cookies_menu "Cookies">
 <!ENTITY pref_cookies_accept_all "Enabled">
 <!ENTITY pref_cookies_not_accept_foreign "Enabled, excluding 3rd party">
 <!ENTITY pref_cookies_disabled "Disabled">
 
 <!ENTITY pref_tap_to_load_images_title "Tap-to-load images">
-<!ENTITY pref_tap_to_load_images_summary "Load images only when you tap on them">
+<!ENTITY pref_tap_to_load_images_summary2 "Load images only when you long press them">
 
 <!ENTITY pref_tracking_protection_title "Tracking protection">
 <!ENTITY pref_tracking_protection_summary3 "Enabled in Private Browsing">
 <!ENTITY pref_donottrack_title "Do not track">
 <!ENTITY pref_donottrack_summary "&brandShortName; will tell sites that you do not want to be tracked">
 
 <!ENTITY tracking_protection_prompt_title "Now with Tracking Protection">
 <!ENTITY tracking_protection_prompt_text "Actively block tracking elements so you don\'t have to worry.">
--- a/mobile/android/base/resources/layout/search_engine_row.xml
+++ b/mobile/android/base/resources/layout/search_engine_row.xml
@@ -14,16 +14,17 @@
                                           android:minWidth="@dimen/favicon_bg"
                                           android:minHeight="@dimen/favicon_bg"/>
 
     <org.mozilla.gecko.widget.FlowLayout android:id="@+id/suggestion_layout"
                                          android:layout_toRightOf="@id/suggestion_icon"
                                          android:layout_centerVertical="true"
                                          android:layout_width="wrap_content"
                                          android:layout_height="wrap_content"
+                                         android:layout_marginRight="15dp"
                                          android:duplicateParentState="true">
 
         <include layout="@layout/suggestion_item"
                  android:id="@+id/suggestion_user_entered"/>
 
     </org.mozilla.gecko.widget.FlowLayout>
 
 </merge>
--- a/mobile/android/base/resources/layout/suggestion_item.xml
+++ b/mobile/android/base/resources/layout/suggestion_item.xml
@@ -1,18 +1,16 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
-<!-- For reasons unknown, the minHeight attribute is required to keep suggestion_user_entered the same size as all other
-     search suggestion buttons-->
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:layout_width="wrap_content"
-              android:layout_height="32dp"
+              android:layout_height="wrap_content"
               android:minHeight="32dp"
               android:orientation="horizontal"
               android:background="@drawable/search_suggestion_button"
               android:gravity="center_vertical"
               android:clickable="true"
               android:padding="5dp">
 
     <ImageView android:id="@+id/suggestion_item_icon"
--- a/mobile/android/base/resources/values-v21/themes.xml
+++ b/mobile/android/base/resources/values-v21/themes.xml
@@ -9,16 +9,17 @@
         Base application theme.
     -->
     <style name="GeckoBase" parent="@android:style/Theme.Material.Light.DarkActionBar">
         <item name="android:colorPrimary">@color/text_and_tabs_tray_grey</item>
         <item name="android:colorPrimaryDark">@color/text_and_tabs_tray_grey</item>
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowContentOverlay">@null</item>
         <item name="android:actionBarStyle">@style/GeckoActionBar</item>
+        <item name="android:colorAccent">@color/action_orange</item>
     </style>
 
     <style name="ActionBar.FxAccountStatusActivity" parent="@android:style/Widget.Material.ActionBar.Solid">
         <item name="android:displayOptions">homeAsUp|showTitle</item>
     </style>
 
     <style name="ActionBar.GeckoPreferences" parent="@android:style/Widget.Material.ActionBar.Solid">
     </style>
--- a/mobile/android/base/resources/xml-v11/preferences_customize.xml
+++ b/mobile/android/base/resources/xml-v11/preferences_customize.xml
@@ -28,17 +28,17 @@
                     android:title="@string/pref_restore"
                     android:defaultValue="quit"
                     android:entries="@array/pref_restore_entries"
                     android:entryValues="@array/pref_restore_values"
                     android:persistent="true" />
 
     <CheckBoxPreference android:key="browser.image_blocking.enabled"
                         android:title="@string/pref_tap_to_load_images_title"
-                        android:summary="@string/pref_tap_to_load_images_summary"
+                        android:summary="@string/pref_tap_to_load_images_summary2"
                         android:defaultValue="false"/>
 
     <CheckBoxPreference android:key="android.not_a_preference.tab_queue"
                         android:title="@string/pref_tab_queue_title"
                         android:summary="@string/pref_tab_queue_summary"
                         android:defaultValue="false" />
 
     <CheckBoxPreference android:key="android.not_a_preference.openExternalURLsPrivately"
--- a/mobile/android/base/resources/xml-v11/preferences_customize_tablet.xml
+++ b/mobile/android/base/resources/xml-v11/preferences_customize_tablet.xml
@@ -35,17 +35,17 @@
                     android:title="@string/pref_restore"
                     android:defaultValue="quit"
                     android:entries="@array/pref_restore_entries"
                     android:entryValues="@array/pref_restore_values"
                     android:persistent="true" />
 
     <CheckBoxPreference android:key="browser.image_blocking.enabled"
                         android:title="@string/pref_tap_to_load_images_title"
-                        android:summary="@string/pref_tap_to_load_images_summary"
+                        android:summary="@string/pref_tap_to_load_images_summary2"
                         android:defaultValue="false"/>
 
     <CheckBoxPreference android:key="android.not_a_preference.tab_queue"
                         android:title="@string/pref_tab_queue_title"
                         android:summary="@string/pref_tab_queue_summary"
                         android:defaultValue="false" />
 
     <CheckBoxPreference android:key="android.not_a_preference.openExternalURLsPrivately"
--- a/mobile/android/base/resources/xml/preferences_customize.xml
+++ b/mobile/android/base/resources/xml/preferences_customize.xml
@@ -36,17 +36,17 @@
                     android:title="@string/pref_restore"
                     android:defaultValue="quit"
                     android:entries="@array/pref_restore_entries"
                     android:entryValues="@array/pref_restore_values"
                     android:persistent="true" />
 
     <CheckBoxPreference android:key="browser.image_blocking.enabled"
                         android:title="@string/pref_tap_to_load_images_title"
-                        android:summary="@string/pref_tap_to_load_images_summary"
+                        android:summary="@string/pref_tap_to_load_images_summary2"
                         android:defaultValue="false"/>
 
     <CheckBoxPreference android:key="android.not_a_preference.tab_queue"
                         android:title="@string/pref_tab_queue_title"
                         android:summary="@string/pref_tab_queue_summary"
                         android:defaultValue="false" />
 
     <CheckBoxPreference android:key="android.not_a_preference.openExternalURLsPrivately"
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -204,17 +204,17 @@
   <string name="pref_manage_logins">&pref_manage_logins;</string>
 
   <string name="pref_cookies_menu">&pref_cookies_menu;</string>
   <string name="pref_cookies_accept_all">&pref_cookies_accept_all;</string>
   <string name="pref_cookies_not_accept_foreign">&pref_cookies_not_accept_foreign;</string>
   <string name="pref_cookies_disabled">&pref_cookies_disabled;</string>
 
   <string name="pref_tap_to_load_images_title">&pref_tap_to_load_images_title;</string>
-  <string name="pref_tap_to_load_images_summary">&pref_tap_to_load_images_summary;</string>
+  <string name="pref_tap_to_load_images_summary2">&pref_tap_to_load_images_summary2;</string>
 
   <string name="pref_tracking_protection_title">&pref_tracking_protection_title;</string>
   <string name="pref_tracking_protection_summary">&pref_tracking_protection_summary3;</string>
   <string name="pref_donottrack_title">&pref_donottrack_title;</string>
   <string name="pref_donottrack_summary">&pref_donottrack_summary;</string>
 
   <string name="pref_char_encoding">&pref_char_encoding;</string>
   <string name="pref_char_encoding_on">&pref_char_encoding_on;</string>
--- a/mobile/android/base/tabs/TabsGridLayout.java
+++ b/mobile/android/base/tabs/TabsGridLayout.java
@@ -194,18 +194,21 @@ class TabsGridLayout extends GridView
     private void autoHidePanel() {
         tabsPanel.autoHidePanel();
     }
 
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
         switch (msg) {
             case ADDED:
-                // Refresh the list to make sure the new tab is added in the right position.
-                refreshTabsData();
+                // Refresh only if panel is shown. show() will call refreshTabsData() later again.
+                if (tabsPanel.isShown()) {
+                    // Refresh the list to make sure the new tab is added in the right position.
+                    refreshTabsData();
+                }
                 break;
 
             case CLOSED:
 
                 // This is limited to >= ICS as animations on GB devices are generally pants
                 if (Build.VERSION.SDK_INT >= 11 && tabsAdapter.getCount() > 0) {
                     animateRemoveTab(tab);
                 }
@@ -332,16 +335,20 @@ class TabsGridLayout extends GridView
     }
 
     private View getViewForTab(Tab tab) {
         final int position = tabsAdapter.getPositionForTab(tab);
         return getChildAt(position - getFirstVisiblePosition());
     }
 
     void closeTab(View v) {
+        if (tabsAdapter.getCount() == 1) {
+            autoHidePanel();
+        }
+
         TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag();
         Tab tab = Tabs.getInstance().getTab(itemView.getTabId());
 
         Tabs.getInstance().closeTab(tab, true);
     }
 
     private void animateRemoveTab(final Tab removedTab) {
         final int removedPosition = tabsAdapter.getPositionForTab(removedTab);
--- a/mobile/android/chrome/content/aboutLogins.xhtml
+++ b/mobile/android/chrome/content/aboutLogins.xhtml
@@ -70,17 +70,17 @@
       <div id="edit-login-header" class="header">
         <div id="edit-login-header-text"/>
       </div>
       <div class="edit-login-div">
         <div id="favicon" class="edit-login-icon"/>
           <input type="text" name="hostname" id="hostname" class="edit-login-input"/>
       </div>
       <div class="edit-login-div">
-        <input type="text" name="username" id="username" class="edit-login-input"/>
+        <input type="text" name="username" id="username" class="edit-login-input" autocomplete="off"/>
       </div>
       <div class="edit-login-div">
         <input type="password" id="password" name="password" value="password" class="edit-login-input" />
         <button id="password-btn"></button>
       </div>
       <div class="edit-login-div">
         <button id="update-btn" class="update-button">&aboutLogins.update;</button>
       </div>
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -583,27 +583,23 @@ pref("apz.zoom_animation_duration_ms", 2
 
 #if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
 // Desktop prefs
 pref("apz.fling_repaint_interval", 16);
 pref("apz.smooth_scroll_repaint_interval", 16);
 pref("apz.pan_repaint_interval", 16);
 pref("apz.x_skate_size_multiplier", "2.5");
 pref("apz.y_skate_size_multiplier", "3.5");
-pref("apz.x_skate_highmem_adjust", "1.0");
-pref("apz.y_skate_highmem_adjust", "2.5");
 #else
 // Mobile prefs
 pref("apz.fling_repaint_interval", 75);
 pref("apz.smooth_scroll_repaint_interval", 75);
 pref("apz.pan_repaint_interval", 250);
 pref("apz.x_skate_size_multiplier", "1.5");
 pref("apz.y_skate_size_multiplier", "2.5");
-pref("apz.x_skate_highmem_adjust", "0.0");
-pref("apz.y_skate_highmem_adjust", "0.0");
 #endif
 
 // APZ testing (bug 961289)
 pref("apz.test.logging_enabled", false);
 
 #ifdef XP_MACOSX
 // Whether to run in native HiDPI mode on machines with "Retina"/HiDPI display;
 //   <= 0 : hidpi mode disabled, display will just use pixel-based upscaling
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -337,10 +337,8 @@ user_pref("media.webspeech.synth.test", 
 // connections.
 user_pref("browser.urlbar.suggest.searches", false);
 
 // Turn off the location bar search suggestions opt-in.  It interferes with
 // tests that don't expect it to be there.
 user_pref("browser.urlbar.userMadeSearchSuggestionsChoice", true);
 
 user_pref("dom.audiochannel.mutedByDefault", false);
-
-user_pref("view_source.tab", true);
--- a/testing/testsuite-targets.mk
+++ b/testing/testsuite-targets.mk
@@ -168,17 +168,17 @@ endif
 # Usage: |make [EXTRA_TEST_ARGS=...] *test|.
 RUN_REFTEST = rm -f ./$@.log && $(PYTHON) _tests/reftest/runreftest.py \
   --extra-profile-file=$(DIST)/plugins \
   $(SYMBOLS_PATH) $(EXTRA_TEST_ARGS) $(1) | tee ./$@.log
 
 REMOTE_REFTEST = rm -f ./$@.log && $(PYTHON) _tests/reftest/remotereftest.py \
   --dm_trans=$(DM_TRANS) --ignore-window-size \
   --app=$(TEST_PACKAGE_NAME) --deviceIP=${TEST_DEVICE} --xre-path=${MOZ_HOST_BIN} \
-  --httpd-path=_tests/modules \
+  --httpd-path=_tests/modules --suite reftest \
   $(SYMBOLS_PATH) $(EXTRA_TEST_ARGS) $(1) | tee ./$@.log
 
 RUN_REFTEST_B2G = rm -f ./$@.log && $(PYTHON) _tests/reftest/runreftestb2g.py \
   --remote-webserver=10.0.2.2 --b2gpath=${B2G_PATH} --adbpath=${ADB_PATH} \
   --xre-path=${MOZ_HOST_BIN} $(SYMBOLS_PATH) --ignore-window-size \
   --httpd-path=_tests/modules \
   $(EXTRA_TEST_ARGS) '$(1)' | tee ./$@.log
 
--- a/toolkit/components/alerts/resources/content/alert.js
+++ b/toolkit/components/alerts/resources/content/alert.js
@@ -86,27 +86,29 @@ function onAlertLoad() {
   } else {
     moveWindowToEnd();
   }
 
   window.addEventListener("XULAlertClose", function() { window.close(); });
 
   if (Services.prefs.getBoolPref("alerts.disableSlidingEffect")) {
     setTimeout(function() { window.close(); }, ALERT_DURATION_IMMEDIATE);
-    return;
+  } else {
+    let alertBox = document.getElementById("alertBox");
+    alertBox.addEventListener("animationend", function hideAlert(event) {
+      if (event.animationName == "alert-animation") {
+        alertBox.removeEventListener("animationend", hideAlert, false);
+        window.close();
+      }
+    }, false);
+    alertBox.setAttribute("animate", true);
   }
 
-  let alertBox = document.getElementById("alertBox");
-  alertBox.addEventListener("animationend", function hideAlert(event) {
-    if (event.animationName == "alert-animation") {
-      alertBox.removeEventListener("animationend", hideAlert, false);
-      window.close();
-    }
-  }, false);
-  alertBox.setAttribute("animate", true);
+  let ev = new CustomEvent("AlertActive", {bubbles: true, cancelable: true});
+  document.documentElement.dispatchEvent(ev);
 
   if (gAlertListener) {
     gAlertListener.observe(null, "alertshow", gAlertCookie);
   }
 }
 
 function moveWindowToReplace(aReplacedAlert) {
   let heightDelta = window.outerHeight - aReplacedAlert.outerHeight;
--- a/toolkit/components/alerts/resources/content/alert.xul
+++ b/toolkit/components/alerts/resources/content/alert.xul
@@ -10,17 +10,17 @@
 
 <?xml-stylesheet href="chrome://global/content/alerts/alert.css" type="text/css"?>
 <?xml-stylesheet href="chrome://global/skin/alerts/alert.css" type="text/css"?>
 
 <window id="alertNotification"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         windowtype="alert:alert"
         xmlns:xhtml="http://www.w3.org/1999/xhtml"
-        xhtml:role="alert"
+        role="alert"
         pack="start"
         onload="onAlertLoad();"
         onclick="onAlertClick();"
         onbeforeunload="onAlertBeforeUnload();">
 
   <script type="application/javascript" src="chrome://global/content/alerts/alert.js"/>
 
   <box id="alertBox" class="alertBox">
--- a/toolkit/components/search/nsSearchService.js
+++ b/toolkit/components/search/nsSearchService.js
@@ -1250,37 +1250,32 @@ EngineURL.prototype = {
     if (!aJson.params)
       return;
 
     this.rels = aJson.rels;
 
     for (let i = 0; i < aJson.params.length; ++i) {
       let param = aJson.params[i];
       if (param.mozparam) {
-        if (param.condition == "defaultEngine") {
-          if (aEngine._isDefaultEngine())
-            this.addParam(param.name, param.trueValue);
-          else
-            this.addParam(param.name, param.falseValue);
-        } else if (param.condition == "pref") {
+        if (param.condition == "pref") {
           let value = getMozParamPref(param.pref);
           this.addParam(param.name, value);
         }
         this._addMozParam(param);
       }
       else
         this.addParam(param.name, param.value, param.purpose);
     }
   },
 
   /**
    * Creates a JavaScript object that represents this URL.
    * @returns An object suitable for serialization as JSON.
    **/
-  _serializeToJSON: function SRCH_EURL__serializeToJSON() {
+  toJSON: function SRCH_EURL_toJSON() {
     var json = {
       template: this.template,
       rels: this.rels,
       resultDomain: this.resultDomain
     };
 
     if (this.type != URLTYPE_SEARCH_HTML)
       json.type = this.type;
@@ -1394,18 +1389,16 @@ Engine.prototype = {
     return this.__file;
   },
   set _file(aValue) {
     this.__file = aValue;
   },
   // Set to true if the engine has a preferred icon (an icon that should not be
   // overridden by a non-preferred icon).
   _hasPreferredIcon: null,
-  // Whether the engine is hidden from the user.
-  _hidden: null,
   // The engine's name.
   _name: null,
   // The name of the charset used to submit the search terms.
   _queryCharset: null,
   // The engine's raw SearchForm value (URL string pointing to a search form).
   __searchForm: null,
   get _searchForm() {
     return this.__searchForm;
@@ -2036,29 +2029,16 @@ Engine.prototype = {
         switch (condition) {
           case "purpose":
             url.addParam(param.getAttribute("name"),
                          param.getAttribute("value"),
                          param.getAttribute("purpose"));
             // _addMozParam is not needed here since it can be serialized fine without. _addMozParam
             // also requires a unique "name" which is not normally the case when @purpose is used.
             break;
-          case "defaultEngine":
-            // If this engine was the default search engine, use the true value
-            if (this._isDefaultEngine())
-              value = param.getAttribute("trueValue");
-            else
-              value = param.getAttribute("falseValue");
-            url.addParam(param.getAttribute("name"), value);
-            url._addMozParam({"name": param.getAttribute("name"),
-                              "falseValue": param.getAttribute("falseValue"),
-                              "trueValue": param.getAttribute("trueValue"),
-                              "condition": "defaultEngine"});
-            break;
-
           case "pref":
             try {
               value = getMozParamPref(param.getAttribute("pref"), value);
               url.addParam(param.getAttribute("name"), value);
               url._addMozParam({"pref": param.getAttribute("pref"),
                                 "name": param.getAttribute("name"),
                                 "condition": "pref"});
             } catch (e) { }
@@ -2070,27 +2050,16 @@ Engine.prototype = {
           break;
         }
       }
     }
 
     this._urls.push(url);
   },
 
-  _isDefaultEngine: function SRCH_ENG__isDefaultEngine() {
-    let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
-    let nsIPLS = Ci.nsIPrefLocalizedString;
-    let defaultEngine;
-    let pref = getGeoSpecificPrefName("defaultenginename");
-    try {
-      defaultEngine = defaultPrefB.getComplexValue(pref, nsIPLS).data;
-    } catch (ex) {}
-    return this.name == defaultEngine;
-  },
-
   /**
    * Get the icon from an OpenSearch Image element.
    * @see http://opensearch.a9.com/spec/1.1/description/#image
    */
   _parseImage: function SRCH_ENG_parseImage(aElement) {
     LOG("_parseImage: Image textContent: \"" + limitURILength(aElement.textContent) + "\"");
 
     let width = parseInt(aElement.getAttribute("width"), 10);
@@ -2169,17 +2138,16 @@ Engine.prototype = {
   _initWithJSON: function SRCH_ENG__initWithJSON(aJson) {
     this.__id = aJson._id;
     this._name = aJson._name;
     this._description = aJson.description;
     if (aJson._hasPreferredIcon == undefined)
       this._hasPreferredIcon = true;
     else
       this._hasPreferredIcon = false;
-    this._hidden = aJson._hidden;
     this._queryCharset = aJson.queryCharset || DEFAULT_QUERY_CHARSET;
     this.__searchForm = aJson.__searchForm;
     this.__installLocation = aJson._installLocation || SEARCH_APP_DIR;
     this._updateInterval = aJson._updateInterval || null;
     this._updateURL = aJson._updateURL || null;
     this._iconUpdateURL = aJson._iconUpdateURL || null;
     if (aJson._readOnly == undefined)
       this._readOnly = true;
@@ -2197,50 +2165,46 @@ Engine.prototype = {
                                     url.resultDomain);
       engineURL._initWithJSON(url, this);
       this._urls.push(engineURL);
     }
   },
 
   /**
    * Creates a JavaScript object that represents this engine.
-   * @param aFilter
-   *        Whether or not to filter out common default values. Recommended for
-   *        use with _initWithJSON().
    * @returns An object suitable for serialization as JSON.
    **/
-  _serializeToJSON: function SRCH_ENG__serializeToJSON(aFilter) {
+  toJSON: function SRCH_ENG_toJSON() {
     var json = {
       _id: this._id,
       _name: this._name,
-      _hidden: this.hidden,
       description: this.description,
       __searchForm: this.__searchForm,
       _iconURL: this._iconURL,
       _iconMapObj: this._iconMapObj,
-      _urls: [url._serializeToJSON() for each(url in this._urls)]
+      _urls: this._urls
     };
 
     if (this._file instanceof Ci.nsILocalFile)
       json.filePath = this._file.persistentDescriptor;
     if (this._uri)
       json._url = this._uri.spec;
-    if (this._installLocation != SEARCH_APP_DIR || !aFilter)
+    if (this._installLocation != SEARCH_APP_DIR)
       json._installLocation = this._installLocation;
-    if (this._updateInterval || !aFilter)
+    if (this._updateInterval)
       json._updateInterval = this._updateInterval;
-    if (this._updateURL || !aFilter)
+    if (this._updateURL)
       json._updateURL = this._updateURL;
-    if (this._iconUpdateURL || !aFilter)
+    if (this._iconUpdateURL)
       json._iconUpdateURL = this._iconUpdateURL;
-    if (!this._hasPreferredIcon || !aFilter)
+    if (!this._hasPreferredIcon)
       json._hasPreferredIcon = this._hasPreferredIcon;
-    if (this.queryCharset != DEFAULT_QUERY_CHARSET || !aFilter)
+    if (this.queryCharset != DEFAULT_QUERY_CHARSET)
       json.queryCharset = this.queryCharset;
-    if (!this._readOnly || !aFilter)
+    if (!this._readOnly)
       json._readOnly = this._readOnly;
     if (this._extensionID) {
       json.extensionID = this._extensionID;
     }
 
     return json;
   },
 
@@ -2399,24 +2363,21 @@ Engine.prototype = {
     return this._identifier = leaf.substring(0, ext);
   },
 
   get description() {
     return this._description;
   },
 
   get hidden() {
-    if (this._hidden === null)
-      this._hidden = engineMetadataService.getAttr(this, "hidden") || false;
-    return this._hidden;
+    return engineMetadataService.getAttr(this, "hidden") || false;
   },
   set hidden(val) {
     var value = !!val;
-    if (value != this._hidden) {
-      this._hidden = value;
+    if (value != this.hidden) {
       engineMetadataService.setAttr(this, "hidden", value);
       notifyAction(this, SEARCH_ENGINE_CHANGED);
     }
   },
 
   get iconURI() {
     if (this._iconURI)
       return this._iconURI;
@@ -3127,17 +3088,17 @@ SearchService.prototype = {
 
       let cacheKey = parent.path;
       if (!cache.directories[cacheKey]) {
         let cacheEntry = {};
         cacheEntry.lastModifiedTime = parent.lastModifiedTime;
         cacheEntry.engines = [];
         cache.directories[cacheKey] = cacheEntry;
       }
-      cache.directories[cacheKey].engines.push(engine._serializeToJSON(true));
+      cache.directories[cacheKey].engines.push(engine);
     }
 
     try {
       LOG("_buildCache: Writing to cache file.");
       let path = OS.Path.join(OS.Constants.Path.profileDir, "search.json");
       let data = gEncoder.encode(JSON.stringify(cache));
       let promise = OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp"});
 
--- a/toolkit/components/search/tests/xpcshell/data/engine.xml
+++ b/toolkit/components/search/tests/xpcshell/data/engine.xml
@@ -6,22 +6,20 @@
 <Image width="16" height="16">data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA</Image>
 <Url type="application/x-suggestions+json" method="GET" template="http://suggestqueries.google.com/complete/search?output=firefox&amp;client=firefox&amp;hl={moz:locale}&amp;q={searchTerms}"/>
 <Url type="text/html" method="GET" template="http://www.google.com/search" resultdomain="google.com">
   <Param name="q" value="{searchTerms}"/>
   <Param name="ie" value="utf-8"/>
   <Param name="oe" value="utf-8"/>
   <Param name="aq" value="t"/>
   <!-- Dynamic parameters -->
-  <MozParam name="client" condition="defaultEngine" trueValue="firefox-a" falseValue="firefox"/>
   <MozParam name="channel" condition="purpose" purpose="contextmenu" value="rcs"/>
   <MozParam name="channel" condition="purpose" purpose="keyword" value="fflb"/>
 </Url>
 <Url type="application/x-moz-default-purpose" method="GET" template="http://www.google.com/search" resultdomain="purpose.google.com">
   <Param name="q" value="{searchTerms}"/>
-  <MozParam name="client" condition="defaultEngine" trueValue="firefox-a" falseValue="firefox"/>
   <!-- MozParam with a default value if purpose is not specified -->
   <MozParam name="channel" condition="purpose" purpose="" value="none"/>
   <MozParam name="channel" condition="purpose" purpose="contextmenu" value="rcs"/>
   <MozParam name="channel" condition="purpose" purpose="keyword" value="fflb"/>
 </Url>
 <SearchForm>http://www.google.com/</SearchForm>
 </SearchPlugin>
--- a/toolkit/components/search/tests/xpcshell/data/search.json
+++ b/toolkit/components/search/tests/xpcshell/data/search.json
@@ -4,17 +4,16 @@
   "locale": "en-US",
   "directories": {
     "[profile]/searchplugins": {
       "lastModifiedTime": 1333761316000,
       "engines": [
         {
           "_id": "[app]/test-search-engine.xml",
           "_name": "Test search engine",
-          "_hidden": false,
           "description": "A test search engine (based on Google search)",
           "extensionID": "test-addon-id@mozilla.org",
           "__searchForm": "http://www.google.com/",
           "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA",
           "_urls": [
             {
               "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}",
               "rels": [
@@ -41,23 +40,16 @@
                   "name": "oe",
                   "value": "utf-8"
                 },
                 {
                   "name": "aq",
                   "value": "t"
                 },
                 {
-                  "name": "client",
-                  "falseValue": "firefox",
-                  "trueValue": "firefox-a",
-                  "condition": "defaultEngine",
-                  "mozparam": true
-                },
-                {
                   "name": "channel",
                   "value": "fflb",
                   "purpose": "keyword"
                 },
                 {
                   "name": "channel",
                   "value": "rcs",
                   "purpose": "contextmenu"
@@ -71,23 +63,16 @@
               ],
               "type": "application/x-moz-default-purpose",
               "params": [
                 {
                   "name": "q",
                   "value": "{searchTerms}"
                 },
                 {
-                  "name": "client",
-                  "falseValue": "firefox",
-                  "trueValue": "firefox-a",
-                  "condition": "defaultEngine",
-                  "mozparam": true
-                },
-                {
                   "name": "channel",
                   "value": "none",
                   "purpose": ""
                 },
                 {
                   "name": "channel",
                   "value": "fflb",
                   "purpose": "keyword"
--- a/toolkit/components/search/tests/xpcshell/test_json_cache.js
+++ b/toolkit/components/search/tests/xpcshell/test_json_cache.js
@@ -269,79 +269,51 @@ var EXPECTED_ENGINE = {
               "purpose": undefined,
             },
             {
               "name": "aq",
               "value": "t",
               "purpose": undefined,
             },
             {
-              "name": "client",
-              "value": "firefox",
-              "purpose": undefined,
-            },
-            {
               "name": "channel",
               "value": "fflb",
               "purpose": "keyword",
             },
             {
               "name": "channel",
               "value": "rcs",
               "purpose": "contextmenu",
             },
           ],
-          mozparams: {
-            "client": {
-              "name": "client",
-              "falseValue": "firefox",
-              "trueValue": "firefox-a",
-              "condition": "defaultEngine",
-              "mozparam": true,
-            },
-          },
         },
         {
           type: "application/x-moz-default-purpose",
           method: "GET",
           template: "http://www.google.com/search",
           resultDomain: "purpose.google.com",
           params: [
             {
               "name": "q",
               "value": "{searchTerms}",
               "purpose": undefined,
             },
             {
-              "name": "client",
-              "value": "firefox",
-              "purpose": undefined,
-            },
-            {
               "name": "channel",
               "value": "none",
               "purpose": "",
             },
             {
               "name": "channel",
               "value": "fflb",
               "purpose": "keyword",
             },
             {
               "name": "channel",
               "value": "rcs",
               "purpose": "contextmenu",
             },
           ],
-          mozparams: {
-            "client": {
-              "name": "client",
-              "falseValue": "firefox",
-              "trueValue": "firefox-a",
-              "condition": "defaultEngine",
-              "mozparam": true,
-            },
-          },
         },
       ],
     },
   },
 };
--- a/toolkit/components/search/tests/xpcshell/test_purpose.js
+++ b/toolkit/components/search/tests/xpcshell/test_purpose.js
@@ -22,28 +22,28 @@ add_task(function* test_purpose() {
     { name: "Test search engine", xmlFileName: "engine.xml" },
   ]);
 
   function check_submission(aExpected, aSearchTerm, aType, aPurpose) {
     do_check_eq(engine.getSubmission(aSearchTerm, aType, aPurpose).uri.spec,
                 base + aExpected);
   }
 
-  let base = "http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t&client=firefox";
+  let base = "http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t";
   check_submission("",              "foo");
   check_submission("",              "foo", null);
   check_submission("",              "foo", "text/html");
   check_submission("&channel=rcs",  "foo", null,        "contextmenu");
   check_submission("&channel=rcs",  "foo", "text/html", "contextmenu");
   check_submission("&channel=fflb", "foo", null,        "keyword");
   check_submission("&channel=fflb", "foo", "text/html", "keyword");
   check_submission("",              "foo", "text/html", "invalid");
 
   // Tests for a param that varies with a purpose but has a default value.
-  base = "http://www.google.com/search?q=foo&client=firefox";
+  base = "http://www.google.com/search?q=foo";
   check_submission("&channel=none", "foo", "application/x-moz-default-purpose");
   check_submission("&channel=none", "foo", "application/x-moz-default-purpose", null);
   check_submission("&channel=none", "foo", "application/x-moz-default-purpose", "");
   check_submission("&channel=rcs",  "foo", "application/x-moz-default-purpose", "contextmenu");
   check_submission("&channel=fflb", "foo", "application/x-moz-default-purpose", "keyword");
   check_submission("",              "foo", "application/x-moz-default-purpose", "invalid");
 
   // Tests for a purpose on the search form (ie. empty query).
--- a/toolkit/components/viewsource/content/viewPartialSource.js
+++ b/toolkit/components/viewsource/content/viewPartialSource.js
@@ -11,12 +11,12 @@ function onLoadViewPartialSource() {
   // and set the menuitem's checked attribute accordingly
   let wrapLongLines = Services.prefs.getBoolPref("view_source.wrap_long_lines");
   document.getElementById("menu_wrapLongLines")
           .setAttribute("checked", wrapLongLines);
   document.getElementById("menu_highlightSyntax")
           .setAttribute("checked",
                         Services.prefs.getBoolPref("view_source.syntax_highlight"));
 
-  let args = window.arguments;
+  let args = window.arguments[0];
   viewSourceChrome.loadViewSourceFromSelection(args.URI, args.drawSelection, args.baseURI);
   window.content.focus();
 }
--- a/toolkit/components/viewsource/content/viewSource.js
+++ b/toolkit/components/viewsource/content/viewSource.js
@@ -213,16 +213,23 @@ ViewSourceChrome.prototype = {
    * the document source out of the network cache), we automatically re-load
    * the frame script.
    */
   get mm() {
     return window.messageManager;
   },
 
   /**
+   * Getter for the nsIWebNavigation of the view source browser.
+   */
+  get webNav() {
+    return this.browser.webNavigation;
+  },
+
+  /**
    * Send the browser forward in its history.
    */
   goForward() {
     this.browser.goForward();
   },
 
   /**
    * Send the browser backward in its history.
--- a/toolkit/components/viewsource/test/browser/browser_contextmenu.js
+++ b/toolkit/components/viewsource/test/browser/browser_contextmenu.js
@@ -3,44 +3,65 @@
  */
 
 var source = "data:text/html,text<link%20href='http://example.com/'%20/>more%20text<a%20href='mailto:abc@def.ghi'>email</a>";
 var gViewSourceWindow, gContextMenu, gCopyLinkMenuItem, gCopyEmailMenuItem;
 
 var expectedData = [];
 
 add_task(function *() {
+  // Full source in view source window
   let newWindow = yield loadViewSourceWindow(source);
   yield SimpleTest.promiseFocus(newWindow);
 
   yield* onViewSourceWindowOpen(newWindow, false);
 
   let contextMenu = gViewSourceWindow.document.getElementById("viewSourceContextMenu");
 
   for (let test of expectedData) {
     yield* checkMenuItems(contextMenu, false, test[0], test[1], test[2], test[3]);
   }
 
   yield new Promise(resolve => {
     closeViewSourceWindow(newWindow, resolve);
   });
 
+  // Selection source in view source tab
   expectedData = [];
   let newTab = yield openDocumentSelect(source, "body");
   yield* onViewSourceWindowOpen(window, true);
 
   contextMenu = document.getElementById("contentAreaContextMenu");
 
   // Prepend view-source to this one as it opens in a tab.
   expectedData[0][3] = "view-source:" + expectedData[0][3];
   for (let test of expectedData) {
     yield* checkMenuItems(contextMenu, true, test[0], test[1], test[2], test[3]);
   }
 
   gBrowser.removeTab(newTab);
+
+  // Selection source in view source window
+  yield pushPrefs(["view_source.tab", false]);
+
+  expectedData = [];
+  newWindow = yield openDocumentSelect(source, "body");
+  yield SimpleTest.promiseFocus(newWindow);
+
+  yield* onViewSourceWindowOpen(newWindow, false);
+
+  contextMenu = newWindow.document.getElementById("viewSourceContextMenu");
+
+  for (let test of expectedData) {
+    yield* checkMenuItems(contextMenu, false, test[0], test[1], test[2], test[3]);
+  }
+
+  yield new Promise(resolve => {
+    closeViewSourceWindow(newWindow, resolve);
+  });
 });
 
 function* onViewSourceWindowOpen(aWindow, aIsTab) {
   gViewSourceWindow = aWindow;
 
   gCopyLinkMenuItem = aWindow.document.getElementById(aIsTab ? "context-copylink" : "context-copyLink");
   gCopyEmailMenuItem = aWindow.document.getElementById(aIsTab ? "context-copyemail" : "context-copyEmail");
 
--- a/toolkit/components/viewsource/test/browser/head.js
+++ b/toolkit/components/viewsource/test/browser/head.js
@@ -1,15 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 Cu.import("resource://gre/modules/PromiseUtils.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 
+const WINDOW_TYPE = "navigator:view-source";
+
 function openViewSourceWindow(aURI, aCallback) {
   let viewSourceWindow = openDialog("chrome://global/content/viewSource.xul", null, null, aURI);
   viewSourceWindow.addEventListener("pageshow", function pageShowHandler(event) {
     // Wait for the inner window to load, not viewSourceWindow.
     if (event.target.location == "view-source:" + aURI) {
       info("View source window opened: " + event.target.location);
       viewSourceWindow.removeEventListener("pageshow", pageShowHandler, false);
       aCallback(viewSourceWindow);
@@ -35,43 +37,72 @@ function closeViewSourceWindow(aWindow, 
 
 function testViewSourceWindow(aURI, aTestCallback, aCloseCallback) {
   openViewSourceWindow(aURI, function(aWindow) {
     aTestCallback(aWindow);
     closeViewSourceWindow(aWindow, aCloseCallback);
   });
 }
 
+function waitForViewSourceWindow() {
+  return new Promise(resolve => {
+    let windowListener = {
+      onOpenWindow(xulWindow) {
+        let win = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                           .getInterface(Ci.nsIDOMWindow);
+        win.addEventListener("load", function listener() {
+          win.removeEventListener("load", listener, false);
+          if (win.document.documentElement.getAttribute("windowtype") !=
+              WINDOW_TYPE) {
+            return;
+          }
+          // Found the window
+          resolve(win);
+          Services.wm.removeListener(windowListener);
+        }, false);
+      },
+      onCloseWindow() {},
+      onWindowTitleChange() {}
+    };
+    Services.wm.addListener(windowListener);
+  });
+}
+
 /**
- * Opens a view source tab for a selection (View Selection Source) within the
- * currently selected browser in gBrowser.
+ * Opens a view source tab / window for a selection (View Selection Source)
+ * within the currently selected browser in gBrowser.
  *
  * @param aCSSSelector - used to specify a node within the selection to
  *                       view the source of. It is expected that this node is
  *                       within an existing selection.
- * @returns the new tab which shows the source.
+ * @returns the new tab / window which shows the source.
  */
-function* openViewPartialSourceTab(aCSSSelector) {
+function* openViewPartialSource(aCSSSelector) {
   let contentAreaContextMenuPopup =
     document.getElementById("contentAreaContextMenu");
   let popupShownPromise =
     BrowserTestUtils.waitForEvent(contentAreaContextMenuPopup, "popupshown");
   yield BrowserTestUtils.synthesizeMouseAtCenter(aCSSSelector,
           { type: "contextmenu", button: 2 }, gBrowser.selectedBrowser);
   yield popupShownPromise;
 
-  let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null);
+  let openPromise;
+  if (Services.prefs.getBoolPref("view_source.tab")) {
+    openPromise = BrowserTestUtils.waitForNewTab(gBrowser, null);
+  } else {
+    openPromise = waitForViewSourceWindow();
+  }
 
   let popupHiddenPromise =
     BrowserTestUtils.waitForEvent(contentAreaContextMenuPopup, "popuphidden");
   let item = document.getElementById("context-viewpartialsource-selection");
   EventUtils.synthesizeMouseAtCenter(item, {});
   yield popupHiddenPromise;
 
-  return (yield newTabPromise);
+  return (yield openPromise);
 }
 
 /**
  * Opens a view source tab for a frame (View Frame Source) within the
  * currently selected browser in gBrowser.
  *
  * @param aCSSSelector - used to specify the frame to view the source of.
  * @returns the new tab which shows the source.
@@ -98,61 +129,69 @@ function* openViewFrameSourceTab(aCSSSel
   let item = document.getElementById("context-viewframesource");
   EventUtils.synthesizeMouseAtCenter(item, {});
   yield popupHiddenPromise;
 
   return (yield newTabPromise);
 }
 
 registerCleanupFunction(function() {
-  var windows = Services.wm.getEnumerator("navigator:view-source");
+  var windows = Services.wm.getEnumerator(WINDOW_TYPE);
   ok(!windows.hasMoreElements(), "No remaining view source windows still open");
   while (windows.hasMoreElements())
     windows.getNext().close();
 });
 
 /**
- * For a given view source tab, wait for the source loading step to complete.
+ * For a given view source tab / window, wait for the source loading step to
+ * complete.
  */
-function waitForSourceLoaded(tab) {
+function waitForSourceLoaded(tabOrWindow) {
   return new Promise(resolve => {
-    let mm = tab.linkedBrowser.messageManager;
+    let mm = tabOrWindow.messageManager ||
+             tabOrWindow.linkedBrowser.messageManager;
     mm.addMessageListener("ViewSource:SourceLoaded", function sourceLoaded() {
       mm.removeMessageListener("ViewSource:SourceLoaded", sourceLoaded);
       setTimeout(resolve, 0);
     });
   });
 }
 
 /**
  * Open a new document in a new tab, select part of it, and view the source of
  * that selection. The document is not closed afterwards.
  *
  * @param aURI - url to load
  * @param aCSSSelector - used to specify a node to select. All of this node's
  *                       children will be selected.
- * @returns the new tab which shows the source.
+ * @returns the new tab / window which shows the source.
  */
 function* openDocumentSelect(aURI, aCSSSelector) {
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, aURI);
   registerCleanupFunction(function() {
     gBrowser.removeTab(tab);
   });
 
   yield ContentTask.spawn(gBrowser.selectedBrowser, { selector: aCSSSelector }, function* (arg) {
     let element = content.document.querySelector(arg.selector);
     content.getSelection().selectAllChildren(element);
   });
 
-  let newtab = yield openViewPartialSourceTab(aCSSSelector);
+  let tabOrWindow = yield openViewPartialSource(aCSSSelector);
 
   // Wait until the source has been loaded.
-  yield waitForSourceLoaded(newtab);
+  yield waitForSourceLoaded(tabOrWindow);
+
+  return tabOrWindow;
+}
 
-  return newtab;
+function pushPrefs(...aPrefs) {
+  return new Promise(resolve => {
+    SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve);
+  });
 }
 
 function waitForPrefChange(pref) {
   let deferred = PromiseUtils.defer();
   let observer = () => {
     Preferences.ignore(pref, observer);
     deferred.resolve();
   };
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -758,24 +758,24 @@
   <binding id="addon-generic"
            extends="chrome://mozapps/content/extensions/extensions.xml#addon-base">
     <content>
       <xul:hbox anonid="warning-container"
                 class="warning">
         <xul:image class="warning-icon"/>
         <xul:label anonid="warning" flex="1"/>
         <xul:label anonid="warning-link" class="text-link"/>
-        <xul:button anonid="warning-btn" class="button-link"/>
+        <xul:button anonid="warning-btn" class="button-link" hidden="true"/>
         <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
       </xul:hbox>
       <xul:hbox anonid="error-container"
                 class="error">
         <xul:image class="error-icon"/>
         <xul:label anonid="error" flex="1"/>
-        <xul:label anonid="error-link" class="text-link"/>
+        <xul:label anonid="error-link" class="text-link" hidden="true"/>
         <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
       </xul:hbox>
       <xul:hbox anonid="pending-container"
                 class="pending">
         <xul:image class="pending-icon"/>
         <xul:label anonid="pending" flex="1"/>
         <xul:button anonid="restart-btn" class="button-link"
                     label="&addon.restartNow.label;"
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -285,16 +285,17 @@ function loadLazyObjects() {
     AddonInternal,
     XPIProvider,
     XPIStates,
     syncLoadManifestFromFile,
     isUsableAddon,
     recordAddonTelemetry,
     applyBlocklistChanges,
     flushStartupCache,
+    canRunInSafeMode,
   }
 
   for (let key of Object.keys(shared))
     scope[key] = shared[key];
 
   Services.scriptloader.loadSubScript(uri, scope);
 
   for (let name of LAZY_OBJECTS) {
@@ -627,16 +628,31 @@ function applyBlocklistChanges(aOldAddon
   }
   else {
     // If the new add-on is not softblocked then it cannot be softDisabled
     aNewAddon.softDisabled = false;
   }
 }
 
 /**
+ * Evaluates whether an add-on is allowed to run in safe mode.
+ *
+ * @param  aAddon
+ *         The add-on to check
+ * @return true if the add-on should run in safe mode
+ */
+function canRunInSafeMode(aAddon) {
+  // Even though the updated system add-ons aren't generally run in safe mode we
+  // include them here so their uninstall functions get called when switching
+  // back to the default set.
+  return aAddon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS ||
+         aAddon._installLocation.name == KEY_APP_SYSTEM_ADDONS;
+}
+
+/**
  * Calculates whether an add-on should be appDisabled or not.
  *
  * @param  aAddon
  *         The add-on to check
  * @return true if the add-on should not be appDisabled
  */
 function isUsableAddon(aAddon) {
   // Hack to ensure the default theme is always usable
@@ -698,17 +714,18 @@ function EM_R(aProperty) {
   return gRDF.GetResource(PREFIX_NS_EM + aProperty);
 }
 
 function createAddonDetails(id, aAddon) {
   return {
     id: id || aAddon.id,
     type: aAddon.type,
     version: aAddon.version,
-    multiprocessCompatible: aAddon.multiprocessCompatible
+    multiprocessCompatible: aAddon.multiprocessCompatible,
+    runInSafeMode: aAddon.runInSafeMode,
   };
 }
 
 /**
  * Converts an internal add-on type to the type presented through the API.
  *
  * @param  aType
  *         The internal add-on type
@@ -2787,16 +2804,20 @@ this.XPIProvider = {
     ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
 
     // Ensure any changes to the add-ons list are flushed to disk
     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
                                !XPIDatabase.writeAddonsList());
   },
 
   updateSystemAddons: Task.async(function XPI_updateSystemAddons() {
+    // Don't do anything in safe mode
+    if (Services.appinfo.inSafeMode)
+      return;
+
     // Download the list of system add-ons
     let url = Preferences.get(PREF_SYSTEM_ADDON_UPDATE_URL, null);
     if (!url)
       return;
 
     url = UpdateUtils.formatUpdateURL(url);
 
     logger.info(`Starting system add-on update check from ${url}.`);
@@ -4166,26 +4187,30 @@ this.XPIProvider = {
    * @param  aFile
    *         The nsIFile for the add-on
    * @param  aVersion
    *         The add-on's version
    * @param  aType
    *         The type for the add-on
    * @param  aMultiprocessCompatible
    *         Boolean indicating whether the add-on is compatible with electrolysis.
+   * @param  aRunInSafeMode
+   *         Boolean indicating whether the add-on can run in safe mode.
    * @return a JavaScript scope
    */
   loadBootstrapScope: function XPI_loadBootstrapScope(aId, aFile, aVersion, aType,
-                                                      aMultiprocessCompatible) {
+                                                      aMultiprocessCompatible,
+                                                      aRunInSafeMode) {
     // Mark the add-on as active for the crash reporter before loading
     this.bootstrappedAddons[aId] = {
       version: aVersion,
       type: aType,
       descriptor: aFile.persistentDescriptor,
-      multiprocessCompatible: aMultiprocessCompatible
+      multiprocessCompatible: aMultiprocessCompatible,
+      runInSafeMode: aRunInSafeMode,
     };
     this.persistBootstrappedAddons();
     this.addAddonsToCrashReporter();
 
     // Locales only contain chrome and can't have bootstrap scripts
     if (aType == "locale") {
       this.bootstrapScopes[aId] = null;
       return;
@@ -4296,36 +4321,39 @@ this.XPIProvider = {
    *         The name of the bootstrap method to call
    * @param  aReason
    *         The reason flag to pass to the bootstrap's startup method
    * @param  aExtraParams
    *         An object of additional key/value pairs to pass to the method in
    *         the params argument
    */
   callBootstrapMethod: function XPI_callBootstrapMethod(aAddon, aFile, aMethod, aReason, aExtraParams) {
-    // Never call any bootstrap methods in safe mode
-    if (Services.appinfo.inSafeMode)
-      return;
-
     if (!aAddon.id || !aAddon.version || !aAddon.type) {
       logger.error(new Error("aAddon must include an id, version, and type"));
       return;
     }
 
+    // Only run in safe mode if allowed to
+    let runInSafeMode = "runInSafeMode" in aAddon ? aAddon.runInSafeMode : canRunInSafeMode(aAddon);
+    if (Services.appinfo.inSafeMode && !runInSafeMode)
+      return;
+
     let timeStart = new Date();
     if (CHROME_TYPES.has(aAddon.type) && aMethod == "startup") {
       logger.debug("Registering manifest for " + aFile.path);
       Components.manager.addBootstrappedManifestLocation(aFile);
     }
 
     try {
       // Load the scope if it hasn't already been loaded
-      if (!(aAddon.id in this.bootstrapScopes))
+      if (!(aAddon.id in this.bootstrapScopes)) {
         this.loadBootstrapScope(aAddon.id, aFile, aAddon.version, aAddon.type,
-                                aAddon.multiprocessCompatible || false);
+                                aAddon.multiprocessCompatible || false,
+                                runInSafeMode);
+      }
 
       // Nothing to call for locales
       if (aAddon.type == "locale")
         return;
 
       if (!(aMethod in this.bootstrapScopes[aAddon.id])) {
         logger.warn("Add-on " + aAddon.id + " is missing bootstrap method " + aMethod);
         return;
@@ -6809,19 +6837,21 @@ function AddonWrapper(aAddon) {
     return this.isActive && aAddon.bootstrap;
   });
 
   this.__defineGetter__("permissions", function AddonWrapper_permisionsGetter() {
     return aAddon.permissions();
   });
 
   this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() {
-    if (Services.appinfo.inSafeMode)
+    if (!aAddon.active)
       return false;
-    return aAddon.active;
+    if (!Services.appinfo.inSafeMode)
+      return true;
+    return aAddon.bootstrap && canRunInSafeMode(aAddon);
   });
 
   this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() {
     if (XPIProvider._enabledExperiments.has(aAddon.id)) {
       return false;
     }
 
     return aAddon.softDisabled || aAddon.userDisabled;
@@ -7520,16 +7550,20 @@ Object.assign(SystemAddonInstallLocation
   /**
    * Saves the current set of system add-ons
    */
   _saveAddonSet: function(aAddonSet) {
     Preferences.set(PREF_SYSTEM_ADDON_SET, JSON.stringify(aAddonSet));
   },
 
   getAddonLocations: function() {
+    // Updated system add-ons are ignored in safe mode
+    if (Services.appinfo.inSafeMode)
+      return new Map();
+
     let addons = DirectoryInstallLocation.prototype.getAddonLocations.call(this);
 
     // Strip out any unexpected add-ons from the list
     for (let id of addons.keys()) {
       if (!(id in this._addonSet.addons))
         addons.delete(id);
     }
 
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -2098,17 +2098,18 @@ this.XPIDatabaseReconcile = {
       currentAddon.active = isActive;
 
       // Make sure the bootstrap information is up to date for this ID
       if (currentAddon.bootstrap && currentAddon.active) {
         XPIProvider.bootstrappedAddons[id] = {
           version: currentAddon.version,
           type: currentAddon.type,
           descriptor: currentAddon._sourceBundle.persistentDescriptor,
-          multiprocessCompatible: currentAddon.multiprocessCompatible
+          multiprocessCompatible: currentAddon.multiprocessCompatible,
+          runInSafeMode: canRunInSafeMode(currentAddon),
         };
       }
 
       if (currentAddon.active && currentAddon.internalName == XPIProvider.selectedSkin)
         sawActiveTheme = true;
     }
 
     // Pass over the set of previously visible add-ons that have now gone away
--- a/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
@@ -165,16 +165,37 @@ add_task(function* test_updated() {
 
   startupManager(false);
 
   yield check_installed(true, null, "1.0", "1.0");
 
   yield promiseShutdownManager();
 });
 
+// Entering safe mode should disable the updated system add-ons and use the
+// default system add-ons
+add_task(function* safe_mode_disabled() {
+  gAppInfo.inSafeMode = true;
+  startupManager(false);
+
+  yield check_installed(false, "1.0", "1.0", null);
+
+  yield promiseShutdownManager();
+});
+
+// Leaving safe mode should re-enable the updated system add-ons
+add_task(function* normal_mode_enabled() {
+  gAppInfo.inSafeMode = false;
+  startupManager(false);
+
+  yield check_installed(true, null, "1.0", "1.0");
+
+  yield promiseShutdownManager();
+});
+
 // An additional add-on in the directory should be ignored
 add_task(function* test_skips_additional() {
   // Copy in the system add-ons
   let file = do_get_file("data/system_addons/system1_1.xpi");
   file.copyTo(featureDir, "system1@tests.mozilla.org.xpi");
 
   startupManager(false);
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update.js
@@ -439,16 +439,36 @@ add_task(function* test_app_update_disab
   ]));
   Services.prefs.clearUserPref(PREF_APP_UPDATE_ENABLED);
 
   yield verify_state(TEST_CONDITIONS.blank.initialState);
 
   yield promiseShutdownManager();
 });
 
+// Safe mode should block system add-on updates
+add_task(function* test_safe_mode() {
+  gAppInfo.inSafeMode = true;
+
+  yield setup_conditions(TEST_CONDITIONS.blank);
+
+  Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, false);
+  yield update_all_addons(yield build_xml([
+    { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
+    { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
+  ]));
+  Services.prefs.clearUserPref(PREF_APP_UPDATE_ENABLED);
+
+  yield verify_state(TEST_CONDITIONS.blank.initialState);
+
+  yield promiseShutdownManager();
+
+  gAppInfo.inSafeMode = false;
+});
+
 // Tests that a set that matches the default set does nothing
 add_task(function* test_match_default() {
   yield setup_conditions(TEST_CONDITIONS.withAppSet);
 
   yield install_system_addons(yield build_xml([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
   ]));