Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 18 Jun 2015 15:48:51 -0400
changeset 249559 d56a1257088ed44e2804f7fe7c615341954bd7de
parent 249525 c9a79ec162805cca965c7442a70396f17eef1b8f (current diff)
parent 249558 288cd0b9c9a3cad41633ea159d7107febf0a46e1 (diff)
child 249588 4829be6aa0bd1523a1b4ba894b3fc23fc7a3d15f
push id28929
push userryanvm@gmail.com
push dateThu, 18 Jun 2015 19:51:41 +0000
treeherdermozilla-central@d56a1257088e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone41.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 m-c. a=merge
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1404,18 +1404,18 @@ pref("devtools.command-button-rulers.ena
 // Inspector preferences
 // Enable the Inspector
 pref("devtools.inspector.enabled", true);
 // What was the last active sidebar in the inspector
 pref("devtools.inspector.activeSidebar", "ruleview");
 // Enable the markup preview
 pref("devtools.inspector.markupPreview", false);
 pref("devtools.inspector.remote", false);
-// Expand pseudo-elements by default in the rule-view
-pref("devtools.inspector.show_pseudo_elements", true);
+// Collapse pseudo-elements by default in the rule-view
+pref("devtools.inspector.show_pseudo_elements", false);
 // The default size for image preview tooltips in the rule-view/computed-view/markup-view
 pref("devtools.inspector.imagePreviewTooltipSize", 300);
 // Enable user agent style inspection in rule-view
 pref("devtools.inspector.showUserAgentStyles", false);
 // Show all native anonymous content (like controls in <video> tags)
 pref("devtools.inspector.showAllAnonymousContent", false);
 // Enable the MDN docs tooltip
 pref("devtools.inspector.mdnDocsTooltip.enabled", true);
@@ -1831,16 +1831,19 @@ pref("identity.fxaccounts.remote.webchan
 pref("identity.fxaccounts.settings.uri", "https://accounts.firefox.com/settings");
 
 // The remote URL of the FxA Profile Server
 pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
 
 // The remote URL of the FxA OAuth Server
 pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
 
+// Whether we display profile images in the UI or not.
+pref("identity.fxaccounts.profile_image.enabled", true);
+
 // Migrate any existing Firefox Account data from the default profile to the
 // Developer Edition profile.
 #ifdef MOZ_DEV_EDITION
 pref("identity.fxaccounts.migrateToDevEdition", true);
 #else
 pref("identity.fxaccounts.migrateToDevEdition", false);
 #endif
 
--- a/browser/base/content/browser-fullScreen.js
+++ b/browser/base/content/browser-fullScreen.js
@@ -85,16 +85,22 @@ var FullScreen = {
       // mozfullscreenchange event fired, which could confuse content script.
       this.hideNavToolbox(document.mozFullScreen);
     }
     else {
       this.showNavToolbox(false);
       // This is needed if they use the context menu to quit fullscreen
       this._isPopupOpen = false;
       this.cleanup();
+      // In TabsInTitlebar._update(), we cancel the appearance update on
+      // resize event for exiting fullscreen, since that happens before we
+      // change the UI here in the "fullscreen" event. Hence we need to
+      // call it here to ensure the appearance is properly updated. See
+      // TabsInTitlebar._update() and bug 1173768.
+      TabsInTitlebar.updateAppearance(true);
     }
   },
 
   exitDomFullScreen : function() {
     document.mozCancelFullScreen();
   },
 
   handleEvent: function (event) {
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -5061,17 +5061,25 @@ var TabsInTitlebar = {
       // _update is called on resize events, because the window is not ready
       // after sizemode events. However, we only care about the event when the
       // sizemode is different from the last time we updated the appearance of
       // the tabs in the titlebar.
       let sizemode = document.documentElement.getAttribute("sizemode");
       if (this._lastSizeMode == sizemode) {
         return;
       }
+      let oldSizeMode = this._lastSizeMode;
       this._lastSizeMode = sizemode;
+      // Don't update right now if we are leaving fullscreen, since the UI is
+      // still changing in the consequent "fullscreen" event. Code there will
+      // call this function again when everything is ready.
+      // See browser-fullScreen.js: FullScreen.toggle and bug 1173768.
+      if (oldSizeMode == "fullscreen") {
+        return;
+      }
     }
 
     for (let something in this._disallowed) {
       allowed = false;
       break;
     }
 
     let titlebar = $("titlebar");
--- a/browser/base/content/newtab/newTab.css
+++ b/browser/base/content/newtab/newTab.css
@@ -496,16 +496,26 @@ input[type=button] {
   background-color: white;
   border-radius: 6px;
   filter: drop-shadow(0 0 1px rgba(0,0,0,0.4)) drop-shadow(0 3px 4px rgba(0,0,0,0.4));
   transition: all 200ms ease-in-out;
   transform-origin: top right;
   transform: translate(-30px, -20px) scale(0) translate(30px, 20px);
 }
 
+#newtab-customize-panel:-moz-locale-dir(rtl) {
+  transform-origin: 40px top 20px;
+}
+
+#newtab-customize-panel:-moz-locale-dir(rtl),
+#newtab-customize-panel-anchor:-moz-locale-dir(rtl) {
+  left: 15px;
+  right: auto;
+}
+
 #newtab-customize-panel[open="true"] {
   transform: translate(-30px, -20px) scale(1) translate(30px, 20px);
 }
 
 #newtab-customize-panel-anchor {
   width: 18px;
   height: 18px;
   background-color: white;
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -332,17 +332,17 @@ skip-if = buildapp == 'mulet' || e10s # 
 [browser_overflowScroll.js]
 [browser_pageInfo.js]
 skip-if = buildapp == 'mulet'
 [browser_page_style_menu.js]
 
 [browser_parsable_css.js]
 skip-if = e10s
 [browser_parsable_script.js]
-skip-if = asan # Disabled because it takes a long time (see test for more information)
+skip-if = asan || (os == 'linux' && !debug && (bits == 32)) # disabled on asan because of timeouts, and bug 1172468 for the linux 32-bit pgo issue.
 
 [browser_pinnedTabs.js]
 [browser_plainTextLinks.js]
 [browser_popupUI.js]
 skip-if = buildapp == 'mulet'
 [browser_popup_blocker.js]
 skip-if = (os == 'linux') || (e10s && debug) # Frequent bug 1081925 and bug 1125520 failures
 [browser_printpreview.js]
--- a/browser/base/content/test/newtab/browser_newtab_enhanced.js
+++ b/browser/base/content/test/newtab/browser_newtab_enhanced.js
@@ -3,17 +3,17 @@
 
 const PRELOAD_PREF = "browser.newtab.preload";
 
 let suggestedLink = {
   url: "http://example1.com/2",
   imageURI: "data:image/png;base64,helloWORLD3",
   title: "title2",
   type: "affiliate",
-  frecent_sites: ["classroom.google.com", "codecademy.com", "elearning.ut.ac.id", "khanacademy.org", "learn.jquery.com", "teamtreehouse.com", "tutorialspoint.com", "udacity.com", "w3cschool.cc", "w3schools.com"]
+  frecent_sites: ["classroom.google.com", "codeacademy.org", "codecademy.com", "codeschool.com", "codeyear.com", "elearning.ut.ac.id", "how-to-build-websites.com", "htmlcodetutorial.com", "htmldog.com", "htmlplayground.com", "learn.jquery.com", "quackit.com", "roseindia.net", "teamtreehouse.com", "tizag.com", "tutorialspoint.com", "udacity.com", "w3schools.com", "webdevelopersnotes.com"]
 };
 
 gDirectorySource = "data:application/json," + JSON.stringify({
   "enhanced": [{
     url: "http://example.com/",
     enhancedImageURI: "data:image/png;base64,helloWORLD",
     title: "title",
     type: "organic",
@@ -134,17 +134,17 @@ function runTests() {
   yield addNewTabPageTab();
   yield customizeNewTabPage("enhanced");
 
   // Suggested link was not enhanced by directory link with same domain
   ({type, enhanced, title, suggested} = getData(0));
   is(type, "affiliate", "suggested link is affiliate");
   is(enhanced, "", "suggested link has no enhanced image");
   is(title, "title2");
-  ok(suggested.indexOf("Suggested for <strong> Web Education </strong> visitors") > -1, "Suggested for 'Web Education'");
+  ok(suggested.indexOf("Suggested for <strong> webdev education </strong> visitors") > -1, "Suggested for 'webdev education'");
 
   // Enhanced history link shows up second
   ({type, enhanced, title, suggested} = getData(1));
   is(type, "enhanced", "pinned history link is enhanced");
   isnot(enhanced, "", "pinned history link has enhanced image");
   is(title, "title");
   is(suggested, "", "There is no suggested explanation");
 
@@ -171,19 +171,45 @@ function runTests() {
   yield watchLinksChangeOnce().then(TestRunner.next);
 
   yield addNewTabPageTab();
   ({type, enhanced, title, suggested} = getData(0));
   Cu.reportError("SUGGEST " + suggested);
   ok(suggested.indexOf("Suggested for <strong> Technology </strong> enthusiasts who visit sites like <strong> classroom.google.com </strong>") > -1, "Suggested for 'Technology' enthusiasts");
 
 
-    // Test server provided explanation string without category override.
+  // Test server provided explanation string without category override.
   delete suggestedLink.adgroup_name;
   Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE,
     "data:application/json," + encodeURIComponent(JSON.stringify({"suggested": [suggestedLink]})));
   yield watchLinksChangeOnce().then(TestRunner.next);
 
   yield addNewTabPageTab();
   ({type, enhanced, title, suggested} = getData(0));
   Cu.reportError("SUGGEST " + suggested);
-  ok(suggested.indexOf("Suggested for <strong> Web Education </strong> enthusiasts who visit sites like <strong> classroom.google.com </strong>") > -1, "Suggested for 'Web Education' enthusiasts");
+  ok(suggested.indexOf("Suggested for <strong> webdev education </strong> enthusiasts who visit sites like <strong> classroom.google.com </strong>") > -1, "Suggested for 'webdev education' enthusiasts");
+
+
+
+  // Test with xml entities in category name
+  suggestedLink.url = "http://example1.com/3";
+  suggestedLink.adgroup_name = ">angles< & \"quotes\'";
+  Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE,
+    "data:application/json," + encodeURIComponent(JSON.stringify({"suggested": [suggestedLink]})));
+  yield watchLinksChangeOnce().then(TestRunner.next);
+
+  yield addNewTabPageTab();
+  ({type, enhanced, title, suggested} = getData(0));
+  Cu.reportError("SUGGEST " + suggested);
+  ok(suggested.indexOf("Suggested for <strong> &gt;angles&lt; &amp; \"quotes\' </strong> enthusiasts who visit sites like <strong> classroom.google.com </strong>") > -1, "Suggested for 'xml entities' enthusiasts");
+
+
+  // Test with xml entities in explanation.
+  suggestedLink.explanation = "Testing junk explanation &<>\"'";
+  Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE,
+    "data:application/json," + encodeURIComponent(JSON.stringify({"suggested": [suggestedLink]})));
+  yield watchLinksChangeOnce().then(TestRunner.next);
+
+  yield addNewTabPageTab();
+  ({type, enhanced, title, suggested} = getData(0));
+  Cu.reportError("SUGGEST " + suggested);
+  ok(suggested.indexOf("Testing junk explanation &amp;&lt;&gt;\"'") > -1, "Junk test");
 }
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -725,16 +725,17 @@ loop.panel = (function(_, mozL10n) {
         if (!this.isMounted()) {
           return;
         }
 
         var previewImage = metadata.favicon || "";
         var description = metadata.title || metadata.description;
         var url = metadata.url;
         this.setState({
+          checked: false,
           previewImage: previewImage,
           description: description,
           url: url
         });
       }.bind(this));
     },
 
     onCheckboxChange: function(newState) {
@@ -770,17 +771,18 @@ loop.panel = (function(_, mozL10n) {
         context: true,
         hide: !hostname ||
           !this.props.mozLoop.getLoopPref("contextInConversations.enabled")
       });
 
       return (
         React.createElement("div", {className: "new-room-view"}, 
           React.createElement("div", {className: contextClasses}, 
-            React.createElement(Checkbox, {label: mozL10n.get("context_inroom_label"), 
+            React.createElement(Checkbox, {checked: this.state.checked, 
+                      label: mozL10n.get("context_inroom_label"), 
                       onChange: this.onCheckboxChange}), 
             React.createElement(sharedViews.ContextUrlView, {
               allowClick: false, 
               description: this.state.description, 
               showContextTitle: false, 
               thumbnail: this.state.previewImage, 
               url: this.state.url, 
               useDesktopPaths: true})
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -725,16 +725,17 @@ loop.panel = (function(_, mozL10n) {
         if (!this.isMounted()) {
           return;
         }
 
         var previewImage = metadata.favicon || "";
         var description = metadata.title || metadata.description;
         var url = metadata.url;
         this.setState({
+          checked: false,
           previewImage: previewImage,
           description: description,
           url: url
         });
       }.bind(this));
     },
 
     onCheckboxChange: function(newState) {
@@ -770,17 +771,18 @@ loop.panel = (function(_, mozL10n) {
         context: true,
         hide: !hostname ||
           !this.props.mozLoop.getLoopPref("contextInConversations.enabled")
       });
 
       return (
         <div className="new-room-view">
           <div className={contextClasses}>
-            <Checkbox label={mozL10n.get("context_inroom_label")}
+            <Checkbox checked={this.state.checked}
+                      label={mozL10n.get("context_inroom_label")}
                       onChange={this.onCheckboxChange} />
             <sharedViews.ContextUrlView
               allowClick={false}
               description={this.state.description}
               showContextTitle={false}
               thumbnail={this.state.previewImage}
               url={this.state.url}
               useDesktopPaths={true} />
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -659,24 +659,31 @@ loop.roomViews = (function(mozL10n) {
           if (this.state.mediaConnected) {
             // since the remoteVideo hasn't yet been enabled, if the
             // media is connected, then we should be displaying an avatar.
             return false;
           }
 
           return true;
 
+        case ROOM_STATES.READY:
+        case ROOM_STATES.INIT:
+        case ROOM_STATES.JOINING:
         case ROOM_STATES.SESSION_CONNECTED:
         case ROOM_STATES.JOINED:
+        case ROOM_STATES.MEDIA_WAIT:
           // this case is so that we don't show an avatar while waiting for
           // the other party to connect
           return true;
 
+        case ROOM_STATES.CLOSING:
+          return true;
+
         default:
-          console.warn("StandaloneRoomView.shouldRenderRemoteVideo:" +
+          console.warn("DesktopRoomConversationView.shouldRenderRemoteVideo:" +
             " unexpected roomState: ", this.state.roomState);
           return true;
       }
     },
 
     render: function() {
       if (this.state.roomName) {
         this.setTitle(this.state.roomName);
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -659,24 +659,31 @@ loop.roomViews = (function(mozL10n) {
           if (this.state.mediaConnected) {
             // since the remoteVideo hasn't yet been enabled, if the
             // media is connected, then we should be displaying an avatar.
             return false;
           }
 
           return true;
 
+        case ROOM_STATES.READY:
+        case ROOM_STATES.INIT:
+        case ROOM_STATES.JOINING:
         case ROOM_STATES.SESSION_CONNECTED:
         case ROOM_STATES.JOINED:
+        case ROOM_STATES.MEDIA_WAIT:
           // this case is so that we don't show an avatar while waiting for
           // the other party to connect
           return true;
 
+        case ROOM_STATES.CLOSING:
+          return true;
+
         default:
-          console.warn("StandaloneRoomView.shouldRenderRemoteVideo:" +
+          console.warn("DesktopRoomConversationView.shouldRenderRemoteVideo:" +
             " unexpected roomState: ", this.state.roomState);
           return true;
       }
     },
 
     render: function() {
       if (this.state.roomName) {
         this.setTitle(this.state.roomName);
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -255,18 +255,17 @@
 }
 
 .fx-embedded .no-video {
   background: black none repeat scroll 0% 0%;
   height: 100%;
   width: 100%;
 }
 
-.standalone .local-stream,
-.standalone .remote-inset-stream {
+.standalone .local-stream {
   /* required to have it superimposed to the control toolbar */
   z-index: 1001;
 }
 
 /* Side by side video elements */
 
 .conversation .media.side-by-side .focus-stream {
   width: 50%;
@@ -549,17 +548,17 @@
   /*
    * Expand to fill the available space, since there is no video any
    * intrinsic width. XXX should really change to an <img> for clarity
    */
   height: 100%;
   width: 100%;
 }
 
-.local .avatar {
+.conversation .local .avatar {
   position: absolute;
   z-index: 1;
 }
 
 .remote .avatar {
   /* make visually distinct from local avatar */
   opacity: 0.25;
 }
@@ -642,26 +641,25 @@
  * */
 html, .fx-embedded, #main,
 .video-layout-wrapper,
 .conversation {
   height: 100%;
 }
 
 /* We use 641px rather than 640, as min-width and max-width are inclusive */
-@media screen and (min-width:641px) {
-  .standalone .conversation-toolbar {
+@media screen and (min-width: 641px) {
+  .standalone .conversation .conversation-toolbar {
     position: absolute;
     bottom: 0;
     left: 0;
     right: 0;
   }
 
-  .standalone .local-stream,
-  .standalone .remote-inset-stream {
+  .standalone .conversation .local-stream {
     position: absolute;
     right: 15px;
     bottom: 15px;
     width: 20%;
     height: 20%;
     max-width: 400px;
     max-height: 300px;
   }
@@ -696,21 +694,17 @@ html, .fx-embedded, #main,
   .standalone .media {
     height: 90%;
   }
 
   .standalone .media.nested {
     min-height: 500px;
   }
 
-  .standalone .remote-inset-stream {
-    display: none;
-  }
-
-  .standalone .local-stream {
+  .standalone .conversation .local-stream {
     flex: 1;
     min-width: 120px;
     min-height: 150px;
     width: 100%;
   }
 
   /* Nested video elements */
   .standalone .conversation .media.nested {
@@ -748,39 +742,52 @@ html, .fx-embedded, #main,
   position: relative;
   height: 100%;
 }
 
 .room-conversation-wrapper header {
   background: #000;
   height: 50px;
   text-align: left;
-  width: 75%;
+  margin: 0 10px;
+}
+
+html[dir="rtl"] .room-conversation-wrapper header {
+  text-align: right;
 }
 
 .room-conversation-wrapper header h1 {
   font-size: 1.5em;
   color: #fff;
   line-height: 50px;
-  text-indent: 60px;
+  text-indent: 40px;
   background-image: url("../img/firefox-logo.png");
   background-size: 30px;
-  background-position: 20px;
+  background-position: 0 center;
   background-repeat: no-repeat;
   display: inline-block;
+  margin: 0 10px;
+}
+
+html[dir="rtl"] .room-conversation-wrapper header h1 {
+  background-position: 100% center;
 }
 
 .room-conversation-wrapper header a {
   float: right;
 }
 
+html[dir="rtl"] .room-conversation-wrapper header a {
+  float: left;
+}
+
 .room-conversation-wrapper header .icon-help {
   display: inline-block;
   background-size: contain;
-  margin-top: 20px;
+  margin-top: 15px;
   width: 20px;
   height: 20px;
   background: transparent url("../img/svg/glyph-help-16x16.svg") no-repeat;
 }
 
 .fx-embedded .room-conversation .conversation-toolbar .btn-hangup {
   background-image: url("../img/icons-16x16.svg#leave");
 }
@@ -880,16 +887,17 @@ body[platform="win"] .share-service-drop
 .dropdown-menu-item:hover:active > .icon-add-share-service {
   background-image: url("../img/icons-16x16.svg#add-active");
 }
 
 .context-url-view-wrapper {
   padding-left: 1em;
   padding-right: 1em;
   padding-bottom: 0.5em;
+  margin-bottom: 0.5em;
   background-color: #E8F6FE;
 }
 
 .room-context {
   background: rgba(0,0,0,.6);
   border-top: 2px solid #444;
   border-bottom: 2px solid #444;
   padding: .5rem;
@@ -1075,16 +1083,208 @@ html[dir="rtl"] .room-context-btn-edit {
   right: auto;
   left: 8px;
 }
 
 html[dir="rtl"] .room-context-btn-edit {
   left: 20px;
 }
 
+.media-layout {
+  /* 50px is the header, 3em is the footer. */
+  height: calc(100% - 50px - 3em);
+}
+
+.media-layout > .media-wrapper {
+  display: flex;
+  flex-flow: column wrap;
+  /* 64px for .conversation-toolbar */
+  height: calc(100% - 64px);
+  margin: 0 10px;
+}
+
+.media-wrapper > .focus-stream {
+  /* We want this to be the width, minus 200px which is for the right-side text
+     chat and video displays. */
+  width: calc(100% - 200px);
+  /* 100% height to fill up media-layout, thus forcing other elements into the
+     second column that's 200px wide */
+  height: 100%;
+  background-color: #4E4E4E;
+}
+
+.media-wrapper > .remote {
+  /* Works around an issue with object-fit: cover in Google Chrome - it doesn't
+     currently crop but overlaps the surrounding elements.
+     https://code.google.com/p/chromium/issues/detail?id=400829 */
+  overflow: hidden;
+}
+
+.media-wrapper > .remote > .remote-video {
+  object-fit: cover;
+}
+
+/* Note: we can't use flex for the text-chat-view as this lets it overflow
+   the expected column heights, and we ca't fix its height. */
+.media-wrapper > .text-chat-view {
+  flex: 0 0 auto;
+  /* Text chat is a fixed 200px width for normal displays. */
+  width: 200px;
+  height: 100%;
+}
+
+.media-wrapper.showing-local-streams > .text-chat-view {
+  /* When we're displaying the local streams, then we need to make the text
+     chat view a bit shorter to give room. */
+  height: calc(100% - 150px);
+}
+
+.media-wrapper.showing-local-streams.receiving-screen-share > .text-chat-view {
+  /* When we're displaying the local streams, then we need to make the text
+     chat view a bit shorter to give room. */
+  height: calc(100% - 300px);
+}
+
+.media-wrapper > .text-chat-view > .text-chat-entries {
+  /* 40px is the height of .text-chat-box. */
+  height: calc(100% - 40px);
+}
+
+.media-wrapper > .local {
+  flex: 0 1 auto;
+  width: 200px;
+  height: 150px;
+}
+
+.media-wrapper.receiving-screen-share > .screen {
+  order: 1;
+}
+
+.media-wrapper.receiving-screen-share > .text-chat-view {
+  order: 2;
+}
+
+.media-wrapper.receiving-screen-share > .remote {
+  order: 3;
+  flex: 0 1 auto;
+  width: 200px;
+  height: 150px;
+}
+
+.media-wrapper.receiving-screen-share > .local {
+  order: 4;
+}
+
+@media screen and (max-width:640px) {
+  .media-layout {
+    /* 50px is height of header, 25px is height of footer. */
+    height: calc(100% - 50px - 25px);
+  }
+
+  .media-layout > .media-wrapper {
+    flex-direction: row;
+    margin: 0;
+    width: 100%;
+    /* conversation toolbar is 38px in narrow mode */
+    height: calc(100% - 38px);
+  }
+
+  .media-wrapper > .focus-stream {
+    width: 100%;
+    /* A reasonable height */
+    height: 70%;
+  }
+
+  .media-wrapper.receiving-screen-share > .focus-stream {
+    height: 50%;
+  }
+
+  .media-wrapper > .text-chat-view > .text-chat-entries {
+    /* 40px is the height of .text-chat-box. */
+    height: calc(100% - 40px);
+    width: 100%;
+  }
+
+  .media-wrapper > .local {
+    /* Position over the remote video */
+    position: absolute;
+    /* Make sure its on top */
+    z-index: 1001;
+    margin: 3px;
+    right: 0;
+    /* 29px is (30% of 50px high header) + (height toolbar (38px) +
+       height footer (25px) - height header (50px)) */
+    bottom: calc(30% + 29px);
+    width: 120px;
+    height: 120px;
+  }
+
+  html[dir="rtl"] .media-wrapper > .local {
+    right: auto;
+    left: 0;
+  }
+
+  .media-wrapper > .text-chat-view {
+    order: 3;
+    flex: 1 1 auto;
+    width: 100%;
+  }
+
+  .media-wrapper > .text-chat-view,
+  .media-wrapper.showing-local-streams > .text-chat-view,
+  .media-wrapper.showing-local-streams.receiving-screen-share > .text-chat-view {
+    /* The remaining 30% that the .focus-stream doesn't use. */
+    height: 30%;
+  }
+
+  .media-wrapper.receiving-screen-share > .screen {
+    order: 1;
+  }
+
+  .media-wrapper.receiving-screen-share > .remote {
+    /* Screen shares have remote & local video side-by-side on narrow screens */
+    order: 2;
+    flex: 1 1 auto;
+    height: 20%;
+    /* Ensure no previously specified widths take effect, and we take up no more
+       than half the width. */
+    width: auto;
+    max-width: 50%;
+  }
+
+  .media-wrapper.receiving-screen-share > .remote > .remote-video {
+      /* Reset the object-fit for this. */
+    object-fit: contain;
+  }
+
+  .media-wrapper.receiving-screen-share > .local {
+    /* Screen shares have remote & local video side-by-side on narrow screens */
+    order: 3;
+    flex: 1 1 auto;
+    height: 20%;
+    /* Ensure no previously specified widths take effect, and we take up no more
+       than half the width. */
+    width: auto;
+    max-width: 50%;
+    /* This cancels out the absolute positioning when it's just remote video. */
+    position: relative;
+    bottom: auto;
+    right: auto;
+    margin: 0;
+  }
+
+  .media-wrapper.receiving-screen-share > .text-chat-view {
+    order: 4;
+  }
+}
+
+.standalone > #main > .room-conversation-wrapper > .media-layout > .conversation-toolbar {
+  border: none;
+}
+
 /* Standalone rooms */
 
 .standalone .room-conversation-wrapper {
   position: relative;
   height: 100%;
   background: #000;
 }
 
@@ -1103,16 +1303,21 @@ html[dir="rtl"] .room-context-btn-edit {
   left: 25%;
   z-index: 1000;
   /* `width` here is specified by the design spec. */
   width: 250px;
   color: #fff;
   box-sizing: content-box;
 }
 
+html[dir="rtl"] .standalone .room-conversation-wrapper .room-inner-info-area {
+  right: 25%;
+  left: auto;
+}
+
 .standalone .prompt-media-message {
   padding-top: 136px; /* Fallback for browsers that don't support calc() */
   /* 122px is 2x the intrinsic height of the background-image, and
      1rem puts one line of margin between the background-image and
      supporting text. */
   padding-top: calc(122px + 1rem);
   color: #000;
   background-color: #fff;
@@ -1148,33 +1353,16 @@ html[dir="rtl"] .room-context-btn-edit {
 
 .standalone .room-conversation-wrapper .room-inner-info-area a.btn {
   padding: .5em 3em .3em 3em;
   border-radius: 3px;
   font-weight: normal;
   max-width: 400px;
 }
 
-.standalone-room-info {
-  position: absolute;
-  display: block;
-  top: 0;
-  right: 10px;
-  /* 20px is 10px for left and right margins. */
-  width: calc(25% - 20px);
-  z-index: 2000000;
-  font-size: 1.2em;
-  padding: .4em;
-  height: 100%;
-}
-
-.standalone-room-info > h2 {
-  color: #fff;
-}
-
 .standalone-context-url {
   color: #fff;
   /* Try and keep clear of local video */
   height: 40%;
 }
 
 .standalone-context-url.screen-share-active {
   /* Try and keep clear of remote video when screensharing */
@@ -1238,17 +1426,17 @@ html[dir="rtl"] .room-context-btn-edit {
 
 .fx-embedded .text-chat-entries {
   flex: 1 1 auto;
   max-height: 120px;
   min-height: 60px;
   padding: .7em .5em 0;
 }
 
-.fx-embedded .text-chat-box {
+.text-chat-box {
   flex: 0 0 auto;
   max-height: 40px;
   min-height: 40px;
   width: 100%;
 }
 
 .text-chat-entries {
   overflow: scroll;
@@ -1304,43 +1492,27 @@ html[dir="rtl"] .room-context-btn-edit {
 }
 
 .fx-embedded .text-chat-box > form > input {
   border: 0;
   border-top: 1px solid #999;
 }
 
 @media screen and (max-width:640px) {
-  .standalone-room-info {
-    /* This isn't perfect, we just center the heading for now. Bug 1141493
-       should fix this. */
-    position: absolute;
-    width: 100%;
-    right: 0px;
-
-    /* Override the 100% specified in the .standalone-room-info selector
-       block so that this div doesn't take over the _whole_ screen and
-       transparently occlude UI widgetry (like the Join button), making
-       it unusable. */
-    height: auto;
-  }
-
   .standalone-context-url {
     /* XXX We haven't got UX for standalone yet, so temporarily not displaying
        on narrow window widths. See bug 1153827. */
     display: none;
   }
 
   /* Rooms specific responsive styling */
   .standalone .room-conversation {
     background: #000;
   }
-  .room-conversation-wrapper header {
-    width: 100%;
-  }
+
   .standalone .room-conversation-wrapper .room-inner-info-area {
     right: 0;
     margin: auto;
     width: 100%;
     left: 0;
   }
   .standalone .room-conversation-wrapper .video-layout-wrapper {
     /* 50px: header's height; 25px: footer's height */
@@ -1352,17 +1524,17 @@ html[dir="rtl"] .room-context-btn-edit {
   .standalone .room-conversation .video_wrapper.remote_wrapper.not-joined {
     width: 100%;
   }
 
   .standalone .conversation-toolbar {
     height: 38px;
     padding: 8px;
   }
-  .standalone .focus-stream {
+  .standalone .conversation .focus-stream {
     /* Set at maximum height, minus height of conversation toolbar */
     height: 100%;
   }
 
   .standalone .media.nested {
     /* This forces the remote video stream to fit within wrapper's height */
     min-height: 0px;
   }
@@ -1413,16 +1585,14 @@ html[dir="rtl"] .room-context-btn-edit {
      convention in video conferencing systems. */
   transform: scale(-1, 1);
   transform-origin: 50% 50% 0;
 }
 
 .remote-video {
   width: 100%;
   height: 100%;
-  display: block;
-  position: absolute;
 }
 
 .screen-share-video {
   width: 100%;
   height: 100%;
 }
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -164,16 +164,17 @@ loop.shared.actions = (function() {
      */
     RemotePeerConnected: Action.define("remotePeerConnected", {
     }),
 
     /**
      * Used to notify that the session has a data channel available.
      */
     DataChannelsAvailable: Action.define("dataChannelsAvailable", {
+      available: Boolean
     }),
 
     /**
      * Used to send a message to the other peer.
      */
     SendTextChatMessage: Action.define("sendTextChatMessage", {
       contentType: String,
       message: String
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -282,26 +282,18 @@ loop.shared.mixins = (function() {
     }
   };
 
   /**
    * Media setup mixin. Provides a common location for settings for the media
    * elements and handling updates of the media containers.
    */
   var MediaSetupMixin = {
-
     componentDidMount: function() {
       this.resetDimensionsCache();
-      rootObject.addEventListener("orientationchange", this.updateVideoContainer);
-      rootObject.addEventListener("resize", this.updateVideoContainer);
-    },
-
-    componentWillUnmount: function() {
-      rootObject.removeEventListener("orientationchange", this.updateVideoContainer);
-      rootObject.removeEventListener("resize", this.updateVideoContainer);
     },
 
     /**
      * Resets the dimensions cache, e.g. for when the session is ended, and
      * before a new session, so that we always ensure we see an update when a
      * new session is started.
      */
     resetDimensionsCache: function() {
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -269,16 +269,20 @@ loop.OTSdkDriver = (function() {
     },
 
     /**
      * Disconnects the sdk session.
      */
     disconnectSession: function() {
       this.endScreenShare();
 
+      this.dispatcher.dispatch(new sharedActions.DataChannelsAvailable({
+        available: false
+      }));
+
       if (this.session) {
         this.session.off("sessionDisconnected streamCreated streamDestroyed connectionCreated connectionDestroyed streamPropertyChanged");
         this.session.disconnect();
         delete this.session;
 
         this._notifyMetricsEvent("Session.connectionDestroyed", "local");
       }
       if (this.publisher) {
@@ -290,16 +294,18 @@ loop.OTSdkDriver = (function() {
       this._noteConnectionLengthIfNeeded(this._getTwoWayMediaStartTime(), performance.now());
 
       // Also, tidy these variables ready for next time.
       delete this._sessionConnected;
       delete this._publisherReady;
       delete this._publishedLocalStream;
       delete this._subscribedRemoteStream;
       delete this._mockPublisherEl;
+      delete this._publisherChannel;
+      delete this._subscriberChannel;
       this.connections = {};
       this._setTwoWayMediaStartTime(this.CONNECTION_START_TIME_UNINITIALIZED);
     },
 
     /**
      * Oust all users from an ongoing session. This is typically done when a room
      * owner deletes the room.
      *
@@ -718,17 +724,19 @@ loop.OTSdkDriver = (function() {
     },
 
     /**
      * Checks to see if all channels have been obtained, and if so it dispatches
      * a notification to the stores to inform them.
      */
     _checkDataChannelsAvailable: function() {
       if (this._publisherChannel && this._subscriberChannel) {
-        this.dispatcher.dispatch(new sharedActions.DataChannelsAvailable());
+        this.dispatcher.dispatch(new sharedActions.DataChannelsAvailable({
+          available: true
+        }));
       }
     },
 
     /**
      * Sends a text chat message on the data channel.
      *
      * @param {String} message The message to send.
      */
@@ -816,16 +824,20 @@ loop.OTSdkDriver = (function() {
      *
      * @param {StreamEvent} event The event details:
      * https://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
      */
     _onRemoteStreamDestroyed: function(event) {
       this._notifyMetricsEvent("Session.streamDestroyed");
 
       if (event.stream.videoType !== "screen") {
+        this.dispatcher.dispatch(new sharedActions.DataChannelsAvailable({
+          available: false
+        }));
+        delete this._subscriberChannel;
         delete this._mockSubscribeEl;
         return;
       }
 
       // All we need to do is notify the store we're no longer receiving,
       // the sdk should do the rest.
       this.dispatcher.dispatch(new sharedActions.ReceivingScreenShare({
         receiving: false
@@ -834,16 +846,20 @@ loop.OTSdkDriver = (function() {
       delete this._mockScreenShareEl;
     },
 
     /**
      * Handles the event when the remote stream is destroyed.
      */
     _onLocalStreamDestroyed: function() {
       this._notifyMetricsEvent("Publisher.streamDestroyed");
+      this.dispatcher.dispatch(new sharedActions.DataChannelsAvailable({
+        available: false
+      }));
+      delete this._publisherChannel;
       delete this._mockPublisherEl;
     },
 
     /**
      * Called from the sdk when the media access dialog is opened.
      * Prevents the default action, to prevent the SDK's "allow access"
      * dialog from being shown.
      *
--- a/browser/components/loop/content/shared/js/textChatStore.js
+++ b/browser/components/loop/content/shared/js/textChatStore.js
@@ -1,16 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var loop = loop || {};
 loop.store = loop.store || {};
 
-loop.store.TextChatStore = (function(mozL10n) {
+loop.store.TextChatStore = (function() {
   "use strict";
 
   var sharedActions = loop.shared.actions;
 
   var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES = {
     RECEIVED: "recv",
     SENT: "sent",
     SPECIAL: "special"
@@ -64,20 +64,25 @@ loop.store.TextChatStore = (function(moz
         messageList: [],
         length: 0
       };
     },
 
     /**
      * Handles information for when data channels are available - enables
      * text chat.
+     *
+     * @param {sharedActions.DataChannelsAvailable} actionData
      */
-    dataChannelsAvailable: function() {
-      this.setStoreState({ textChatEnabled: true });
-      window.dispatchEvent(new CustomEvent("LoopChatEnabled"));
+    dataChannelsAvailable: function(actionData) {
+      this.setStoreState({ textChatEnabled: actionData.available });
+
+      if (actionData.available) {
+        window.dispatchEvent(new CustomEvent("LoopChatEnabled"));
+      }
     },
 
     /**
      * Appends a message to the store, which may be of type 'sent' or 'received'.
      *
      * @param {CHAT_MESSAGE_TYPES} type
      * @param {Object} messageData Data for this message. Options are:
      * - {CHAT_CONTENT_TYPES} contentType
@@ -132,32 +137,34 @@ loop.store.TextChatStore = (function(moz
      * Handles receiving information about the room - specifically the room name
      * so it can be added to the list.
      *
      * @param  {sharedActions.UpdateRoomInfo} actionData
      */
     updateRoomInfo: function(actionData) {
       // XXX When we add special messages to desktop, we'll need to not post
       // multiple changes of room name, only the first. Bug 1171940 should fix this.
-      this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, {
-        contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
-        message: mozL10n.get("rooms_welcome_title", {conversationName: actionData.roomName})
-      });
+      if (actionData.roomName) {
+        this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, {
+          contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
+          message: actionData.roomName
+        });
+      }
 
       // Append the context if we have any.
-      if ("urls" in actionData && actionData.urls.length) {
+      if (("urls" in actionData) && actionData.urls && actionData.urls.length) {
         // We only support the first url at the moment.
         var urlData = actionData.urls[0];
 
         this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, {
           contentType: CHAT_CONTENT_TYPES.CONTEXT,
           message: urlData.description,
           extraData: {
             location: urlData.location,
             thumbnail: urlData.thumbnail
           }
         });
       }
     }
   });
 
   return TextChatStore;
-})(navigator.mozL10n || window.mozL10n);
+})();
--- a/browser/components/loop/content/shared/js/textChatView.js
+++ b/browser/components/loop/content/shared/js/textChatView.js
@@ -34,16 +34,32 @@ loop.shared.views.TextChatView = (functi
       return (
         React.createElement("div", {className: classes}, 
           React.createElement("p", null, this.props.message)
         )
       );
     }
   });
 
+  var TextChatRoomName = React.createClass({displayName: "TextChatRoomName",
+    mixins: [React.addons.PureRenderMixin],
+
+    propTypes: {
+      message: React.PropTypes.string.isRequired
+    },
+
+    render: function() {
+      return (
+        React.createElement("div", {className: "text-chat-entry special room-name"}, 
+          React.createElement("p", null, mozL10n.get("rooms_welcome_title", {conversationName: this.props.message}))
+        )
+      );
+    }
+  });
+
   /**
    * Manages the text entries in the chat entries view. This is split out from
    * TextChatView so that scrolling can be managed more efficiently - this
    * component only updates when the message list is changed.
    */
   var TextChatEntriesView = React.createClass({displayName: "TextChatEntriesView",
     mixins: [React.addons.PureRenderMixin],
 
@@ -76,31 +92,38 @@ loop.shared.views.TextChatView = (functi
         return null;
       }
 
       return (
         React.createElement("div", {className: "text-chat-entries"}, 
           React.createElement("div", {className: "text-chat-scroller"}, 
             
               this.props.messageList.map(function(entry, i) {
-                if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL &&
-                    entry.contentType === CHAT_CONTENT_TYPES.CONTEXT) {
-                  return (
-                    React.createElement("div", {className: "context-url-view-wrapper"}, 
-                      React.createElement(sharedViews.ContextUrlView, {
-                        allowClick: true, 
-                        description: entry.message, 
-                        dispatcher: this.props.dispatcher, 
-                        key: i, 
-                        showContextTitle: true, 
-                        thumbnail: entry.extraData.thumbnail, 
-                        url: entry.extraData.location, 
-                        useDesktopPaths: false})
-                    )
-                  );
+                if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) {
+                  switch (entry.contentType) {
+                    case CHAT_CONTENT_TYPES.ROOM_NAME:
+                      return React.createElement(TextChatRoomName, {message: entry.message});
+                    case CHAT_CONTENT_TYPES.CONTEXT:
+                      return (
+                        React.createElement("div", {className: "context-url-view-wrapper"}, 
+                          React.createElement(sharedViews.ContextUrlView, {
+                            allowClick: true, 
+                            description: entry.message, 
+                            dispatcher: this.props.dispatcher, 
+                            key: i, 
+                            showContextTitle: true, 
+                            thumbnail: entry.extraData.thumbnail, 
+                            url: entry.extraData.location, 
+                            useDesktopPaths: false})
+                        )
+                      );
+                    default:
+                      console.error("Unsupported contentType", entry.contentType);
+                      return null;
+                  }
                 }
 
                 return (
                   React.createElement(TextChatEntry, {key: i, 
                                  contentType: entry.contentType, 
                                  message: entry.message, 
                                  type: entry.type})
                 );
@@ -153,16 +176,21 @@ loop.shared.views.TextChatView = (functi
     /**
      * Handles submitting of the form - dispatches a send text chat message.
      *
      * @param {Object} event The DOM event.
      */
     handleFormSubmit: function(event) {
       event.preventDefault();
 
+      // Don't send empty messages.
+      if (!this.state.messageDetail) {
+        return;
+      }
+
       this.props.dispatcher.dispatch(new sharedActions.SendTextChatMessage({
         contentType: CHAT_CONTENT_TYPES.TEXT,
         message: this.state.messageDetail
       }));
 
       // Reset the form to empty, ready for the next message.
       this.setState({ messageDetail: "" });
     },
--- a/browser/components/loop/content/shared/js/textChatView.jsx
+++ b/browser/components/loop/content/shared/js/textChatView.jsx
@@ -34,16 +34,32 @@ loop.shared.views.TextChatView = (functi
       return (
         <div className={classes}>
           <p>{this.props.message}</p>
         </div>
       );
     }
   });
 
+  var TextChatRoomName = React.createClass({
+    mixins: [React.addons.PureRenderMixin],
+
+    propTypes: {
+      message: React.PropTypes.string.isRequired
+    },
+
+    render: function() {
+      return (
+        <div className="text-chat-entry special room-name">
+          <p>{mozL10n.get("rooms_welcome_title", {conversationName: this.props.message})}</p>
+        </div>
+      );
+    }
+  });
+
   /**
    * Manages the text entries in the chat entries view. This is split out from
    * TextChatView so that scrolling can be managed more efficiently - this
    * component only updates when the message list is changed.
    */
   var TextChatEntriesView = React.createClass({
     mixins: [React.addons.PureRenderMixin],
 
@@ -76,31 +92,38 @@ loop.shared.views.TextChatView = (functi
         return null;
       }
 
       return (
         <div className="text-chat-entries">
           <div className="text-chat-scroller">
             {
               this.props.messageList.map(function(entry, i) {
-                if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL &&
-                    entry.contentType === CHAT_CONTENT_TYPES.CONTEXT) {
-                  return (
-                    <div className="context-url-view-wrapper">
-                      <sharedViews.ContextUrlView
-                        allowClick={true}
-                        description={entry.message}
-                        dispatcher={this.props.dispatcher}
-                        key={i}
-                        showContextTitle={true}
-                        thumbnail={entry.extraData.thumbnail}
-                        url={entry.extraData.location}
-                        useDesktopPaths={false} />
-                    </div>
-                  );
+                if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) {
+                  switch (entry.contentType) {
+                    case CHAT_CONTENT_TYPES.ROOM_NAME:
+                      return <TextChatRoomName message={entry.message}/>;
+                    case CHAT_CONTENT_TYPES.CONTEXT:
+                      return (
+                        <div className="context-url-view-wrapper">
+                          <sharedViews.ContextUrlView
+                            allowClick={true}
+                            description={entry.message}
+                            dispatcher={this.props.dispatcher}
+                            key={i}
+                            showContextTitle={true}
+                            thumbnail={entry.extraData.thumbnail}
+                            url={entry.extraData.location}
+                            useDesktopPaths={false} />
+                        </div>
+                      );
+                    default:
+                      console.error("Unsupported contentType", entry.contentType);
+                      return null;
+                  }
                 }
 
                 return (
                   <TextChatEntry key={i}
                                  contentType={entry.contentType}
                                  message={entry.message}
                                  type={entry.type} />
                 );
@@ -153,16 +176,21 @@ loop.shared.views.TextChatView = (functi
     /**
      * Handles submitting of the form - dispatches a send text chat message.
      *
      * @param {Object} event The DOM event.
      */
     handleFormSubmit: function(event) {
       event.preventDefault();
 
+      // Don't send empty messages.
+      if (!this.state.messageDetail) {
+        return;
+      }
+
       this.props.dispatcher.dispatch(new sharedActions.SendTextChatMessage({
         contentType: CHAT_CONTENT_TYPES.TEXT,
         message: this.state.messageDetail
       }));
 
       // Reset the form to empty, ready for the next message.
       this.setState({ messageDetail: "" });
     },
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -628,16 +628,25 @@ loop.shared.views = (function(_, l10n) {
         additionalClass: "",
         checked: false,
         disabled: false,
         label: null,
         value: ""
       };
     },
 
+    componentWillReceiveProps: function(nextProps) {
+      // Only change the state if the prop has changed, and if it is also
+      // different from the state.
+      if (this.props.checked !== nextProps.checked &&
+          this.state.checked !== nextProps.checked) {
+        this.setState({ checked: nextProps.checked });
+      }
+    },
+
     getInitialState: function() {
       return {
         checked: this.props.checked,
         value: this.props.checked ? this.props.value : ""
       };
     },
 
     _handleClick: function(event) {
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -628,16 +628,25 @@ loop.shared.views = (function(_, l10n) {
         additionalClass: "",
         checked: false,
         disabled: false,
         label: null,
         value: ""
       };
     },
 
+    componentWillReceiveProps: function(nextProps) {
+      // Only change the state if the prop has changed, and if it is also
+      // different from the state.
+      if (this.props.checked !== nextProps.checked &&
+          this.state.checked !== nextProps.checked) {
+        this.setState({ checked: nextProps.checked });
+      }
+    },
+
     getInitialState: function() {
       return {
         checked: this.props.checked,
         value: this.props.checked ? this.props.value : ""
       };
     },
 
     _handleClick: function(event) {
--- a/browser/components/loop/standalone/content/css/webapp.css
+++ b/browser/components/loop/standalone/content/css/webapp.css
@@ -124,17 +124,17 @@ body,
   background-image: url("../shared/img/mozilla-logo.png");
   background-repeat: no-repeat;
 }
 
 /* Rooms Footer */
 
 .rooms-footer {
   background: #000;
-  margin: 0 20px;
+  margin: 0 10px;
   text-align: left;
   height: 3em;
   position: relative;
 }
 
 html[dir="rtl"] .rooms-footer {
   text-align: right;
 }
@@ -349,44 +349,26 @@ p.standalone-btn-label {
   box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4);
   border-radius: 3px;
   z-index: 1002; /* ensures the form is always on top of the control bar */
 }
 .standalone .room-conversation-wrapper .ended-conversation .feedback {
   right: 35%;
 }
 
+html[dir="rtl"] .standalone .room-conversation-wrapper .ended-conversation .feedback {
+  right: auto;
+  left: 35%;
+}
+
 .standalone .ended-conversation .local-stream {
   /* Hide  local media stream when feedback form is shown. */
   display: none;
 }
 
-/**
- * The .text-chat-* styles are very temporarily whilst we work on text chat
- * (bug 1108892 and dependencies).
- */
-.text-chat-view {
-  height: 60px;
-  color: black;
-}
-
-.text-chat-entries {
-  /* XXX Should use flex, this is just for the initial implementation. */
-  height: calc(100% - 2em);
-}
-
-.text-chat-box {
-  width: 30%;
-  margin: auto;
-}
-
-.text-chat-box > form > input {
-  width: 100%;
-}
-
 @media screen and (max-width:640px) {
   .standalone .ended-conversation .feedback {
     width: 92%;
     top: 10%;
     left: 5px;
     right: 5px;
   }
 }
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -224,116 +224,22 @@ loop.standaloneRoomViews = (function(moz
           React.createElement("div", {className: "footer-logo"}), 
           React.createElement("p", {dangerouslySetInnerHTML: {__html: this._getContent()}, 
              onClick: this.recordClick})
         )
       );
     }
   });
 
-  var StandaloneRoomContextItem = React.createClass({displayName: "StandaloneRoomContextItem",
-    propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      receivingScreenShare: React.PropTypes.bool,
-      roomContextUrl: React.PropTypes.object
-    },
-
-    recordClick: function() {
-      this.props.dispatcher.dispatch(new sharedActions.RecordClick({
-        linkInfo: "Shared URL"
-      }));
-    },
-
-    render: function() {
-      if (!this.props.roomContextUrl ||
-          !this.props.roomContextUrl.location) {
-        return null;
-      }
-
-      var locationInfo = sharedUtils.formatURL(this.props.roomContextUrl.location);
-      if (!locationInfo) {
-        return null;
-      }
-
-      var cx = React.addons.classSet;
-
-      var classes = cx({
-        "standalone-context-url": true,
-        "screen-share-active": this.props.receivingScreenShare
-      });
-
-      return (
-        React.createElement("div", {className: classes}, 
-          React.createElement("img", {src: this.props.roomContextUrl.thumbnail || "shared/img/icons-16x16.svg#globe"}), 
-          React.createElement("div", {className: "standalone-context-url-description-wrapper"}, 
-            this.props.roomContextUrl.description, 
-            React.createElement("br", null), React.createElement("a", {href: locationInfo.location, 
-                     onClick: this.recordClick, 
-                     target: "_blank", 
-                     title: locationInfo.location}, locationInfo.hostname)
-          )
-        )
-      );
-    }
-  });
-
-  var StandaloneRoomContextView = React.createClass({displayName: "StandaloneRoomContextView",
-    propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      receivingScreenShare: React.PropTypes.bool.isRequired,
-      roomContextUrls: React.PropTypes.array,
-      roomName: React.PropTypes.string,
-      roomInfoFailure: React.PropTypes.string
-    },
-
-    getInitialState: function() {
-      return {
-        failureLogged: false
-      };
-    },
-
-    _logFailure: function(message) {
-      if (!this.state.failureLogged) {
-        console.error(mozL10n.get(message));
-        this.state.failureLogged = true;
-      }
-    },
-
-    render: function() {
-      // For failures, we currently just log the messages - UX doesn't want them
-      // displayed on primary UI at the moment.
-      if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
-        this._logFailure("room_information_failure_unsupported_browser");
-        return null;
-      } else if (this.props.roomInfoFailure) {
-        this._logFailure("room_information_failure_not_available");
-        return null;
-      }
-
-      // We only support one item in the context Urls array for now.
-      var roomContextUrl = (this.props.roomContextUrls &&
-                            this.props.roomContextUrls.length > 0) ?
-                            this.props.roomContextUrls[0] : null;
-      return (
-        React.createElement("div", {className: "standalone-room-info"}, 
-          React.createElement("h2", {className: "room-name"}, this.props.roomName), 
-          React.createElement(StandaloneRoomContextItem, {
-            dispatcher: this.props.dispatcher, 
-            receivingScreenShare: this.props.receivingScreenShare, 
-            roomContextUrl: roomContextUrl})
-        )
-      );
-    }
-  });
-
   var StandaloneRoomView = React.createClass({displayName: "StandaloneRoomView",
     mixins: [
       Backbone.Events,
       sharedMixins.MediaSetupMixin,
-      sharedMixins.RoomsAudioMixin
+      sharedMixins.RoomsAudioMixin,
+      loop.store.StoreMixin("activeRoomStore")
     ],
 
     propTypes: {
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
       ]).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
@@ -347,257 +253,71 @@ loop.standaloneRoomViews = (function(moz
     getInitialState: function() {
       var storeState = this.props.activeRoomStore.getStoreState();
       return _.extend({}, storeState, {
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState
       });
     },
 
-    componentWillMount: function() {
-      this.listenTo(this.props.activeRoomStore, "change",
-                    this._onActiveRoomStateChanged);
-    },
-
-    /**
-     * Handles a "change" event on the roomStore, and updates this.state
-     * to match the store.
-     *
-     * @private
-     */
-    _onActiveRoomStateChanged: function() {
-      var state = this.props.activeRoomStore.getStoreState();
-      this.updateVideoDimensions(state.localVideoDimensions, state.remoteVideoDimensions);
-      this.setState(state);
-    },
-
     componentDidMount: function() {
       // Adding a class to the document body element from here to ease styling it.
       document.body.classList.add("is-standalone-room");
     },
 
-    componentWillUnmount: function() {
-      this.stopListening(this.props.activeRoomStore);
-    },
-
     /**
      * Watches for when we transition to MEDIA_WAIT room state, so we can request
      * user media access.
      *
      * @param  {Object} nextProps (Unused)
      * @param  {Object} nextState Next state object.
      */
     componentWillUpdate: function(nextProps, nextState) {
       if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
           nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
         this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
           publisherConfig: this.getDefaultPublisherConfig({publishVideo: true})
         }));
       }
 
-      if (this.state.roomState !== ROOM_STATES.JOINED &&
-          nextState.roomState === ROOM_STATES.JOINED) {
-        // This forces the video size to update - creating the publisher
-        // first, and then connecting to the session doesn't seem to set the
-        // initial size correctly.
-        this.updateVideoContainer();
-      }
-
-      if (nextState.roomState === ROOM_STATES.INIT ||
-          nextState.roomState === ROOM_STATES.GATHER ||
-          nextState.roomState === ROOM_STATES.READY) {
-        this.resetDimensionsCache();
-      }
-
-      // When screen sharing stops.
-      if (this.state.receivingScreenShare && !nextState.receivingScreenShare) {
-        // Remove the custom screenshare styles on the remote camera.
-        var node = this._getElement(".remote");
-        node.removeAttribute("style");
-      }
-
-      if (this.state.receivingScreenShare != nextState.receivingScreenShare ||
-          this.state.remoteVideoEnabled != nextState.remoteVideoEnabled) {
-        this.updateVideoContainer();
+      // UX don't want to surface these errors (as they would imply the user
+      // needs to do something to fix them, when if they're having a conversation
+      // they just need to connect). However, we do want there to be somewhere to
+      // find reasonably easily, in case there's issues raised.
+      if (!this.state.roomInfoFailure && nextState.roomInfoFailure) {
+        if (nextState.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
+          console.error(mozL10n.get("room_information_failure_unsupported_browser"));
+        } else {
+          console.error(mozL10n.get("room_information_failure_not_available"));
+        }
       }
     },
 
     joinRoom: function() {
       this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
     },
 
     leaveRoom: function() {
       this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
     },
 
     /**
-     * Wrapper for window.matchMedia so that we use an appropriate version
-     * for the ui-showcase, which puts views inside of their own iframes.
-     *
-     * Currently, we use an icky hack, and the showcase conspires with
-     * react-frame-component to set iframe.contentWindow.matchMedia onto
-     * activeRoomStore.  Once React context matures a bit (somewhere between
-     * 0.14 and 1.0, apparently):
-     *
-     * https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
-     *
-     * we should be able to use those to clean this up.
-     *
-     * @param queryString
-     * @returns {MediaQueryList|null}
-     * @private
-     */
-    _matchMedia: function(queryString) {
-      if ("matchMedia" in this.state) {
-        return this.state.matchMedia(queryString);
-      } else if ("matchMedia" in window) {
-        return window.matchMedia(queryString);
-      }
-      return null;
-    },
-
-    /**
      * Toggles streaming status for a given stream type.
      *
      * @param  {String}  type     Stream type ("audio" or "video").
      * @param  {Boolean} enabled  Enabled stream flag.
      */
     publishStream: function(type, enabled) {
       this.props.dispatcher.dispatch(new sharedActions.SetMute({
         type: type,
         enabled: enabled
       }));
     },
 
     /**
-     * Specifically updates the local camera stream size and position, depending
-     * on the size and position of the remote video stream.
-     * This method gets called from `updateVideoContainer`, which is defined in
-     * the `MediaSetupMixin`.
-     *
-     * @param  {Object} ratio Aspect ratio of the local camera stream
-     */
-    updateLocalCameraPosition: function(ratio) {
-      // The local stream is a quarter of the remote stream.
-      var LOCAL_STREAM_SIZE = 0.25;
-      // The local stream overlaps the remote stream by a quarter of the local stream.
-      var LOCAL_STREAM_OVERLAP = 0.25;
-      // The minimum size of video height/width allowed by the sdk css.
-      var SDK_MIN_SIZE = 48;
-
-      var node = this._getElement(".local");
-      var targetWidth;
-
-      node.style.right = "auto";
-      if (this._matchMedia("screen and (max-width:640px)").matches) {
-        // For reduced screen widths, we just go for a fixed size and no overlap.
-        targetWidth = 180;
-        node.style.width = (targetWidth * ratio.width) + "px";
-        node.style.height = (targetWidth * ratio.height) + "px";
-        node.style.left = "auto";
-      } else {
-        // The local camera view should be a quarter of the size of the remote stream
-        // and positioned to overlap with the remote stream at a quarter of its width.
-
-        // Now position the local camera view correctly with respect to the remote
-        // video stream or the screen share stream.
-        var remoteVideoDimensions;
-        var isScreenShare = this.state.receivingScreenShare;
-        var videoDisplayed = isScreenShare ?
-          this.state.screenShareVideoObject || this.props.screenSharePosterUrl :
-          this.state.remoteSrcVideoObject || this.props.remotePosterUrl;
-
-        if ((isScreenShare || this.shouldRenderRemoteVideo()) && videoDisplayed) {
-          remoteVideoDimensions = this.getRemoteVideoDimensions(
-            isScreenShare ? "screen" : "camera");
-        } else {
-          var remoteElement = this.getDOMNode().querySelector(".remote.focus-stream");
-          if (!remoteElement) {
-            return;
-          }
-          remoteVideoDimensions = {
-            streamWidth: remoteElement.offsetWidth,
-            offsetX: remoteElement.offsetLeft
-          };
-        }
-
-        targetWidth = remoteVideoDimensions.streamWidth * LOCAL_STREAM_SIZE;
-
-        var realWidth = targetWidth * ratio.width;
-        var realHeight = targetWidth * ratio.height;
-
-        // If we've hit the min size limits, then limit at the minimum.
-        if (realWidth < SDK_MIN_SIZE) {
-          realWidth = SDK_MIN_SIZE;
-          realHeight = realWidth / ratio.width * ratio.height;
-        }
-        if (realHeight < SDK_MIN_SIZE) {
-          realHeight = SDK_MIN_SIZE;
-          realWidth = realHeight / ratio.height * ratio.width;
-        }
-
-        var offsetX = (remoteVideoDimensions.streamWidth + remoteVideoDimensions.offsetX);
-        // The horizontal offset of the stream, and the width of the resulting
-        // pillarbox, is determined by the height exponent of the aspect ratio.
-        // Therefore we multiply the width of the local camera view by the height
-        // ratio.
-        node.style.left = (offsetX - (realWidth * LOCAL_STREAM_OVERLAP)) + "px";
-        node.style.width = realWidth + "px";
-        node.style.height = realHeight + "px";
-      }
-    },
-
-    /**
-     * Specifically updates the remote camera stream size and position, if
-     * a screen share is being received. It is slaved from the position of the
-     * local stream.
-     * This method gets called from `updateVideoContainer`, which is defined in
-     * the `MediaSetupMixin`.
-     *
-     * @param  {Object} ratio Aspect ratio of the remote camera stream
-     */
-    updateRemoteCameraPosition: function(ratio) {
-      // Nothing to do for screenshare
-      if (!this.state.receivingScreenShare) {
-        return;
-      }
-      // XXX For the time being, if we're a narrow screen, aka mobile, we don't display
-      // the remote media (bug 1133534).
-      if (this._matchMedia("screen and (max-width:640px)").matches) {
-        return;
-      }
-
-      // 10px separation between the two streams.
-      var LOCAL_REMOTE_SEPARATION = 10;
-
-      var node = this._getElement(".remote");
-      var localNode = this._getElement(".local");
-
-      // Match the width to the local video.
-      node.style.width = localNode.offsetWidth + "px";
-
-      // The height is then determined from the aspect ratio
-      var height = ((localNode.offsetWidth / ratio.width) * ratio.height);
-      node.style.height = height + "px";
-
-      node.style.right = "auto";
-      node.style.bottom = "auto";
-
-      // Now position the local camera view correctly with respect to the remote
-      // video stream.
-
-      // The top is measured from the top of the element down the screen,
-      // so subtract the height of the video and the separation distance.
-      node.style.top = (localNode.offsetTop - height - LOCAL_REMOTE_SEPARATION) + "px";
-
-      // Match the left-hand sides.
-      node.style.left = localNode.offsetLeft + "px";
-    },
-
-    /**
      * Checks if current room is active.
      *
      * @return {Boolean}
      */
     _roomIsActive: function() {
       return this.state.roomState === ROOM_STATES.JOINED            ||
              this.state.roomState === ROOM_STATES.SESSION_CONNECTED ||
              this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
@@ -642,104 +362,92 @@ loop.standaloneRoomViews = (function(moz
           console.warn("StandaloneRoomView.shouldRenderRemoteVideo:" +
             " unexpected roomState: ", this.state.roomState);
           return true;
 
       }
     },
 
     render: function() {
-      var localStreamClasses = React.addons.classSet({
-        local: true,
-        "local-stream": true,
-        "local-stream-audio": this.state.videoMuted
-      });
+      var displayScreenShare = this.state.receivingScreenShare ||
+        this.props.screenSharePosterUrl;
 
       var remoteStreamClasses = React.addons.classSet({
-        "video_inner": true,
         "remote": true,
-        "focus-stream": !this.state.receivingScreenShare,
-        "remote-inset-stream": this.state.receivingScreenShare
+        "focus-stream": !displayScreenShare
       });
 
       var screenShareStreamClasses = React.addons.classSet({
         "screen": true,
-        "focus-stream": this.state.receivingScreenShare,
-        hide: !this.state.receivingScreenShare
+        "focus-stream": displayScreenShare
       });
 
-      // XXX Temporarily showAlways = showRoomName = false for TextChatView
-      // until bug 1168829 is completed.
+      var mediaWrapperClasses = React.addons.classSet({
+        "media-wrapper": true,
+        "receiving-screen-share": displayScreenShare,
+        "showing-local-streams": this.state.localSrcVideoObject ||
+          this.props.localPosterUrl
+      });
+
       return (
         React.createElement("div", {className: "room-conversation-wrapper"}, 
           React.createElement("div", {className: "beta-logo"}), 
-          React.createElement(sharedViews.TextChatView, {
-            dispatcher: this.props.dispatcher, 
-            showAlways: false, 
-            showRoomName: false}), 
           React.createElement(StandaloneRoomHeader, {dispatcher: this.props.dispatcher}), 
           React.createElement(StandaloneRoomInfoArea, {roomState: this.state.roomState, 
                                   failureReason: this.state.failureReason, 
                                   joinRoom: this.joinRoom, 
                                   isFirefox: this.props.isFirefox, 
                                   activeRoomStore: this.props.activeRoomStore, 
                                   roomUsed: this.state.used}), 
-          React.createElement("div", {className: "video-layout-wrapper"}, 
-            React.createElement("div", {className: "conversation room-conversation"}, 
-              React.createElement(StandaloneRoomContextView, {
+          React.createElement("div", {className: "media-layout"}, 
+            React.createElement("div", {className: mediaWrapperClasses}, 
+              React.createElement("span", {className: "self-view-hidden-message"}, 
+                mozL10n.get("self_view_hidden_message")
+              ), 
+              React.createElement("div", {className: remoteStreamClasses}, 
+                React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(), 
+                  posterUrl: this.props.remotePosterUrl, 
+                  mediaType: "remote", 
+                  srcVideoObject: this.state.remoteSrcVideoObject})
+              ), 
+              React.createElement("div", {className: screenShareStreamClasses}, 
+                React.createElement(sharedViews.MediaView, {displayAvatar: false, 
+                  posterUrl: this.props.screenSharePosterUrl, 
+                  mediaType: "screen-share", 
+                  srcVideoObject: this.state.screenShareVideoObject})
+              ), 
+              React.createElement(sharedViews.TextChatView, {
                 dispatcher: this.props.dispatcher, 
-                receivingScreenShare: this.state.receivingScreenShare, 
-                roomContextUrls: this.state.roomContextUrls, 
-                roomName: this.state.roomName, 
-                roomInfoFailure: this.state.roomInfoFailure}), 
-              React.createElement("div", {className: "media nested"}, 
-                React.createElement("span", {className: "self-view-hidden-message"}, 
-                  mozL10n.get("self_view_hidden_message")
-                ), 
-                React.createElement("div", {className: "video_wrapper remote_wrapper"}, 
-                  React.createElement("div", {className: remoteStreamClasses}, 
-                    React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(), 
-                      posterUrl: this.props.remotePosterUrl, 
-                      mediaType: "remote", 
-                      srcVideoObject: this.state.remoteSrcVideoObject})
-                  ), 
-                  React.createElement("div", {className: screenShareStreamClasses}, 
-                    React.createElement(sharedViews.MediaView, {displayAvatar: false, 
-                      posterUrl: this.props.screenSharePosterUrl, 
-                      mediaType: "screen-share", 
-                      srcVideoObject: this.state.screenShareVideoObject})
-                  )
-                ), 
-                React.createElement("div", {className: localStreamClasses}, 
-                  React.createElement(sharedViews.MediaView, {displayAvatar: this.state.videoMuted, 
-                    posterUrl: this.props.localPosterUrl, 
-                    mediaType: "local", 
-                    srcVideoObject: this.state.localSrcVideoObject})
-                )
-              ), 
-              React.createElement(sharedViews.ConversationToolbar, {
-                dispatcher: this.props.dispatcher, 
-                video: {enabled: !this.state.videoMuted,
-                        visible: this._roomIsActive()}, 
-                audio: {enabled: !this.state.audioMuted,
-                        visible: this._roomIsActive()}, 
-                publishStream: this.publishStream, 
-                hangup: this.leaveRoom, 
-                hangupButtonLabel: mozL10n.get("rooms_leave_button_label"), 
-                enableHangup: this._roomIsActive()})
-            )
+                showAlways: true, 
+                showRoomName: true}), 
+              React.createElement("div", {className: "local"}, 
+                React.createElement(sharedViews.MediaView, {displayAvatar: this.state.videoMuted, 
+                  posterUrl: this.props.localPosterUrl, 
+                  mediaType: "local", 
+                  srcVideoObject: this.state.localSrcVideoObject})
+              )
+            ), 
+            React.createElement(sharedViews.ConversationToolbar, {
+              dispatcher: this.props.dispatcher, 
+              video: {enabled: !this.state.videoMuted,
+                      visible: this._roomIsActive()}, 
+              audio: {enabled: !this.state.audioMuted,
+                      visible: this._roomIsActive()}, 
+              publishStream: this.publishStream, 
+              hangup: this.leaveRoom, 
+              hangupButtonLabel: mozL10n.get("rooms_leave_button_label"), 
+              enableHangup: this._roomIsActive()})
           ), 
           React.createElement(loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView, {
             marketplaceSrc: this.state.marketplaceSrc, 
             onMarketplaceMessage: this.state.onMarketplaceMessage}), 
           React.createElement(StandaloneRoomFooter, {dispatcher: this.props.dispatcher})
         )
       );
     }
   });
 
   return {
-    StandaloneRoomContextView: StandaloneRoomContextView,
     StandaloneRoomFooter: StandaloneRoomFooter,
     StandaloneRoomHeader: StandaloneRoomHeader,
     StandaloneRoomView: StandaloneRoomView
   };
 })(navigator.mozL10n);
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -224,116 +224,22 @@ loop.standaloneRoomViews = (function(moz
           <div className="footer-logo" />
           <p dangerouslySetInnerHTML={{__html: this._getContent()}}
              onClick={this.recordClick}></p>
         </footer>
       );
     }
   });
 
-  var StandaloneRoomContextItem = React.createClass({
-    propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      receivingScreenShare: React.PropTypes.bool,
-      roomContextUrl: React.PropTypes.object
-    },
-
-    recordClick: function() {
-      this.props.dispatcher.dispatch(new sharedActions.RecordClick({
-        linkInfo: "Shared URL"
-      }));
-    },
-
-    render: function() {
-      if (!this.props.roomContextUrl ||
-          !this.props.roomContextUrl.location) {
-        return null;
-      }
-
-      var locationInfo = sharedUtils.formatURL(this.props.roomContextUrl.location);
-      if (!locationInfo) {
-        return null;
-      }
-
-      var cx = React.addons.classSet;
-
-      var classes = cx({
-        "standalone-context-url": true,
-        "screen-share-active": this.props.receivingScreenShare
-      });
-
-      return (
-        <div className={classes}>
-          <img src={this.props.roomContextUrl.thumbnail || "shared/img/icons-16x16.svg#globe"} />
-          <div className="standalone-context-url-description-wrapper">
-            {this.props.roomContextUrl.description}
-            <br /><a href={locationInfo.location}
-                     onClick={this.recordClick}
-                     target="_blank"
-                     title={locationInfo.location}>{locationInfo.hostname}</a>
-          </div>
-        </div>
-      );
-    }
-  });
-
-  var StandaloneRoomContextView = React.createClass({
-    propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      receivingScreenShare: React.PropTypes.bool.isRequired,
-      roomContextUrls: React.PropTypes.array,
-      roomName: React.PropTypes.string,
-      roomInfoFailure: React.PropTypes.string
-    },
-
-    getInitialState: function() {
-      return {
-        failureLogged: false
-      };
-    },
-
-    _logFailure: function(message) {
-      if (!this.state.failureLogged) {
-        console.error(mozL10n.get(message));
-        this.state.failureLogged = true;
-      }
-    },
-
-    render: function() {
-      // For failures, we currently just log the messages - UX doesn't want them
-      // displayed on primary UI at the moment.
-      if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
-        this._logFailure("room_information_failure_unsupported_browser");
-        return null;
-      } else if (this.props.roomInfoFailure) {
-        this._logFailure("room_information_failure_not_available");
-        return null;
-      }
-
-      // We only support one item in the context Urls array for now.
-      var roomContextUrl = (this.props.roomContextUrls &&
-                            this.props.roomContextUrls.length > 0) ?
-                            this.props.roomContextUrls[0] : null;
-      return (
-        <div className="standalone-room-info">
-          <h2 className="room-name">{this.props.roomName}</h2>
-          <StandaloneRoomContextItem
-            dispatcher={this.props.dispatcher}
-            receivingScreenShare={this.props.receivingScreenShare}
-            roomContextUrl={roomContextUrl} />
-        </div>
-      );
-    }
-  });
-
   var StandaloneRoomView = React.createClass({
     mixins: [
       Backbone.Events,
       sharedMixins.MediaSetupMixin,
-      sharedMixins.RoomsAudioMixin
+      sharedMixins.RoomsAudioMixin,
+      loop.store.StoreMixin("activeRoomStore")
     ],
 
     propTypes: {
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
       ]).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
@@ -347,257 +253,71 @@ loop.standaloneRoomViews = (function(moz
     getInitialState: function() {
       var storeState = this.props.activeRoomStore.getStoreState();
       return _.extend({}, storeState, {
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState
       });
     },
 
-    componentWillMount: function() {
-      this.listenTo(this.props.activeRoomStore, "change",
-                    this._onActiveRoomStateChanged);
-    },
-
-    /**
-     * Handles a "change" event on the roomStore, and updates this.state
-     * to match the store.
-     *
-     * @private
-     */
-    _onActiveRoomStateChanged: function() {
-      var state = this.props.activeRoomStore.getStoreState();
-      this.updateVideoDimensions(state.localVideoDimensions, state.remoteVideoDimensions);
-      this.setState(state);
-    },
-
     componentDidMount: function() {
       // Adding a class to the document body element from here to ease styling it.
       document.body.classList.add("is-standalone-room");
     },
 
-    componentWillUnmount: function() {
-      this.stopListening(this.props.activeRoomStore);
-    },
-
     /**
      * Watches for when we transition to MEDIA_WAIT room state, so we can request
      * user media access.
      *
      * @param  {Object} nextProps (Unused)
      * @param  {Object} nextState Next state object.
      */
     componentWillUpdate: function(nextProps, nextState) {
       if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
           nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
         this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
           publisherConfig: this.getDefaultPublisherConfig({publishVideo: true})
         }));
       }
 
-      if (this.state.roomState !== ROOM_STATES.JOINED &&
-          nextState.roomState === ROOM_STATES.JOINED) {
-        // This forces the video size to update - creating the publisher
-        // first, and then connecting to the session doesn't seem to set the
-        // initial size correctly.
-        this.updateVideoContainer();
-      }
-
-      if (nextState.roomState === ROOM_STATES.INIT ||
-          nextState.roomState === ROOM_STATES.GATHER ||
-          nextState.roomState === ROOM_STATES.READY) {
-        this.resetDimensionsCache();
-      }
-
-      // When screen sharing stops.
-      if (this.state.receivingScreenShare && !nextState.receivingScreenShare) {
-        // Remove the custom screenshare styles on the remote camera.
-        var node = this._getElement(".remote");
-        node.removeAttribute("style");
-      }
-
-      if (this.state.receivingScreenShare != nextState.receivingScreenShare ||
-          this.state.remoteVideoEnabled != nextState.remoteVideoEnabled) {
-        this.updateVideoContainer();
+      // UX don't want to surface these errors (as they would imply the user
+      // needs to do something to fix them, when if they're having a conversation
+      // they just need to connect). However, we do want there to be somewhere to
+      // find reasonably easily, in case there's issues raised.
+      if (!this.state.roomInfoFailure && nextState.roomInfoFailure) {
+        if (nextState.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
+          console.error(mozL10n.get("room_information_failure_unsupported_browser"));
+        } else {
+          console.error(mozL10n.get("room_information_failure_not_available"));
+        }
       }
     },
 
     joinRoom: function() {
       this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
     },
 
     leaveRoom: function() {
       this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
     },
 
     /**
-     * Wrapper for window.matchMedia so that we use an appropriate version
-     * for the ui-showcase, which puts views inside of their own iframes.
-     *
-     * Currently, we use an icky hack, and the showcase conspires with
-     * react-frame-component to set iframe.contentWindow.matchMedia onto
-     * activeRoomStore.  Once React context matures a bit (somewhere between
-     * 0.14 and 1.0, apparently):
-     *
-     * https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
-     *
-     * we should be able to use those to clean this up.
-     *
-     * @param queryString
-     * @returns {MediaQueryList|null}
-     * @private
-     */
-    _matchMedia: function(queryString) {
-      if ("matchMedia" in this.state) {
-        return this.state.matchMedia(queryString);
-      } else if ("matchMedia" in window) {
-        return window.matchMedia(queryString);
-      }
-      return null;
-    },
-
-    /**
      * Toggles streaming status for a given stream type.
      *
      * @param  {String}  type     Stream type ("audio" or "video").
      * @param  {Boolean} enabled  Enabled stream flag.
      */
     publishStream: function(type, enabled) {
       this.props.dispatcher.dispatch(new sharedActions.SetMute({
         type: type,
         enabled: enabled
       }));
     },
 
     /**
-     * Specifically updates the local camera stream size and position, depending
-     * on the size and position of the remote video stream.
-     * This method gets called from `updateVideoContainer`, which is defined in
-     * the `MediaSetupMixin`.
-     *
-     * @param  {Object} ratio Aspect ratio of the local camera stream
-     */
-    updateLocalCameraPosition: function(ratio) {
-      // The local stream is a quarter of the remote stream.
-      var LOCAL_STREAM_SIZE = 0.25;
-      // The local stream overlaps the remote stream by a quarter of the local stream.
-      var LOCAL_STREAM_OVERLAP = 0.25;
-      // The minimum size of video height/width allowed by the sdk css.
-      var SDK_MIN_SIZE = 48;
-
-      var node = this._getElement(".local");
-      var targetWidth;
-
-      node.style.right = "auto";
-      if (this._matchMedia("screen and (max-width:640px)").matches) {
-        // For reduced screen widths, we just go for a fixed size and no overlap.
-        targetWidth = 180;
-        node.style.width = (targetWidth * ratio.width) + "px";
-        node.style.height = (targetWidth * ratio.height) + "px";
-        node.style.left = "auto";
-      } else {
-        // The local camera view should be a quarter of the size of the remote stream
-        // and positioned to overlap with the remote stream at a quarter of its width.
-
-        // Now position the local camera view correctly with respect to the remote
-        // video stream or the screen share stream.
-        var remoteVideoDimensions;
-        var isScreenShare = this.state.receivingScreenShare;
-        var videoDisplayed = isScreenShare ?
-          this.state.screenShareVideoObject || this.props.screenSharePosterUrl :
-          this.state.remoteSrcVideoObject || this.props.remotePosterUrl;
-
-        if ((isScreenShare || this.shouldRenderRemoteVideo()) && videoDisplayed) {
-          remoteVideoDimensions = this.getRemoteVideoDimensions(
-            isScreenShare ? "screen" : "camera");
-        } else {
-          var remoteElement = this.getDOMNode().querySelector(".remote.focus-stream");
-          if (!remoteElement) {
-            return;
-          }
-          remoteVideoDimensions = {
-            streamWidth: remoteElement.offsetWidth,
-            offsetX: remoteElement.offsetLeft
-          };
-        }
-
-        targetWidth = remoteVideoDimensions.streamWidth * LOCAL_STREAM_SIZE;
-
-        var realWidth = targetWidth * ratio.width;
-        var realHeight = targetWidth * ratio.height;
-
-        // If we've hit the min size limits, then limit at the minimum.
-        if (realWidth < SDK_MIN_SIZE) {
-          realWidth = SDK_MIN_SIZE;
-          realHeight = realWidth / ratio.width * ratio.height;
-        }
-        if (realHeight < SDK_MIN_SIZE) {
-          realHeight = SDK_MIN_SIZE;
-          realWidth = realHeight / ratio.height * ratio.width;
-        }
-
-        var offsetX = (remoteVideoDimensions.streamWidth + remoteVideoDimensions.offsetX);
-        // The horizontal offset of the stream, and the width of the resulting
-        // pillarbox, is determined by the height exponent of the aspect ratio.
-        // Therefore we multiply the width of the local camera view by the height
-        // ratio.
-        node.style.left = (offsetX - (realWidth * LOCAL_STREAM_OVERLAP)) + "px";
-        node.style.width = realWidth + "px";
-        node.style.height = realHeight + "px";
-      }
-    },
-
-    /**
-     * Specifically updates the remote camera stream size and position, if
-     * a screen share is being received. It is slaved from the position of the
-     * local stream.
-     * This method gets called from `updateVideoContainer`, which is defined in
-     * the `MediaSetupMixin`.
-     *
-     * @param  {Object} ratio Aspect ratio of the remote camera stream
-     */
-    updateRemoteCameraPosition: function(ratio) {
-      // Nothing to do for screenshare
-      if (!this.state.receivingScreenShare) {
-        return;
-      }
-      // XXX For the time being, if we're a narrow screen, aka mobile, we don't display
-      // the remote media (bug 1133534).
-      if (this._matchMedia("screen and (max-width:640px)").matches) {
-        return;
-      }
-
-      // 10px separation between the two streams.
-      var LOCAL_REMOTE_SEPARATION = 10;
-
-      var node = this._getElement(".remote");
-      var localNode = this._getElement(".local");
-
-      // Match the width to the local video.
-      node.style.width = localNode.offsetWidth + "px";
-
-      // The height is then determined from the aspect ratio
-      var height = ((localNode.offsetWidth / ratio.width) * ratio.height);
-      node.style.height = height + "px";
-
-      node.style.right = "auto";
-      node.style.bottom = "auto";
-
-      // Now position the local camera view correctly with respect to the remote
-      // video stream.
-
-      // The top is measured from the top of the element down the screen,
-      // so subtract the height of the video and the separation distance.
-      node.style.top = (localNode.offsetTop - height - LOCAL_REMOTE_SEPARATION) + "px";
-
-      // Match the left-hand sides.
-      node.style.left = localNode.offsetLeft + "px";
-    },
-
-    /**
      * Checks if current room is active.
      *
      * @return {Boolean}
      */
     _roomIsActive: function() {
       return this.state.roomState === ROOM_STATES.JOINED            ||
              this.state.roomState === ROOM_STATES.SESSION_CONNECTED ||
              this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
@@ -642,104 +362,92 @@ loop.standaloneRoomViews = (function(moz
           console.warn("StandaloneRoomView.shouldRenderRemoteVideo:" +
             " unexpected roomState: ", this.state.roomState);
           return true;
 
       }
     },
 
     render: function() {
-      var localStreamClasses = React.addons.classSet({
-        local: true,
-        "local-stream": true,
-        "local-stream-audio": this.state.videoMuted
-      });
+      var displayScreenShare = this.state.receivingScreenShare ||
+        this.props.screenSharePosterUrl;
 
       var remoteStreamClasses = React.addons.classSet({
-        "video_inner": true,
         "remote": true,
-        "focus-stream": !this.state.receivingScreenShare,
-        "remote-inset-stream": this.state.receivingScreenShare
+        "focus-stream": !displayScreenShare
       });
 
       var screenShareStreamClasses = React.addons.classSet({
         "screen": true,
-        "focus-stream": this.state.receivingScreenShare,
-        hide: !this.state.receivingScreenShare
+        "focus-stream": displayScreenShare
       });
 
-      // XXX Temporarily showAlways = showRoomName = false for TextChatView
-      // until bug 1168829 is completed.
+      var mediaWrapperClasses = React.addons.classSet({
+        "media-wrapper": true,
+        "receiving-screen-share": displayScreenShare,
+        "showing-local-streams": this.state.localSrcVideoObject ||
+          this.props.localPosterUrl
+      });
+
       return (
         <div className="room-conversation-wrapper">
           <div className="beta-logo" />
-          <sharedViews.TextChatView
-            dispatcher={this.props.dispatcher}
-            showAlways={false}
-            showRoomName={false} />
           <StandaloneRoomHeader dispatcher={this.props.dispatcher} />
           <StandaloneRoomInfoArea roomState={this.state.roomState}
                                   failureReason={this.state.failureReason}
                                   joinRoom={this.joinRoom}
                                   isFirefox={this.props.isFirefox}
                                   activeRoomStore={this.props.activeRoomStore}
                                   roomUsed={this.state.used} />
-          <div className="video-layout-wrapper">
-            <div className="conversation room-conversation">
-              <StandaloneRoomContextView
+          <div className="media-layout">
+            <div className={mediaWrapperClasses}>
+              <span className="self-view-hidden-message">
+                {mozL10n.get("self_view_hidden_message")}
+              </span>
+              <div className={remoteStreamClasses}>
+                <sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()}
+                  posterUrl={this.props.remotePosterUrl}
+                  mediaType="remote"
+                  srcVideoObject={this.state.remoteSrcVideoObject} />
+              </div>
+              <div className={screenShareStreamClasses}>
+                <sharedViews.MediaView displayAvatar={false}
+                  posterUrl={this.props.screenSharePosterUrl}
+                  mediaType="screen-share"
+                  srcVideoObject={this.state.screenShareVideoObject} />
+              </div>
+              <sharedViews.TextChatView
                 dispatcher={this.props.dispatcher}
-                receivingScreenShare={this.state.receivingScreenShare}
-                roomContextUrls={this.state.roomContextUrls}
-                roomName={this.state.roomName}
-                roomInfoFailure={this.state.roomInfoFailure} />
-              <div className="media nested">
-                <span className="self-view-hidden-message">
-                  {mozL10n.get("self_view_hidden_message")}
-                </span>
-                <div className="video_wrapper remote_wrapper">
-                  <div className={remoteStreamClasses}>
-                    <sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()}
-                      posterUrl={this.props.remotePosterUrl}
-                      mediaType="remote"
-                      srcVideoObject={this.state.remoteSrcVideoObject} />
-                  </div>
-                  <div className={screenShareStreamClasses}>
-                    <sharedViews.MediaView displayAvatar={false}
-                      posterUrl={this.props.screenSharePosterUrl}
-                      mediaType="screen-share"
-                      srcVideoObject={this.state.screenShareVideoObject} />
-                  </div>
-                </div>
-                <div className={localStreamClasses}>
-                  <sharedViews.MediaView displayAvatar={this.state.videoMuted}
-                    posterUrl={this.props.localPosterUrl}
-                    mediaType="local"
-                    srcVideoObject={this.state.localSrcVideoObject} />
-                </div>
+                showAlways={true}
+                showRoomName={true} />
+              <div className="local">
+                <sharedViews.MediaView displayAvatar={this.state.videoMuted}
+                  posterUrl={this.props.localPosterUrl}
+                  mediaType="local"
+                  srcVideoObject={this.state.localSrcVideoObject} />
               </div>
-              <sharedViews.ConversationToolbar
-                dispatcher={this.props.dispatcher}
-                video={{enabled: !this.state.videoMuted,
-                        visible: this._roomIsActive()}}
-                audio={{enabled: !this.state.audioMuted,
-                        visible: this._roomIsActive()}}
-                publishStream={this.publishStream}
-                hangup={this.leaveRoom}
-                hangupButtonLabel={mozL10n.get("rooms_leave_button_label")}
-                enableHangup={this._roomIsActive()} />
             </div>
+            <sharedViews.ConversationToolbar
+              dispatcher={this.props.dispatcher}
+              video={{enabled: !this.state.videoMuted,
+                      visible: this._roomIsActive()}}
+              audio={{enabled: !this.state.audioMuted,
+                      visible: this._roomIsActive()}}
+              publishStream={this.publishStream}
+              hangup={this.leaveRoom}
+              hangupButtonLabel={mozL10n.get("rooms_leave_button_label")}
+              enableHangup={this._roomIsActive()} />
           </div>
           <loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView
             marketplaceSrc={this.state.marketplaceSrc}
             onMarketplaceMessage={this.state.onMarketplaceMessage} />
           <StandaloneRoomFooter dispatcher={this.props.dispatcher} />
         </div>
       );
     }
   });
 
   return {
-    StandaloneRoomContextView: StandaloneRoomContextView,
     StandaloneRoomFooter: StandaloneRoomFooter,
     StandaloneRoomHeader: StandaloneRoomHeader,
     StandaloneRoomView: StandaloneRoomView
   };
 })(navigator.mozL10n);
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -1089,16 +1089,17 @@ loop.webapp = (function($, _, OT, mozL10
     var standaloneMetricsStore = new loop.store.StandaloneMetricsStore(dispatcher, {
       activeRoomStore: activeRoomStore
     });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: sdkDriver
     });
 
     loop.store.StoreMixin.register({
+      activeRoomStore: activeRoomStore,
       feedbackStore: feedbackStore,
       // This isn't used in any views, but is saved here to ensure it
       // is kept alive.
       standaloneMetricsStore: standaloneMetricsStore,
       textChatStore: textChatStore
     });
 
     window.addEventListener("unload", function() {
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -1089,16 +1089,17 @@ loop.webapp = (function($, _, OT, mozL10
     var standaloneMetricsStore = new loop.store.StandaloneMetricsStore(dispatcher, {
       activeRoomStore: activeRoomStore
     });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: sdkDriver
     });
 
     loop.store.StoreMixin.register({
+      activeRoomStore: activeRoomStore,
       feedbackStore: feedbackStore,
       // This isn't used in any views, but is saved here to ensure it
       // is kept alive.
       standaloneMetricsStore: standaloneMetricsStore,
       textChatStore: textChatStore
     });
 
     window.addEventListener("unload", function() {
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -845,16 +845,35 @@ describe("loop.panel", function() {
 
       // Simulate being visible
       view.onDocumentVisible();
 
       var contextContent = view.getDOMNode().querySelector(".context-content");
       expect(contextContent).to.not.equal(null);
     });
 
+    it("should cancel the checkbox when a new URL is available", function() {
+      fakeMozLoop.getSelectedTabMetadata = function (callback) {
+        callback({
+          url: "https://www.example.com",
+          description: "fake description",
+          previews: [""]
+        });
+      };
+
+      var view = createTestComponent();
+
+      view.setState({ checked: true });
+
+      // Simulate being visible
+      view.onDocumentVisible();
+
+      expect(view.state.checked).eql(false);
+    });
+
     it("should show a default favicon when none is available", function() {
       fakeMozLoop.getSelectedTabMetadata = function (callback) {
         callback({
           url: "https://www.example.com",
           description: "fake description",
           previews: [""]
         });
       };
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -406,16 +406,26 @@ describe("loop.OTSdkDriver", function ()
     it("should disconnect the session", function() {
       driver.session = session;
 
       driver.disconnectSession();
 
       sinon.assert.calledOnce(session.disconnect);
     });
 
+    it("should dispatch a DataChannelsAvailable action with available = false", function() {
+      driver.disconnectSession();
+
+      sinon.assert.calledOnce(dispatcher.dispatch);
+      sinon.assert.calledWithExactly(dispatcher.dispatch,
+        new sharedActions.DataChannelsAvailable({
+          available: false
+        }));
+    });
+
     it("should destroy the publisher", function() {
       driver.publisher = publisher;
 
       driver.disconnectSession();
 
       sinon.assert.calledOnce(publisher.destroy);
     });
 
@@ -1000,26 +1010,36 @@ describe("loop.OTSdkDriver", function ()
     describe("streamDestroyed: publisher/local", function() {
       it("should dispatch a ConnectionStatus action", function() {
         driver._metrics.sendStreams = 1;
         driver._metrics.recvStreams = 1;
         driver._metrics.connections = 2;
 
         publisher.trigger("streamDestroyed");
 
-        sinon.assert.calledOnce(dispatcher.dispatch);
+        sinon.assert.calledTwice(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.ConnectionStatus({
             event: "Publisher.streamDestroyed",
             state: "receiving",
             connections: 2,
             recvStreams: 1,
             sendStreams: 0
           }));
       });
+
+      it("should dispatch a DataChannelsAvailable action", function() {
+        publisher.trigger("streamDestroyed");
+
+        sinon.assert.calledTwice(dispatcher.dispatch);
+        sinon.assert.calledWithExactly(dispatcher.dispatch,
+          new sharedActions.DataChannelsAvailable({
+            available: false
+          }));
+      });
     });
 
     describe("streamDestroyed: session/remote", function() {
       var stream;
 
       beforeEach(function() {
         stream = {
           videoType: "screen"
@@ -1049,24 +1069,43 @@ describe("loop.OTSdkDriver", function ()
             event: "Session.streamDestroyed",
             state: "sending",
             connections: 2,
             recvStreams: 0,
             sendStreams: 1
           }));
       });
 
-      it("should not dispatch an action if the videoType is camera", function() {
+      it("should not dispatch a ConnectionStatus action if the videoType is camera", function() {
         stream.videoType = "camera";
 
         session.trigger("streamDestroyed", { stream: stream });
 
         sinon.assert.neverCalledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "receivingScreenShare"));
       });
+
+      it("should dispatch a DataChannelsAvailable action for videoType = camera", function() {
+        stream.videoType = "camera";
+
+        session.trigger("streamDestroyed", { stream: stream });
+
+        sinon.assert.calledTwice(dispatcher.dispatch);
+        sinon.assert.calledWithExactly(dispatcher.dispatch,
+          new sharedActions.DataChannelsAvailable({
+            available: false
+          }));
+      });
+
+      it("should not dispatch a DataChannelsAvailable action for videoType = screen", function() {
+        session.trigger("streamDestroyed", { stream: stream });
+
+        sinon.assert.neverCalledWithMatch(dispatcher.dispatch,
+          sinon.match.hasOwn("name", "dataChannelsAvailable"));
+      });
     });
 
     describe("streamPropertyChanged", function() {
       var stream = {
         connection: { id: "fake" },
         videoType: "screen",
         videoDimensions: {
           width: 320,
@@ -1292,17 +1331,19 @@ describe("loop.OTSdkDriver", function ()
 
         subscriber._.getDataChannel.callsArgWith(2, null, fakeChannel);
         publisher._.getDataChannel.callsArgWith(2, null, fakeChannel);
 
         session.trigger("signal:readyForDataChannel");
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.DataChannelsAvailable());
+          new sharedActions.DataChannelsAvailable({
+            available: true
+          }));
       });
 
       it("should dispatch `ReceivedTextChatMessage` when a text message is received", function() {
         var fakeChannel = _.extend({}, Backbone.Events);
 
         subscriber._.getDataChannel.callsArgWith(2, null, fakeChannel);
 
         session.trigger("signal:readyForDataChannel");
--- a/browser/components/loop/test/shared/textChatStore_test.js
+++ b/browser/components/loop/test/shared/textChatStore_test.js
@@ -32,29 +32,41 @@ describe("loop.store.TextChatStore", fun
     });
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
   describe("#dataChannelsAvailable", function() {
-    it("should set textChatEnabled to true", function() {
-      store.dataChannelsAvailable();
+    it("should set textChatEnabled to the supplied state", function() {
+      store.dataChannelsAvailable(new sharedActions.DataChannelsAvailable({
+        available: true
+      }));
 
       expect(store.getStoreState("textChatEnabled")).eql(true);
     });
 
     it("should dispatch a LoopChatEnabled event", function() {
-      store.dataChannelsAvailable();
+      store.dataChannelsAvailable(new sharedActions.DataChannelsAvailable({
+        available: true
+      }));
 
       sinon.assert.calledOnce(window.dispatchEvent);
       sinon.assert.calledWithExactly(window.dispatchEvent,
         new CustomEvent("LoopChatEnabled"));
     });
+
+    it("should not dispatch a LoopChatEnabled event if available is false", function() {
+      store.dataChannelsAvailable(new sharedActions.DataChannelsAvailable({
+        available: false
+      }));
+
+      sinon.assert.notCalled(window.dispatchEvent);
+    });
   });
 
   describe("#receivedTextChatMessage", function() {
     it("should add the message to the list", function() {
       var message = "Hello!";
 
       store.receivedTextChatMessage({
         contentType: CHAT_CONTENT_TYPES.TEXT,
@@ -128,67 +140,61 @@ describe("loop.store.TextChatStore", fun
       sinon.assert.calledOnce(window.dispatchEvent);
       sinon.assert.calledWithExactly(window.dispatchEvent,
         new CustomEvent("LoopChatMessageAppended"));
     });
   });
 
   describe("#updateRoomInfo", function() {
     it("should add the room name to the list", function() {
-      sandbox.stub(navigator.mozL10n, "get").returns("Let's really share!");
-
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
         roomName: "Let's share!",
         roomOwner: "Mark",
         roomUrl: "fake"
       }));
 
       expect(store.getStoreState("messageList")).eql([{
         type: CHAT_MESSAGE_TYPES.SPECIAL,
         contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
-        message: "Let's really share!",
+        message: "Let's share!",
         extraData: undefined
       }]);
     });
 
     it("should add the context to the list", function() {
-      sandbox.stub(navigator.mozL10n, "get").returns("Let's really share!");
-
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
         roomName: "Let's share!",
         roomOwner: "Mark",
         roomUrl: "fake",
         urls: [{
           description: "A wonderful event",
           location: "http://wonderful.invalid",
           thumbnail: "fake"
         }]
       }));
 
       expect(store.getStoreState("messageList")).eql([
         {
           type: CHAT_MESSAGE_TYPES.SPECIAL,
           contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
-          message: "Let's really share!",
+          message: "Let's share!",
           extraData: undefined
         }, {
           type: CHAT_MESSAGE_TYPES.SPECIAL,
           contentType: CHAT_CONTENT_TYPES.CONTEXT,
           message: "A wonderful event",
           extraData: {
             location: "http://wonderful.invalid",
             thumbnail: "fake"
           }
         }
       ]);
     });
 
     it("should not dispatch a LoopChatMessageAppended event", function() {
-      sandbox.stub(navigator.mozL10n, "get").returns("Let's really share!");
-
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
         roomName: "Let's share!",
         roomOwner: "Mark",
         roomUrl: "fake"
       }));
 
       sinon.assert.notCalled(window.dispatchEvent);
     });
--- a/browser/components/loop/test/shared/textChatView_test.js
+++ b/browser/components/loop/test/shared/textChatView_test.js
@@ -169,10 +169,23 @@ describe("loop.shared.views.TextChatView
 
       sinon.assert.calledOnce(dispatcher.dispatch);
       sinon.assert.calledWithExactly(dispatcher.dispatch,
         new sharedActions.SendTextChatMessage({
           contentType: CHAT_CONTENT_TYPES.TEXT,
           message: "Hello!"
         }));
     });
+
+    it("should not dispatch SendTextChatMessage when the message is empty", function() {
+      view = mountTestComponent();
+
+      var entryNode = view.getDOMNode().querySelector(".text-chat-box > form > input");
+
+      TestUtils.Simulate.keyDown(entryNode, {
+        key: "Enter",
+        which: 13
+      });
+
+      sinon.assert.notCalled(dispatcher.dispatch);
+    });
   });
 });
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -760,16 +760,36 @@ describe("loop.shared.views", function()
         view = mountTestComponent({
           disabled: true
         });
 
         var node = view.getDOMNode();
         expect(node.classList.contains("disabled")).to.eql(true);
         expect(node.hasAttribute("disabled")).to.eql(true);
       });
+
+      it("should render the checkbox as checked when the prop is set", function() {
+        view = mountTestComponent({
+          checked: true
+        });
+
+        var checkbox = view.getDOMNode().querySelector(".checkbox");
+        expect(checkbox.classList.contains("checked")).eql(true);
+      });
+
+      it("should alter the render state when the props are changed", function() {
+        view = mountTestComponent({
+          checked: true
+        });
+
+        view.setProps({checked: false});
+
+        var checkbox = view.getDOMNode().querySelector(".checkbox");
+        expect(checkbox.classList.contains("checked")).eql(false);
+      });
     });
 
     describe("#_handleClick", function() {
       var onChange;
 
       beforeEach(function() {
         onChange = sinon.stub();
       });
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -26,150 +26,31 @@ describe("loop.standaloneRoomViews", fun
     });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: {}
     });
     feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       feedbackClient: {}
     });
     loop.store.StoreMixin.register({
+      activeRoomStore: activeRoomStore,
       feedbackStore: feedbackStore,
       textChatStore: textChatStore
     });
 
     sandbox.useFakeTimers();
 
     // Prevents audio request errors in the test console.
     sandbox.useFakeXMLHttpRequest();
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
-  describe("StandaloneRoomContextView", function() {
-    beforeEach(function() {
-      sandbox.stub(navigator.mozL10n, "get").returnsArg(0);
-    });
-
-    function mountTestComponent(extraProps) {
-      var props = _.extend({
-        dispatcher: dispatcher,
-        receivingScreenShare: false
-      }, extraProps);
-      return TestUtils.renderIntoDocument(
-        React.createElement(
-          loop.standaloneRoomViews.StandaloneRoomContextView, props));
-    }
-
-    it("should display the room name if no failures are known", function() {
-      var view = mountTestComponent({
-        roomName: "Mike's room",
-        receivingScreenShare: false
-      });
-
-      expect(view.getDOMNode().textContent).eql("Mike's room");
-    });
-
-    it("should log an unsupported browser message if crypto is unsupported", function() {
-      var view = mountTestComponent({
-        roomName: "Mark's room",
-        roomInfoFailure: ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED
-      });
-
-      sinon.assert.called(console.error);
-      sinon.assert.calledWithMatch(console.error, sinon.match("unsupported"));
-    });
-
-    it("should display a general error message for any other failure", function() {
-      var view = mountTestComponent({
-        roomName: "Mark's room",
-        roomInfoFailure: ROOM_INFO_FAILURES.NO_DATA
-      });
-
-      sinon.assert.called(console.error);
-      sinon.assert.calledWithMatch(console.error, sinon.match("not_available"));
-    });
-
-    it("should display context information if a url is supplied", function() {
-      var view = mountTestComponent({
-        roomName: "Mike's room",
-        roomContextUrls: [{
-          description: "Mark's super page",
-          location: "http://invalid.com",
-          thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
-        }]
-      });
-
-      expect(view.getDOMNode().querySelector(".standalone-context-url")).not.eql(null);
-    });
-
-    it("should format the url for display", function() {
-      sandbox.stub(sharedUtils, "formatURL").returns({
-          location: "location",
-          hostname: "hostname"
-        });
-
-      var view = mountTestComponent({
-        roomName: "Mike's room",
-        roomContextUrls: [{
-          description: "Mark's super page",
-          location: "http://invalid.com",
-          thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
-        }]
-      });
-
-      expect(view.getDOMNode()
-        .querySelector(".standalone-context-url-description-wrapper > a").textContent)
-        .eql("hostname");
-    });
-
-    it("should not display context information if no urls are supplied", function() {
-      var view = mountTestComponent({
-        roomName: "Mike's room"
-      });
-
-      expect(view.getDOMNode().querySelector(".standalone-context-url")).eql(null);
-    });
-
-    it("should dispatch a RecordClick action when the link is clicked", function() {
-      var view = mountTestComponent({
-        roomName: "Mark's room",
-        roomContextUrls: [{
-          description: "Mark's super page",
-          location: "http://invalid.com",
-          thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
-        }]
-      });
-
-      TestUtils.Simulate.click(view.getDOMNode()
-        .querySelector(".standalone-context-url-description-wrapper > a"));
-
-      sinon.assert.calledOnce(dispatcher.dispatch);
-      sinon.assert.calledWithExactly(dispatcher.dispatch,
-        new sharedActions.RecordClick({
-          linkInfo: "Shared URL"
-        }));
-    });
-
-    it("should display the default favicon when no thumbnail is available", function() {
-      var view = mountTestComponent({
-        roomName: "Mike's room",
-        roomContextUrls: [{
-          description: "Mark's super page",
-          location: "http://invalid.com",
-          thumbnail: ""
-        }]
-      });
-
-      expect(view.getDOMNode().querySelector(".standalone-context-url > img").src)
-        .to.match(/shared\/img\/icons-16x16.svg#globe$/);
-    });
-  });
-
   describe("StandaloneRoomHeader", function() {
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(
           loop.standaloneRoomViews.StandaloneRoomHeader, {
             dispatcher: dispatcher
           }));
     }
@@ -219,53 +100,16 @@ describe("loop.standaloneRoomViews", fun
         "re-entered", function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
           var view = mountTestComponent();
 
           activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
 
           expectActionDispatched(view);
         });
-
-      it("should updateVideoContainer when the JOINED state is entered", function() {
-          activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
-
-          var view = mountTestComponent();
-
-          sandbox.stub(view, "updateVideoContainer");
-
-          activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
-
-          sinon.assert.calledOnce(view.updateVideoContainer);
-      });
-
-      it("should updateVideoContainer when the JOINED state is re-entered", function() {
-          activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
-
-          var view = mountTestComponent();
-
-          sandbox.stub(view, "updateVideoContainer");
-
-          activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
-
-          sinon.assert.calledOnce(view.updateVideoContainer);
-      });
-
-      it("should reset the video dimensions cache when the gather state is entered", function() {
-        activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});
-
-        var view = mountTestComponent();
-
-        activeRoomStore.setStoreState({roomState: ROOM_STATES.GATHER});
-
-        expect(view._videoDimensionsCache).eql({
-          local: {},
-          remote: {}
-        });
-      });
     });
 
     describe("#publishStream", function() {
       var view;
 
       beforeEach(function() {
         view = mountTestComponent();
         view.setState({
@@ -292,262 +136,16 @@ describe("loop.standaloneRoomViews", fun
         sinon.assert.calledOnce(dispatch);
         sinon.assert.calledWithExactly(dispatch, new sharedActions.SetMute({
           type: "video",
           enabled: true
         }));
       });
     });
 
-    describe("Local Stream Size Position", function() {
-      var view, localElement;
-
-      beforeEach(function() {
-        sandbox.stub(window, "matchMedia").returns({
-          matches: false
-        });
-        activeRoomStore.setStoreState({
-          remoteSrcVideoObject: {},
-          remoteVideoEnabled: true
-        });
-        view = mountTestComponent();
-        localElement = view._getElement(".local");
-      });
-
-      it("should be a quarter of the width of the main stream", function() {
-        sandbox.stub(view, "getRemoteVideoDimensions").returns({
-          streamWidth: 640,
-          offsetX: 0
-        });
-
-        view.updateLocalCameraPosition({
-          width: 1,
-          height: 0.75
-        });
-
-        expect(localElement.style.width).eql("160px");
-        expect(localElement.style.height).eql("120px");
-      });
-
-      it("should be a quarter of the width of the remote view element when there is no stream", function() {
-        activeRoomStore.setStoreState({
-          remoteSrcVideoObject: null,
-          remoteVideoEnabled: false
-        });
-
-        sandbox.stub(view, "getDOMNode").returns({
-          querySelector: function(selector) {
-            if (selector === ".local") {
-              return localElement;
-            }
-
-            return {
-              offsetWidth: 640,
-              offsetLeft: 0
-            };
-          }
-        });
-
-        view.updateLocalCameraPosition({
-          width: 1,
-          height: 0.75
-        });
-
-        expect(localElement.style.width).eql("160px");
-        expect(localElement.style.height).eql("120px");
-      });
-
-      it("should be a quarter of the width reduced for aspect ratio", function() {
-        sandbox.stub(view, "getRemoteVideoDimensions").returns({
-          streamWidth: 640,
-          offsetX: 0
-        });
-
-        view.updateLocalCameraPosition({
-          width: 0.75,
-          height: 1
-        });
-
-        expect(localElement.style.width).eql("120px");
-        expect(localElement.style.height).eql("160px");
-      });
-
-      it("should ensure the height is a minimum of 48px", function() {
-        sandbox.stub(view, "getRemoteVideoDimensions").returns({
-          streamWidth: 180,
-          offsetX: 0
-        });
-
-        view.updateLocalCameraPosition({
-          width: 1,
-          height: 0.75
-        });
-
-        expect(localElement.style.width).eql("64px");
-        expect(localElement.style.height).eql("48px");
-      });
-
-      it("should ensure the width is a minimum of 48px", function() {
-        sandbox.stub(view, "getRemoteVideoDimensions").returns({
-          streamWidth: 180,
-          offsetX: 0
-        });
-
-        view.updateLocalCameraPosition({
-          width: 0.75,
-          height: 1
-        });
-
-        expect(localElement.style.width).eql("48px");
-        expect(localElement.style.height).eql("64px");
-      });
-
-      it("should position the stream to overlap the main stream by a quarter", function() {
-        sandbox.stub(view, "getRemoteVideoDimensions").returns({
-          streamWidth: 640,
-          offsetX: 0
-        });
-
-        view.updateLocalCameraPosition({
-          width: 1,
-          height: 0.75
-        });
-
-        expect(localElement.style.width).eql("160px");
-        expect(localElement.style.left).eql("600px");
-      });
-
-      it("should position the stream to overlap the remote view element when there is no stream", function() {
-        activeRoomStore.setStoreState({
-          remoteSrcVideoObject: null,
-          remoteVideoEnabled: false
-        });
-
-        sandbox.stub(view, "getDOMNode").returns({
-          querySelector: function(selector) {
-            if (selector === ".local") {
-              return localElement;
-            }
-
-            return {
-              offsetWidth: 640,
-              offsetLeft: 0
-            };
-          }
-        });
-
-        view.updateLocalCameraPosition({
-          width: 1,
-          height: 0.75
-        });
-
-        expect(localElement.style.width).eql("160px");
-        expect(localElement.style.left).eql("600px");
-      });
-
-      it("should position the stream to overlap the main stream by a quarter when the aspect ratio is vertical", function() {
-        sandbox.stub(view, "getRemoteVideoDimensions").returns({
-          streamWidth: 640,
-          offsetX: 0
-        });
-
-        view.updateLocalCameraPosition({
-          width: 0.75,
-          height: 1
-        });
-
-        expect(localElement.style.width).eql("120px");
-        expect(localElement.style.left).eql("610px");
-      });
-    });
-
-    describe("Remote Stream Size Position", function() {
-      var view, localElement, remoteElement;
-
-      beforeEach(function() {
-        sandbox.stub(window, "matchMedia").returns({
-          matches: false
-        });
-        view = mountTestComponent();
-
-        localElement = {
-          style: {}
-        };
-        remoteElement = {
-          style: {},
-          removeAttribute: sinon.spy()
-        };
-
-        sandbox.stub(view, "_getElement", function(className) {
-          return className === ".local" ? localElement : remoteElement;
-        });
-
-        view.setState({"receivingScreenShare": true});
-      });
-
-      it("should do nothing if not receiving screenshare", function() {
-        view.setState({"receivingScreenShare": false});
-        remoteElement.style.width = "10px";
-
-        view.updateRemoteCameraPosition({
-          width: 1,
-          height: 0.75
-        });
-
-        expect(remoteElement.style.width).eql("10px");
-      });
-
-      it("should be the same width as the local video", function() {
-        localElement.offsetWidth = 100;
-
-        view.updateRemoteCameraPosition({
-          width: 1,
-          height: 0.75
-        });
-
-        expect(remoteElement.style.width).eql("100px");
-      });
-
-      it("should be the same left edge as the local video", function() {
-        localElement.offsetLeft = 50;
-
-        view.updateRemoteCameraPosition({
-          width: 1,
-          height: 0.75
-        });
-
-        expect(remoteElement.style.left).eql("50px");
-      });
-
-      it("should have a height determined by the aspect ratio", function() {
-        localElement.offsetWidth = 100;
-
-        view.updateRemoteCameraPosition({
-          width: 1,
-          height: 0.75
-        });
-
-        expect(remoteElement.style.height).eql("75px");
-      });
-
-      it("should have the top be set such that the bottom is 10px above the local video", function() {
-        localElement.offsetWidth = 100;
-        localElement.offsetTop = 200;
-
-        view.updateRemoteCameraPosition({
-          width: 1,
-          height: 0.75
-        });
-
-        // 200 (top) - 75 (height) - 10 (spacing) = 115
-        expect(remoteElement.style.top).eql("115px");
-      });
-
-    });
-
     describe("#render", function() {
       var view;
 
       beforeEach(function() {
         view = mountTestComponent();
       });
 
       describe("Empty room message", function() {
@@ -822,24 +420,24 @@ describe("loop.standaloneRoomViews", fun
           function() {
             activeRoomStore.setStoreState({used: false});
             expect(view.getDOMNode().querySelector(".faces")).eql(null);
           });
 
       });
 
       describe("Mute", function() {
-        it("should render local media as audio-only if video is muted",
+        it("should render a local avatar if video is muted",
           function() {
             activeRoomStore.setStoreState({
               roomState: ROOM_STATES.SESSION_CONNECTED,
               videoMuted: true
             });
 
-            expect(view.getDOMNode().querySelector(".local-stream-audio"))
+            expect(view.getDOMNode().querySelector(".local .avatar"))
               .not.eql(null);
           });
 
         it("should render a local avatar if the room HAS_PARTICIPANTS and" +
           " .videoMuted is true",
           function() {
             activeRoomStore.setStoreState({
               roomState: ROOM_STATES.HAS_PARTICIPANTS,
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -265,16 +265,17 @@
     urls: [{
       description: "A wonderful page!",
       location: "http://wonderful.invalid"
       // use the fallback thumbnail
     }]
   }));
 
   loop.store.StoreMixin.register({
+    activeRoomStore: activeRoomStore,
     conversationStore: conversationStore,
     feedbackStore: feedbackStore,
     textChatStore: textChatStore
   });
 
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
@@ -966,16 +967,31 @@
                   React.createElement(StandaloneRoomView, {
                     dispatcher: dispatcher, 
                     activeRoomStore: updatingActiveRoomStore, 
                     roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                     isFirefox: true, 
                     localPosterUrl: "sample-img/video-screen-local.png", 
                     remotePosterUrl: "sample-img/video-screen-remote.png"})
                 )
+            ), 
+
+            React.createElement(FramedExample, {width: 600, height: 480, 
+                           onContentsRendered: updatingSharingRoomStore.forcedUpdate, 
+              summary: "Standalone room convo (has-participants, receivingScreenShare, 600x480)"}, 
+                React.createElement("div", {className: "standalone", cssClass: "standalone"}, 
+                  React.createElement(StandaloneRoomView, {
+                    dispatcher: dispatcher, 
+                    activeRoomStore: updatingSharingRoomStore, 
+                    roomState: ROOM_STATES.HAS_PARTICIPANTS, 
+                    isFirefox: true, 
+                    localPosterUrl: "sample-img/video-screen-local.png", 
+                    remotePosterUrl: "sample-img/video-screen-remote.png", 
+                    screenSharePosterUrl: "sample-img/video-screen-terminal.png"})
+                )
             )
           ), 
 
           React.createElement(Section, {name: "TextChatView (standalone)"}, 
             React.createElement(FramedExample, {width: 200, height: 400, cssClass: "standalone", 
                           summary: "Standalone Text Chat conversation (200 x 400)"}, 
               React.createElement("div", {className: "standalone text-chat-example"}, 
                 React.createElement(TextChatView, {
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -265,16 +265,17 @@
     urls: [{
       description: "A wonderful page!",
       location: "http://wonderful.invalid"
       // use the fallback thumbnail
     }]
   }));
 
   loop.store.StoreMixin.register({
+    activeRoomStore: activeRoomStore,
     conversationStore: conversationStore,
     feedbackStore: feedbackStore,
     textChatStore: textChatStore
   });
 
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
@@ -967,16 +968,31 @@
                     dispatcher={dispatcher}
                     activeRoomStore={updatingActiveRoomStore}
                     roomState={ROOM_STATES.HAS_PARTICIPANTS}
                     isFirefox={true}
                     localPosterUrl="sample-img/video-screen-local.png"
                     remotePosterUrl="sample-img/video-screen-remote.png" />
                 </div>
             </FramedExample>
+
+            <FramedExample width={600} height={480}
+                           onContentsRendered={updatingSharingRoomStore.forcedUpdate}
+              summary="Standalone room convo (has-participants, receivingScreenShare, 600x480)">
+                <div className="standalone" cssClass="standalone">
+                  <StandaloneRoomView
+                    dispatcher={dispatcher}
+                    activeRoomStore={updatingSharingRoomStore}
+                    roomState={ROOM_STATES.HAS_PARTICIPANTS}
+                    isFirefox={true}
+                    localPosterUrl="sample-img/video-screen-local.png"
+                    remotePosterUrl="sample-img/video-screen-remote.png"
+                    screenSharePosterUrl="sample-img/video-screen-terminal.png" />
+                </div>
+            </FramedExample>
           </Section>
 
           <Section name="TextChatView (standalone)">
             <FramedExample width={200} height={400} cssClass="standalone"
                           summary="Standalone Text Chat conversation (200 x 400)">
               <div className="standalone text-chat-example">
                 <TextChatView
                   dispatcher={dispatcher}
--- a/browser/components/sessionstore/test/browser_595601-restore_hidden.js
+++ b/browser/components/sessionstore/test/browser_595601-restore_hidden.js
@@ -9,16 +9,17 @@ let state = {windows:[{tabs:[
   {entries:[{url:"http://example.com#5"}], hidden: true},
   {entries:[{url:"http://example.com#6"}], hidden: true},
   {entries:[{url:"http://example.com#7"}], hidden: true},
   {entries:[{url:"http://example.com#8"}], hidden: true}
 ]}]};
 
 function test() {
   waitForExplicitFinish();
+  requestLongerTimeout(2);
 
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref("browser.sessionstore.restore_hidden_tabs");
   });
 
   // First stage: restoreHiddenTabs = true
   // Second stage: restoreHiddenTabs = false
   test_loadTabs(true, function () {
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -66,16 +66,17 @@ support-files =
 [browser_perf-front-01.js]
 [browser_perf-front-02.js]
 [browser_perf-highlighted.js]
 [browser_perf-jit-view-01.js]
 [browser_perf-jit-view-02.js]
 [browser_perf-loading-01.js]
 [browser_perf-loading-02.js]
 [browser_perf-marker-details-01.js]
+skip-if = os == 'linux' # Bug 1172120
 [browser_perf-options-01.js]
 [browser_perf-options-02.js]
 [browser_perf-options-03.js]
 [browser_perf-options-invert-call-tree-01.js]
 [browser_perf-options-invert-call-tree-02.js]
 [browser_perf-options-invert-flame-graph-01.js]
 [browser_perf-options-invert-flame-graph-02.js]
 [browser_perf-options-flatten-tree-recursion-01.js]
--- a/browser/devtools/styleinspector/ruleview.css
+++ b/browser/devtools/styleinspector/ruleview.css
@@ -26,31 +26,32 @@ body {
 
 .devtools-toolbar {
   width: 100%;
   display: flex;
 }
 
 #pseudo-class-panel {
   position: relative;
-  top: -1px;
+  margin-top: -1px;
+  margin-bottom: -1px;
   overflow-y: hidden;
   max-height: 24px;
-  justify-content: space-around;
   transition-property: max-height;
   transition-duration: 150ms;
   transition-timing-function: ease;
 }
 
 #pseudo-class-panel[hidden] {
   max-height: 0px;
 }
 
 #pseudo-class-panel > label {
   -moz-user-select: none;
+  flex-grow: 1;
 }
 
 .ruleview {
   overflow: auto;
   -moz-user-select: text;
 }
 
 .ruleview-code {
--- a/browser/devtools/styleinspector/test/browser_ruleview_add-rule_01.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_add-rule_01.js
@@ -11,25 +11,27 @@ let PAGE_CONTENT = [
   '<style type="text/css">',
   '  .testclass {',
   '    text-align: center;',
   '  }',
   '</style>',
   '<div id="testid" class="testclass">Styled Node</div>',
   '<span class="testclass2">This is a span</span>',
   '<span class="class1 class2">Multiple classes</span>',
+  '<span class="class3      class4">Multiple classes</span>',
   '<p>Empty<p>',
   '<h1 class="asd@@@@a!!!!:::@asd">Invalid characters in class</h1>',
   '<h2 id="asd@@@a!!2a">Invalid characters in id</h2>'
 ].join("\n");
 
 const TEST_DATA = [
   { node: "#testid", expected: "#testid" },
   { node: ".testclass2", expected: ".testclass2" },
   { node: ".class1.class2", expected: ".class1.class2" },
+  { node: ".class3.class4", expected: ".class3.class4" },
   { node: "p", expected: "p" },
   { node: "h1", expected: ".asd\\@\\@\\@\\@a\\!\\!\\!\\!\\:\\:\\:\\@asd" },
   { node: "h2", expected: "#asd\\@\\@\\@a\\!\\!2a" }
 ];
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(PAGE_CONTENT));
 
--- a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js
@@ -17,17 +17,22 @@ let PAGE_CONTENT = [
   '  }',
   '</style>',
   '<div id="testid">Styled Node</div>',
   '<span class="testclass">This is a span</span>',
   '<div class="testclass2">A</div>',
   '<div id="testid3">B</div>'
 ].join("\n");
 
+const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements";
+
 add_task(function*() {
+  // Expand the pseudo-elements section by default.
+  Services.prefs.setBoolPref(PSEUDO_PREF, true);
+
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(PAGE_CONTENT));
   let {inspector, view} = yield openRuleView();
 
   info("Selecting the test element");
   yield selectNode(".testclass", inspector);
   yield testEditSelector(view, "div:nth-child(1)");
 
   info("Selecting the modified element");
@@ -36,16 +41,19 @@ add_task(function*() {
 
   info("Selecting the test element");
   yield selectNode("#testid3", inspector);
   yield testEditSelector(view, ".testclass2::first-letter");
 
   info("Selecting the modified element");
   yield selectNode(".testclass2", inspector);
   yield checkModifiedElement(view, ".testclass2::first-letter");
+
+  // Reset the pseudo-elements section pref to its default value.
+  Services.prefs.clearUserPref(PSEUDO_PREF);
 });
 
 function* testEditSelector(view, name) {
   info("Test editing existing selector fields");
 
   let idRuleEditor = getRuleViewRuleEditor(view, 1) ||
     getRuleViewRuleEditor(view, 1, 0);
 
--- a/browser/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js
@@ -2,134 +2,139 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test that pseudoelements are displayed correctly in the rule view
 
 const TEST_URI = TEST_URL_ROOT + "doc_pseudoelement.html";
+const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements";
 
 add_task(function*() {
+  Services.prefs.setBoolPref(PSEUDO_PREF, true);
+
   yield addTab(TEST_URI);
-  let {toolbox, inspector, view} = yield openRuleView();
+  let {inspector, view} = yield openRuleView();
 
   yield testTopLeft(inspector, view);
   yield testTopRight(inspector, view);
   yield testBottomRight(inspector, view);
   yield testBottomLeft(inspector, view);
   yield testParagraph(inspector, view);
   yield testBody(inspector, view);
+
+  Services.prefs.clearUserPref(PSEUDO_PREF);
 });
 
 function* testTopLeft(inspector, view) {
   let selector = "#topleft";
-  let {
-    rules,
-    element,
-    elementStyle
-  } = yield assertPseudoElementRulesNumbers(selector, inspector, view, {
+  let {rules} = yield assertPseudoElementRulesNumbers(selector, inspector, view, {
     elementRulesNb: 4,
     firstLineRulesNb: 2,
     firstLetterRulesNb: 1,
     selectionRulesNb: 0
   });
 
   let gutters = assertGutters(view);
 
-  // Make sure that clicking on the twisty hides pseudo elements
+  info("Make sure that clicking on the twisty hides pseudo elements");
   let expander = gutters[0].querySelector(".ruleview-expander");
-  ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are expanded");
+  ok(view.element.firstChild.classList.contains("show-expandable-container"),
+     "Pseudo Elements are expanded");
+
   expander.click();
-  ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are collapsed by twisty");
+  ok(!view.element.firstChild.classList.contains("show-expandable-container"),
+     "Pseudo Elements are collapsed by twisty");
+
   expander.click();
-  ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are expanded again");
+  ok(view.element.firstChild.classList.contains("show-expandable-container"),
+     "Pseudo Elements are expanded again");
 
-  // Make sure that dblclicking on the header container also toggles the pseudo elements
-  EventUtils.synthesizeMouseAtCenter(gutters[0], {clickCount: 2}, inspector.sidebar.getWindowForTab("ruleview"));
-  ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are collapsed by dblclicking");
-
-  let defaultView = element.ownerDocument.defaultView;
+  info("Make sure that dblclicking on the header container also toggles " +
+       "the pseudo elements");
+  EventUtils.synthesizeMouseAtCenter(gutters[0], {clickCount: 2},
+                                     view.doc.defaultView);
+  ok(!view.element.firstChild.classList.contains("show-expandable-container"),
+     "Pseudo Elements are collapsed by dblclicking");
 
   let elementRule = rules.elementRules[0];
   let elementRuleView = getRuleViewRuleEditor(view, 3);
 
   let elementFirstLineRule = rules.firstLineRules[0];
-  let elementFirstLineRuleView = [].filter.call(view.element.children[1].children, (e) => {
+  let elementFirstLineRuleView = [...view.element.children[1].children].filter(e => {
     return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule;
   })[0]._ruleEditor;
 
-  is
-  (
-    convertTextPropsToString(elementFirstLineRule.textProps),
-    "color: orange",
-    "TopLeft firstLine properties are correct"
-  );
+  is(convertTextPropsToString(elementFirstLineRule.textProps),
+     "color: orange",
+     "TopLeft firstLine properties are correct");
 
   let firstProp = elementFirstLineRuleView.addProperty("background-color", "rgb(0, 255, 0)", "");
   let secondProp = elementFirstLineRuleView.addProperty("font-style", "italic", "");
 
-  is (firstProp, elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 2],
-      "First added property is on back of array");
-  is (secondProp, elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 1],
-      "Second added property is on back of array");
+  is(firstProp,
+     elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 2],
+     "First added property is on back of array");
+  is(secondProp,
+     elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 1],
+     "Second added property is on back of array");
 
   yield elementFirstLineRule._applyingModifications;
 
   is((yield getComputedStyleProperty(selector, ":first-line", "background-color")),
-    "rgb(0, 255, 0)", "Added property should have been used.");
+     "rgb(0, 255, 0)", "Added property should have been used.");
   is((yield getComputedStyleProperty(selector, ":first-line", "font-style")),
-    "italic", "Added property should have been used.");
+     "italic", "Added property should have been used.");
   is((yield getComputedStyleProperty(selector, null, "text-decoration")),
-    "none", "Added property should not apply to element");
+     "none", "Added property should not apply to element");
 
   firstProp.setEnabled(false);
   yield elementFirstLineRule._applyingModifications;
 
   is((yield getComputedStyleProperty(selector, ":first-line", "background-color")),
-    "rgb(255, 0, 0)", "Disabled property should now have been used.");
+     "rgb(255, 0, 0)", "Disabled property should now have been used.");
   is((yield getComputedStyleProperty(selector, null, "background-color")),
-    "rgb(221, 221, 221)", "Added property should not apply to element");
+     "rgb(221, 221, 221)", "Added property should not apply to element");
 
   firstProp.setEnabled(true);
   yield elementFirstLineRule._applyingModifications;
 
   is((yield getComputedStyleProperty(selector, ":first-line", "background-color")),
-    "rgb(0, 255, 0)", "Added property should have been used.");
+     "rgb(0, 255, 0)", "Added property should have been used.");
   is((yield getComputedStyleProperty(selector, null, "text-decoration")),
-    "none", "Added property should not apply to element");
+     "none", "Added property should not apply to element");
 
   firstProp = elementRuleView.addProperty("background-color", "rgb(0, 0, 255)", "");
   yield elementRule._applyingModifications;
 
   is((yield getComputedStyleProperty(selector, null, "background-color")),
-    "rgb(0, 0, 255)", "Added property should have been used.");
+     "rgb(0, 0, 255)", "Added property should have been used.");
   is((yield getComputedStyleProperty(selector, ":first-line", "background-color")),
-    "rgb(0, 255, 0)", "Added prop does not apply to pseudo");
+     "rgb(0, 255, 0)", "Added prop does not apply to pseudo");
 }
 
 function* testTopRight(inspector, view) {
-  let {
-    rules,
-    element,
-    elementStyle
-  } = yield assertPseudoElementRulesNumbers("#topright", inspector, view, {
+  yield assertPseudoElementRulesNumbers("#topright", inspector, view, {
     elementRulesNb: 4,
     firstLineRulesNb: 1,
     firstLetterRulesNb: 1,
     selectionRulesNb: 0
   });
 
   let gutters = assertGutters(view);
 
   let expander = gutters[0].querySelector(".ruleview-expander");
-  ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements remain collapsed after switching element");
+  ok(!view.element.firstChild.classList.contains("show-expandable-container"),
+     "Pseudo Elements remain collapsed after switching element");
+
   expander.scrollIntoView();
   expander.click();
-  ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are shown again after clicking twisty");
+  ok(view.element.firstChild.classList.contains("show-expandable-container"),
+     "Pseudo Elements are shown again after clicking twisty");
 }
 
 function* testBottomRight(inspector, view) {
   yield assertPseudoElementRulesNumbers("#bottomright", inspector, view, {
     elementRulesNb: 4,
     firstLineRulesNb: 1,
     firstLetterRulesNb: 1,
     selectionRulesNb: 0
@@ -141,71 +146,46 @@ function* testBottomLeft(inspector, view
     elementRulesNb: 4,
     firstLineRulesNb: 1,
     firstLetterRulesNb: 1,
     selectionRulesNb: 0
   });
 }
 
 function* testParagraph(inspector, view) {
-  let {
-    rules,
-    element,
-    elementStyle
-  } = yield assertPseudoElementRulesNumbers("#bottomleft p", inspector, view, {
+  let {rules} = yield assertPseudoElementRulesNumbers("#bottomleft p", inspector, view, {
     elementRulesNb: 3,
     firstLineRulesNb: 1,
     firstLetterRulesNb: 1,
     selectionRulesNb: 1
   });
 
-  let gutters = assertGutters(view);
+  assertGutters(view);
 
   let elementFirstLineRule = rules.firstLineRules[0];
-  let elementFirstLineRuleView = [].filter.call(view.element.children[1].children, (e) => {
-    return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule;
-  })[0]._ruleEditor;
-
-  is
-  (
-    convertTextPropsToString(elementFirstLineRule.textProps),
-    "background: blue none repeat scroll 0% 0%",
-    "Paragraph first-line properties are correct"
-  );
+  is(convertTextPropsToString(elementFirstLineRule.textProps),
+     "background: blue none repeat scroll 0% 0%",
+     "Paragraph first-line properties are correct");
 
   let elementFirstLetterRule = rules.firstLetterRules[0];
-  let elementFirstLetterRuleView = [].filter.call(view.element.children[1].children, (e) => {
-    return e._ruleEditor && e._ruleEditor.rule === elementFirstLetterRule;
-  })[0]._ruleEditor;
-
-  is
-  (
-    convertTextPropsToString(elementFirstLetterRule.textProps),
-    "color: red; font-size: 130%",
-    "Paragraph first-letter properties are correct"
-  );
+  is(convertTextPropsToString(elementFirstLetterRule.textProps),
+     "color: red; font-size: 130%",
+     "Paragraph first-letter properties are correct");
 
   let elementSelectionRule = rules.selectionRules[0];
-  let elementSelectionRuleView = [].filter.call(view.element.children[1].children, (e) => {
-    return e._ruleEditor && e._ruleEditor.rule === elementSelectionRule;
-  })[0]._ruleEditor;
-
-  is
-  (
-    convertTextPropsToString(elementSelectionRule.textProps),
-    "color: white; background: black none repeat scroll 0% 0%",
-    "Paragraph first-letter properties are correct"
-  );
+  is(convertTextPropsToString(elementSelectionRule.textProps),
+     "color: white; background: black none repeat scroll 0% 0%",
+     "Paragraph first-letter properties are correct");
 }
 
 function* testBody(inspector, view) {
-  let {element, elementStyle} = yield testNode("body", inspector, view);
+  yield testNode("body", inspector, view);
 
-  let gutters = view.element.querySelectorAll(".theme-gutter");
-  is (gutters.length, 0, "There are no gutter headings");
+  let gutters = getGutters(view);
+  is(gutters.length, 0, "There are no gutter headings");
 }
 
 function convertTextPropsToString(textProps) {
   return textProps.map(t => t.name + ": " + t.value).join("; ");
 }
 
 function* testNode(selector, inspector, view) {
   let element = getNode(selector);
@@ -219,29 +199,38 @@ function* assertPseudoElementRulesNumber
 
   let rules = {
     elementRules: elementStyle.rules.filter(rule => !rule.pseudoElement),
     firstLineRules: elementStyle.rules.filter(rule => rule.pseudoElement === ":first-line"),
     firstLetterRules: elementStyle.rules.filter(rule => rule.pseudoElement === ":first-letter"),
     selectionRules: elementStyle.rules.filter(rule => rule.pseudoElement === ":-moz-selection")
   };
 
-  is(rules.elementRules.length, ruleNbs.elementRulesNb, selector +
-    " has the correct number of non pseudo element rules");
-  is(rules.firstLineRules.length, ruleNbs.firstLineRulesNb, selector +
-    " has the correct number of :first-line rules");
-  is(rules.firstLetterRules.length, ruleNbs.firstLetterRulesNb, selector +
-    " has the correct number of :first-letter rules");
-  is(rules.selectionRules.length, ruleNbs.selectionRulesNb, selector +
-    " has the correct number of :selection rules");
+  is(rules.elementRules.length, ruleNbs.elementRulesNb,
+     selector + " has the correct number of non pseudo element rules");
+  is(rules.firstLineRules.length, ruleNbs.firstLineRulesNb,
+     selector + " has the correct number of :first-line rules");
+  is(rules.firstLetterRules.length, ruleNbs.firstLetterRulesNb,
+     selector + " has the correct number of :first-letter rules");
+  is(rules.selectionRules.length, ruleNbs.selectionRulesNb,
+     selector + " has the correct number of :selection rules");
 
-  return {rules: rules, element: element, elementStyle: elementStyle};
+  return {rules, element, elementStyle};
+}
+
+function getGutters(view) {
+  return view.element.querySelectorAll(".theme-gutter");
 }
 
 function assertGutters(view) {
-  let gutters = view.element.querySelectorAll(".theme-gutter");
-  is (gutters.length, 3, "There are 3 gutter headings");
-  is (gutters[0].textContent, "Pseudo-elements", "Gutter heading is correct");
-  is (gutters[1].textContent, "This Element", "Gutter heading is correct");
-  is (gutters[2].textContent, "Inherited from body", "Gutter heading is correct");
+  let gutters = getGutters(view);
+
+  is(gutters.length, 3,
+     "There are 3 gutter headings");
+  is(gutters[0].textContent, "Pseudo-elements",
+     "Gutter heading is correct");
+  is(gutters[1].textContent, "This Element",
+     "Gutter heading is correct");
+  is(gutters[2].textContent, "Inherited from body",
+     "Gutter heading is correct");
 
   return gutters;
 }
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,3 +1,3 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 1.1.165
+Current extension version is: 1.1.215
--- a/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
+++ b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
@@ -156,17 +156,18 @@ function makeContentReadable(obj, window
   return Cu.cloneInto(obj, window);
 }
 
 function createNewChannel(uri, node, principal) {
   return NetUtil.newChannel({
     uri: uri,
     loadingNode: node,
     loadingPrincipal: principal,
-    contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER});
+    contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+  });
 }
 
 function asyncFetchChannel(channel, callback) {
   return NetUtil.asyncFetch(channel, callback);
 }
 
 // PDF data storage
 function PdfDataListener(length) {
--- a/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm
+++ b/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm
@@ -178,17 +178,18 @@ let PdfjsChromeUtils = {
 
   /*
    * Internal
    */
 
   _findbarFromMessage: function(aMsg) {
     let browser = aMsg.target;
     let tabbrowser = browser.getTabBrowser();
-    let tab = tabbrowser.getTabForBrowser(browser);
+    let tab;
+    tab = tabbrowser.getTabForBrowser(browser);
     return tabbrowser.getFindBar(tab);
   },
 
   _updateControlState: function (aMsg) {
     let data = aMsg.data;
     this._findbarFromMessage(aMsg)
         .updateControlState(data.result, data.findPrevious);
   },
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -17,18 +17,18 @@
 /*jshint globalstrict: false */
 /* globals PDFJS */
 
 // Initializing PDFJS global object (if still undefined)
 if (typeof PDFJS === 'undefined') {
   (typeof window !== 'undefined' ? window : this).PDFJS = {};
 }
 
-PDFJS.version = '1.1.165';
-PDFJS.build = '39d2103';
+PDFJS.version = '1.1.215';
+PDFJS.build = 'c9a7498';
 
 (function pdfjsWrapper() {
   // Use strict in our context only - users might not want it
   'use strict';
 
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* Copyright 2012 Mozilla Foundation
@@ -4203,17 +4203,17 @@ var CanvasGraphics = (function CanvasGra
       this.current.font = fontObj;
       this.current.fontSize = size;
 
       if (fontObj.isType3Font) {
         return; // we don't need ctx.font for Type3 fonts
       }
 
       var name = fontObj.loadedName || 'sans-serif';
-      var bold = fontObj.black ? (fontObj.bold ? 'bolder' : 'bold') :
+      var bold = fontObj.black ? (fontObj.bold ? '900' : 'bold') :
                                  (fontObj.bold ? 'bold' : 'normal');
 
       var italic = fontObj.italic ? 'italic' : 'normal';
       var typeface = '"' + name + '", ' + fontObj.fallbackName;
 
       // Some font backends cannot handle fonts below certain size.
       // Keeping the font at minimal size and using the fontSizeScale to change
       // the current transformation matrix before the fillText/strokeText.
@@ -4463,16 +4463,17 @@ var CanvasGraphics = (function CanvasGra
       var glyphsLength = glyphs.length;
       var isTextInvisible =
         current.textRenderingMode === TextRenderingMode.INVISIBLE;
       var i, glyph, width;
 
       if (isTextInvisible || fontSize === 0) {
         return;
       }
+      this.cachedGetSinglePixelWidth = null;
 
       ctx.save();
       ctx.transform.apply(ctx, current.textMatrix);
       ctx.translate(current.x, current.y);
 
       ctx.scale(textHScale, fontDirection);
 
       for (i = 0; i < glyphsLength; ++i) {
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -17,18 +17,18 @@
 /*jshint globalstrict: false */
 /* globals PDFJS */
 
 // Initializing PDFJS global object (if still undefined)
 if (typeof PDFJS === 'undefined') {
   (typeof window !== 'undefined' ? window : this).PDFJS = {};
 }
 
-PDFJS.version = '1.1.165';
-PDFJS.build = '39d2103';
+PDFJS.version = '1.1.215';
+PDFJS.build = 'c9a7498';
 
 (function pdfjsWrapper() {
   // Use strict in our context only - users might not want it
   'use strict';
 
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* Copyright 2012 Mozilla Foundation
@@ -2057,42 +2057,54 @@ var Page = (function PageClosure() {
     this.resourcesPromise = null;
   }
 
   Page.prototype = {
     getPageProp: function Page_getPageProp(key) {
       return this.pageDict.get(key);
     },
 
-    getInheritedPageProp: function Page_inheritPageProp(key) {
-      var dict = this.pageDict;
-      var value = dict.get(key);
-      while (value === undefined) {
+    getInheritedPageProp: function Page_getInheritedPageProp(key) {
+      var dict = this.pageDict, valueArray = null, loopCount = 0;
+      var MAX_LOOP_COUNT = 100;
+      // Always walk up the entire parent chain, to be able to find
+      // e.g. \Resources placed on multiple levels of the tree.
+      while (dict) {
+        var value = dict.get(key);
+        if (value) {
+          if (!valueArray) {
+            valueArray = [];
+          }
+          valueArray.push(value);
+        }
+        if (++loopCount > MAX_LOOP_COUNT) {
+          warn('Page_getInheritedPageProp: maximum loop count exceeded.');
+          break;
+        }
         dict = dict.get('Parent');
-        if (!dict) {
-          break;
-        }
-        value = dict.get(key);
-      }
-      return value;
+      }
+      if (!valueArray) {
+        return Dict.empty;
+      }
+      if (valueArray.length === 1 || !isDict(valueArray[0]) ||
+          loopCount > MAX_LOOP_COUNT) {
+        return valueArray[0];
+      }
+      return Dict.merge(this.xref, valueArray);
     },
 
     get content() {
       return this.getPageProp('Contents');
     },
 
     get resources() {
-      var value = this.getInheritedPageProp('Resources');
       // For robustness: The spec states that a \Resources entry has to be
-      // present, but can be empty. Some document omit it still. In this case
-      // return an empty dictionary:
-      if (value === undefined) {
-        value = Dict.empty;
-      }
-      return shadow(this, 'resources', value);
+      // present, but can be empty. Some document omit it still, in this case
+      // we return an empty dictionary.
+      return shadow(this, 'resources', this.getInheritedPageProp('Resources'));
     },
 
     get mediaBox() {
       var obj = this.getInheritedPageProp('MediaBox');
       // Reset invalid media box to letter size.
       if (!isArray(obj) || obj.length !== 4) {
         obj = LETTER_SIZE_MEDIABOX;
       }
@@ -2355,16 +2367,20 @@ var PDFDocument = (function PDFDocumentC
         Trapped: isName
       });
     }
   };
 
   PDFDocument.prototype = {
     parse: function PDFDocument_parse(recoveryMode) {
       this.setup(recoveryMode);
+      var version = this.catalog.catDict.get('Version');
+      if (isName(version)) {
+        this.pdfFormatVersion = version.name;
+      }
       try {
         // checking if AcroForm is present
         this.acroForm = this.catalog.catDict.get('AcroForm');
         if (this.acroForm) {
           this.xfa = this.acroForm.get('XFA');
           var fields = this.acroForm.get('Fields');
           if ((!fields || !isArray(fields) || fields.length === 0) &&
               !this.xfa) {
@@ -2456,18 +2472,20 @@ var PDFDocument = (function PDFDocumentC
         var MAX_VERSION_LENGTH = 12;
         var version = '', ch;
         while ((ch = stream.getByte()) > 0x20) { // SPACE
           if (version.length >= MAX_VERSION_LENGTH) {
             break;
           }
           version += String.fromCharCode(ch);
         }
-        // removing "%PDF-"-prefix
-        this.pdfFormatVersion = version.substring(5);
+        if (!this.pdfFormatVersion) {
+          // removing "%PDF-"-prefix
+          this.pdfFormatVersion = version.substring(5);
+        }
         return;
       }
       // May not be a PDF file, continue anyway.
     },
     parseStartXRef: function PDFDocument_parseStartXRef() {
       var startXRef = this.startXRef;
       this.xref.setStartXRef(startXRef);
     },
@@ -2734,16 +2752,34 @@ var Dict = (function DictClosure() {
       for (var key in this.map) {
         callback(key, this.get(key));
       }
     }
   };
 
   Dict.empty = new Dict(null);
 
+  Dict.merge = function Dict_merge(xref, dictArray) {
+    var mergedDict = new Dict(xref);
+
+    for (var i = 0, ii = dictArray.length; i < ii; i++) {
+      var dict = dictArray[i];
+      if (!isDict(dict)) {
+        continue;
+      }
+      for (var keyName in dict.map) {
+        if (mergedDict.map[keyName]) {
+          continue;
+        }
+        mergedDict.map[keyName] = dict.map[keyName];
+      }
+    }
+    return mergedDict;
+  };
+
   return Dict;
 })();
 
 var Ref = (function RefClosure() {
   function Ref(num, gen) {
     this.num = num;
     this.gen = gen;
   }
@@ -5208,17 +5244,20 @@ var PDFFunction = (function PDFFunctionC
         var dmax = domain[1];
         if (i < bounds.length) {
           dmax = bounds[i];
         }
 
         var rmin = encode[2 * i];
         var rmax = encode[2 * i + 1];
 
-        tmpBuf[0] = rmin + (v - dmin) * (rmax - rmin) / (dmax - dmin);
+        // Prevent the value from becoming NaN as a result
+        // of division by zero (fixes issue6113.pdf).
+        tmpBuf[0] = dmin === dmax ? rmin :
+                    rmin + (v - dmin) * (rmax - rmin) / (dmax - dmin);
 
         // call the appropriate function
         fns[i](tmpBuf, 0, dest, destOffset);
       };
     },
 
     constructPostScript: function PDFFunction_constructPostScript(fn, dict,
                                                                   xref) {
@@ -6217,19 +6256,19 @@ var ColorSpace = (function ColorSpaceClo
         case 'CMYK':
           return 'DeviceCmykCS';
         case 'Pattern':
           return ['PatternCS', null];
         default:
           error('unrecognized colorspace ' + mode);
       }
     } else if (isArray(cs)) {
-      mode = cs[0].name;
+      mode = xref.fetchIfRef(cs[0]).name;
       this.mode = mode;
-      var numComps, params;
+      var numComps, params, alt;
 
       switch (mode) {
         case 'DeviceGray':
         case 'G':
           return 'DeviceGrayCS';
         case 'DeviceRGB':
         case 'RGB':
           return 'DeviceRgbCS';
@@ -6241,26 +6280,37 @@ var ColorSpace = (function ColorSpaceClo
           return ['CalGrayCS', params];
         case 'CalRGB':
           params = xref.fetchIfRef(cs[1]).getAll();
           return ['CalRGBCS', params];
         case 'ICCBased':
           var stream = xref.fetchIfRef(cs[1]);
           var dict = stream.dict;
           numComps = dict.get('N');
+          alt = dict.get('Alternate');
+          if (alt) {
+            var altIR = ColorSpace.parseToIR(alt, xref, res);
+            // Parse the /Alternate CS to ensure that the number of components
+            // are correct, and also (indirectly) that it is not a PatternCS.
+            var altCS = ColorSpace.fromIR(altIR);
+            if (altCS.numComps === numComps) {
+              return altIR;
+            }
+            warn('ICCBased color space: Ignoring incorrect /Alternate entry.');
+          }
           if (numComps === 1) {
             return 'DeviceGrayCS';
           } else if (numComps === 3) {
             return 'DeviceRgbCS';
           } else if (numComps === 4) {
             return 'DeviceCmykCS';
           }
           break;
         case 'Pattern':
-          var basePatternCS = cs[1];
+          var basePatternCS = xref.fetchIfRef(cs[1]) || null;
           if (basePatternCS) {
             basePatternCS = ColorSpace.parseToIR(basePatternCS, xref, res);
           }
           return ['PatternCS', basePatternCS];
         case 'Indexed':
         case 'I':
           var baseIndexedCS = ColorSpace.parseToIR(cs[1], xref, res);
           var hiVal = cs[2] + 1;
@@ -6273,21 +6323,21 @@ var ColorSpace = (function ColorSpaceClo
         case 'DeviceN':
           var name = cs[1];
           numComps = 1;
           if (isName(name)) {
             numComps = 1;
           } else if (isArray(name)) {
             numComps = name.length;
           }
-          var alt = ColorSpace.parseToIR(cs[2], xref, res);
+          alt = ColorSpace.parseToIR(cs[2], xref, res);
           var tintFnIR = PDFFunction.getIR(xref, xref.fetchIfRef(cs[3]));
           return ['AlternateCS', numComps, alt, tintFnIR];
         case 'Lab':
-          params = cs[1].getAll();
+          params = xref.fetchIfRef(cs[1]).getAll();
           return ['LabCS', params];
         default:
           error('unimplemented color space object "' + mode + '"');
       }
     } else {
       error('unrecognized color space object: "' + cs + '"');
     }
     return null;
@@ -16328,22 +16378,24 @@ var Font = (function FontClosure() {
     }
     if (code >= 0xFFF0 && code <= 0xFFFF) { // Specials Unicode block
       return true;
     }
     switch (code) {
       case 0x7F: // Control char
       case 0xA0: // Non breaking space
       case 0xAD: // Soft hyphen
-      case 0x0E33: // Thai character SARA AM
       case 0x2011: // Non breaking hyphen
       case 0x205F: // Medium mathematical space
       case 0x25CC: // Dotted circle (combining mark)
         return true;
     }
+    if ((code & ~0xFF) === 0x0E00) { // Thai/Lao chars (with combining mark)
+      return true;
+    }
     return false;
   }
 
   /**
    * Rebuilds the char code to glyph ID map by trying to replace the char codes
    * with their unicode value. It also moves char codes that are in known
    * problematic locations.
    * @return {Object} Two properties:
@@ -17757,23 +17809,28 @@ var Font = (function FontClosure() {
       // The 'post' table has glyphs names.
       if (tables.post) {
         var valid = readPostScriptTable(tables.post, properties, numGlyphs);
         if (!valid) {
           tables.post = null;
         }
       }
 
-      var charCodeToGlyphId = [], charCode, toUnicode = properties.toUnicode;
-
-      function hasGlyph(glyphId, charCode) {
+      var charCodeToGlyphId = [], charCode;
+      var toUnicode = properties.toUnicode, widths = properties.widths;
+      var isIdentityUnicode = toUnicode instanceof IdentityToUnicodeMap;
+
+      function hasGlyph(glyphId, charCode, widthCode) {
         if (!missingGlyphs[glyphId]) {
           return true;
         }
-        if (charCode >= 0 && toUnicode.has(charCode)) {
+        if (!isIdentityUnicode && charCode >= 0 && toUnicode.has(charCode)) {
+          return true;
+        }
+        if (widths && widthCode >= 0 && isNum(widths[widthCode])) {
           return true;
         }
         return false;
       }
 
       if (properties.type === 'CIDFontType2') {
         var cidToGidMap = properties.cidToGidMap || [];
         var isCidToGidMapEmpty = cidToGidMap.length === 0;
@@ -17783,17 +17840,17 @@ var Font = (function FontClosure() {
           var glyphId = -1;
           if (isCidToGidMapEmpty) {
             glyphId = charCode;
           } else if (cidToGidMap[cid] !== undefined) {
             glyphId = cidToGidMap[cid];
           }
 
           if (glyphId >= 0 && glyphId < numGlyphs &&
-              hasGlyph(glyphId, charCode)) {
+              hasGlyph(glyphId, charCode, cid)) {
             charCodeToGlyphId[charCode] = glyphId;
           }
         });
         if (dupFirstEntry) {
           charCodeToGlyphId[0] = numGlyphs - 1;
         }
       } else {
         // Most of the following logic in this code branch is based on the
@@ -17844,28 +17901,29 @@ var Font = (function FontClosure() {
             } else if (cmapPlatformId === 1 && cmapEncodingId === 0) {
               // TODO: the encoding needs to be updated with mac os table.
               unicodeOrCharCode = Encodings.MacRomanEncoding.indexOf(glyphName);
             }
 
             var found = false;
             for (i = 0; i < cmapMappingsLength; ++i) {
               if (cmapMappings[i].charCode === unicodeOrCharCode &&
-                  hasGlyph(cmapMappings[i].glyphId, unicodeOrCharCode)) {
+                  hasGlyph(cmapMappings[i].glyphId, unicodeOrCharCode, -1)) {
                 charCodeToGlyphId[charCode] = cmapMappings[i].glyphId;
                 found = true;
                 break;
               }
             }
             if (!found && properties.glyphNames) {
-              // Try to map using the post table. There are currently no known
-              // pdfs that this fixes.
+              // Try to map using the post table.
               var glyphId = properties.glyphNames.indexOf(glyphName);
-              if (glyphId > 0 && hasGlyph(glyphId, -1)) {
+              if (glyphId > 0 && hasGlyph(glyphId, -1, -1)) {
                 charCodeToGlyphId[charCode] = glyphId;
+              } else {
+                charCodeToGlyphId[charCode] = 0; // notdef
               }
             }
           }
         } else if (cmapPlatformId === 0 && cmapEncodingId === 0) {
           // Default Unicode semantics, use the charcodes as is.
           for (i = 0; i < cmapMappingsLength; ++i) {
             charCodeToGlyphId[cmapMappings[i].charCode] =
               cmapMappings[i].glyphId;
--- a/browser/extensions/pdfjs/content/web/viewer.css
+++ b/browser/extensions/pdfjs/content/web/viewer.css
@@ -798,33 +798,33 @@ html[dir='rtl'] .dropdownToolbarButton {
   box-shadow: 0 1px 1px hsla(0,0%,0%,.2) inset,
               0 0 1px hsla(0,0%,0%,.3) inset,
               0 1px 0 hsla(0,0%,100%,.05);
 }
 
 .dropdownToolbarButton {
   width: 120px;
   max-width: 120px;
-  padding: 3px 2px 2px;
+  padding: 0;
   overflow: hidden;
   background: url(images/toolbarButton-menuArrows.png) no-repeat;
 }
 html[dir='ltr'] .dropdownToolbarButton {
   background-position: 95%;
 }
 html[dir='rtl'] .dropdownToolbarButton {
   background-position: 5%;
 }
 
 .dropdownToolbarButton > select {
   min-width: 140px;
   font-size: 12px;
   color: hsl(0,0%,95%);
   margin: 0;
-  padding: 0;
+  padding: 3px 2px 2px;
   border: none;
   background: rgba(0,0,0,0); /* Opera does not support 'transparent' <select> background */
 }
 
 .dropdownToolbarButton > select > option {
   background: hsl(0,0%,24%);
 }
 
--- a/browser/extensions/pdfjs/content/web/viewer.js
+++ b/browser/extensions/pdfjs/content/web/viewer.js
@@ -13,21 +13,21 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 /* globals PDFJS, PDFBug, FirefoxCom, Stats, Cache, ProgressBar,
            DownloadManager, getFileName, getPDFFileNameFromURL,
            PDFHistory, Preferences, SidebarView, ViewHistory, Stats,
            PDFThumbnailViewer, URL, noContextMenuHandler, SecondaryToolbar,
-           PasswordPrompt, PDFPresentationMode, HandTool, Promise,
-           PDFDocumentProperties, PDFOutlineView, PDFAttachmentView,
+           PasswordPrompt, PDFPresentationMode, PDFDocumentProperties, HandTool,
+           Promise, PDFLinkService, PDFOutlineView, PDFAttachmentView,
            OverlayManager, PDFFindController, PDFFindBar, getVisibleElements,
            watchScroll, PDFViewer, PDFRenderingQueue, PresentationModeState,
-           RenderingStates, DEFAULT_SCALE, UNKNOWN_SCALE,
+           parseQueryString, RenderingStates, DEFAULT_SCALE, UNKNOWN_SCALE,
            IGNORE_CURRENT_POSITION_ON_ZOOM: true */
 
 'use strict';
 
 var DEFAULT_URL = 'compressed.tracemonkey-pldi-09.pdf';
 var DEFAULT_SCALE_DELTA = 1.1;
 var MIN_SCALE = 0.25;
 var MAX_SCALE = 10.0;
@@ -204,16 +204,31 @@ function watchScroll(viewAreaElement, ca
   };
 
   var rAF = null;
   viewAreaElement.addEventListener('scroll', debounceScroll, true);
   return state;
 }
 
 /**
+ * Helper function to parse query string (e.g. ?param1=value&parm2=...).
+ */
+function parseQueryString(query) {
+  var parts = query.split('&');
+  var params = {};
+  for (var i = 0, ii = parts.length; i < ii; ++i) {
+    var param = parts[i].split('=');
+    var key = param[0].toLowerCase();
+    var value = param.length > 1 ? param[1] : null;
+    params[decodeURIComponent(key)] = decodeURIComponent(value);
+  }
+  return params;
+}
+
+/**
  * Use binary search to find the index of the first item in a given array which
  * passes a given condition. The items are expected to be sorted in the sense
  * that if the condition is true for one item in the array, then it is also true
  * for all following items.
  *
  * @returns {Number} Index of the first array element to pass the test,
  *                   or |items.length| if no such element exists.
  */
@@ -1325,366 +1340,669 @@ var PDFFindController = (function PDFFin
       }
       this.findBar.updateUIState(state, previous);
     }
   };
   return PDFFindController;
 })();
 
 
-var PDFHistory = {
-  initialized: false,
-  initialDestination: null,
-
+/**
+ * Performs navigation functions inside PDF, such as opening specified page,
+ * or destination.
+ * @class
+ * @implements {IPDFLinkService}
+ */
+var PDFLinkService = (function () {
   /**
-   * @param {string} fingerprint
-   * @param {IPDFLinkService} linkService
+   * @constructs PDFLinkService
    */
-  initialize: function pdfHistoryInitialize(fingerprint, linkService) {
-    this.initialized = true;
-    this.reInitialized = false;
-    this.allowHashChange = true;
-    this.historyUnlocked = true;
-    this.isViewerInPresentationMode = false;
-
-    this.previousHash = window.location.hash.substring(1);
-    this.currentBookmark = '';
-    this.currentPage = 0;
-    this.updatePreviousBookmark = false;
-    this.previousBookmark = '';
-    this.previousPage = 0;
-    this.nextHashParam = '';
-
-    this.fingerprint = fingerprint;
-    this.linkService = linkService;
-    this.currentUid = this.uid = 0;
-    this.current = {};
-
-    var state = window.history.state;
-    if (this._isStateObjectDefined(state)) {
-      // This corresponds to navigating back to the document
-      // from another page in the browser history.
+  function PDFLinkService() {
+    this.baseUrl = null;
+    this.pdfDocument = null;
+    this.pdfViewer = null;
+    this.pdfHistory = null;
+
+    this._pagesRefCache = null;
+  }
+
+  PDFLinkService.prototype = {
+    setDocument: function PDFLinkService_setDocument(pdfDocument, baseUrl) {
+      this.baseUrl = baseUrl;
+      this.pdfDocument = pdfDocument;
+      this._pagesRefCache = Object.create(null);
+    },
+
+    setViewer: function PDFLinkService_setViewer(pdfViewer) {
+      this.pdfViewer = pdfViewer;
+    },
+
+    setHistory: function PDFLinkService_setHistory(pdfHistory) {
+      this.pdfHistory = pdfHistory;
+    },
+
+    /**
+     * @returns {number}
+     */
+    get pagesCount() {
+      return this.pdfDocument.numPages;
+    },
+
+    /**
+     * @returns {number}
+     */
+    get page() {
+      return this.pdfViewer.currentPageNumber;
+    },
+
+    /**
+     * @param {number} value
+     */
+    set page(value) {
+      this.pdfViewer.currentPageNumber = value;
+    },
+
+    /**
+     * @param dest - The PDF destination object.
+     */
+    navigateTo: function PDFLinkService_navigateTo(dest) {
+      var destString = '';
+      var self = this;
+
+      var goToDestination = function(destRef) {
+        // dest array looks like that: <page-ref> </XYZ|FitXXX> <args..>
+        var pageNumber = destRef instanceof Object ?
+          self._pagesRefCache[destRef.num + ' ' + destRef.gen + ' R'] :
+          (destRef + 1);
+        if (pageNumber) {
+          if (pageNumber > self.pagesCount) {
+            pageNumber = self.pagesCount;
+          }
+          self.pdfViewer.scrollPageIntoView(pageNumber, dest);
+
+          if (self.pdfHistory) {
+            // Update the browsing history.
+            self.pdfHistory.push({
+              dest: dest,
+              hash: destString,
+              page: pageNumber
+            });
+          }
+        } else {
+          self.pdfDocument.getPageIndex(destRef).then(function (pageIndex) {
+            var pageNum = pageIndex + 1;
+            var cacheKey = destRef.num + ' ' + destRef.gen + ' R';
+            self._pagesRefCache[cacheKey] = pageNum;
+            goToDestination(destRef);
+          });
+        }
+      };
+
+      var destinationPromise;
+      if (typeof dest === 'string') {
+        destString = dest;
+        destinationPromise = this.pdfDocument.getDestination(dest);
+      } else {
+        destinationPromise = Promise.resolve(dest);
+      }
+      destinationPromise.then(function(destination) {
+        dest = destination;
+        if (!(destination instanceof Array)) {
+          return; // invalid destination
+        }
+        goToDestination(destination[0]);
+      });
+    },
+
+    /**
+     * @param dest - The PDF destination object.
+     * @returns {string} The hyperlink to the PDF object.
+     */
+    getDestinationHash: function PDFLinkService_getDestinationHash(dest) {
+      if (typeof dest === 'string') {
+        return this.getAnchorUrl('#' + escape(dest));
+      }
+      if (dest instanceof Array) {
+        var destRef = dest[0]; // see navigateTo method for dest format
+        var pageNumber = destRef instanceof Object ?
+          this._pagesRefCache[destRef.num + ' ' + destRef.gen + ' R'] :
+          (destRef + 1);
+        if (pageNumber) {
+          var pdfOpenParams = this.getAnchorUrl('#page=' + pageNumber);
+          var destKind = dest[1];
+          if (typeof destKind === 'object' && 'name' in destKind &&
+              destKind.name === 'XYZ') {
+            var scale = (dest[4] || this.pdfViewer.currentScaleValue);
+            var scaleNumber = parseFloat(scale);
+            if (scaleNumber) {
+              scale = scaleNumber * 100;
+            }
+            pdfOpenParams += '&zoom=' + scale;
+            if (dest[2] || dest[3]) {
+              pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0);
+            }
+          }
+          return pdfOpenParams;
+        }
+      }
+      return '';
+    },
+
+    /**
+     * Prefix the full url on anchor links to make sure that links are resolved
+     * relative to the current URL instead of the one defined in <base href>.
+     * @param {String} anchor The anchor hash, including the #.
+     * @returns {string} The hyperlink to the PDF object.
+     */
+    getAnchorUrl: function PDFLinkService_getAnchorUrl(anchor) {
+      return (this.baseUrl || '') + anchor;
+    },
+
+    /**
+     * @param {string} hash
+     */
+    setHash: function PDFLinkService_setHash(hash) {
+      if (hash.indexOf('=') >= 0) {
+        var params = parseQueryString(hash);
+        // borrowing syntax from "Parameters for Opening PDF Files"
+        if ('nameddest' in params) {
+          if (this.pdfHistory) {
+            this.pdfHistory.updateNextHashParam(params.nameddest);
+          }
+          this.navigateTo(params.nameddest);
+          return;
+        }
+        var pageNumber, dest;
+        if ('page' in params) {
+          pageNumber = (params.page | 0) || 1;
+        }
+        if ('zoom' in params) {
+          // Build the destination array.
+          var zoomArgs = params.zoom.split(','); // scale,left,top
+          var zoomArg = zoomArgs[0];
+          var zoomArgNumber = parseFloat(zoomArg);
+
+          if (zoomArg.indexOf('Fit') === -1) {
+            // If the zoomArg is a number, it has to get divided by 100. If it's
+            // a string, it should stay as it is.
+            dest = [null, { name: 'XYZ' },
+                    zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null,
+                    zoomArgs.length > 2 ? (zoomArgs[2] | 0) : null,
+                    (zoomArgNumber ? zoomArgNumber / 100 : zoomArg)];
+          } else {
+            if (zoomArg === 'Fit' || zoomArg === 'FitB') {
+              dest = [null, { name: zoomArg }];
+            } else if ((zoomArg === 'FitH' || zoomArg === 'FitBH') ||
+                       (zoomArg === 'FitV' || zoomArg === 'FitBV')) {
+              dest = [null, { name: zoomArg },
+                      zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null];
+            } else if (zoomArg === 'FitR') {
+              if (zoomArgs.length !== 5) {
+                console.error('PDFLinkService_setHash: ' +
+                              'Not enough parameters for \'FitR\'.');
+              } else {
+                dest = [null, { name: zoomArg },
+                        (zoomArgs[1] | 0), (zoomArgs[2] | 0),
+                        (zoomArgs[3] | 0), (zoomArgs[4] | 0)];
+              }
+            } else {
+              console.error('PDFLinkService_setHash: \'' + zoomArg +
+                            '\' is not a valid zoom value.');
+            }
+          }
+        }
+        if (dest) {
+          this.pdfViewer.scrollPageIntoView(pageNumber || this.page, dest);
+        } else if (pageNumber) {
+          this.page = pageNumber; // simple page
+        }
+        if ('pagemode' in params) {
+          if (params.pagemode === 'thumbs' || params.pagemode === 'bookmarks' ||
+              params.pagemode === 'attachments') {
+            this.switchSidebarView((params.pagemode === 'bookmarks' ?
+                                   'outline' : params.pagemode), true);
+          } else if (params.pagemode === 'none' && this.sidebarOpen) {
+            document.getElementById('sidebarToggle').click();
+          }
+        }
+      } else if (/^\d+$/.test(hash)) { // page number
+        this.page = hash;
+      } else { // named destination
+        if (this.pdfHistory) {
+          this.pdfHistory.updateNextHashParam(unescape(hash));
+        }
+        this.navigateTo(unescape(hash));
+      }
+    },
+
+    /**
+     * @param {string} action
+     */
+    executeNamedAction: function PDFLinkService_executeNamedAction(action) {
+      // See PDF reference, table 8.45 - Named action
+      switch (action) {
+        case 'GoBack':
+          if (this.pdfHistory) {
+            this.pdfHistory.back();
+          }
+          break;
+
+        case 'GoForward':
+          if (this.pdfHistory) {
+            this.pdfHistory.forward();
+          }
+          break;
+
+        case 'NextPage':
+          this.page++;
+          break;
+
+        case 'PrevPage':
+          this.page--;
+          break;
+
+        case 'LastPage':
+          this.page = this.pagesCount;
+          break;
+
+        case 'FirstPage':
+          this.page = 1;
+          break;
+
+        default:
+          break; // No action according to spec
+      }
+
+      var event = document.createEvent('CustomEvent');
+      event.initCustomEvent('namedaction', true, true, {
+        action: action
+      });
+      this.pdfViewer.container.dispatchEvent(event);
+    },
+
+    /**
+     * @param {number} pageNum - page number.
+     * @param {Object} pageRef - reference to the page.
+     */
+    cachePageRef: function PDFLinkService_cachePageRef(pageNum, pageRef) {
+      var refStr = pageRef.num + ' ' + pageRef.gen + ' R';
+      this._pagesRefCache[refStr] = pageNum;
+    }
+  };
+
+  return PDFLinkService;
+})();
+
+
+var PDFHistory = (function () {
+  function PDFHistory(options) {
+    this.linkService = options.linkService;
+
+    this.initialized = false;
+    this.initialDestination = null;
+    this.initialBookmark = null;
+  }
+
+  PDFHistory.prototype = {
+    /**
+     * @param {string} fingerprint
+     * @param {IPDFLinkService} linkService
+     */
+    initialize: function pdfHistoryInitialize(fingerprint) {
+      this.initialized = true;
+      this.reInitialized = false;
+      this.allowHashChange = true;
+      this.historyUnlocked = true;
+      this.isViewerInPresentationMode = false;
+
+      this.previousHash = window.location.hash.substring(1);
+      this.currentBookmark = '';
+      this.currentPage = 0;
+      this.updatePreviousBookmark = false;
+      this.previousBookmark = '';
+      this.previousPage = 0;
+      this.nextHashParam = '';
+
+      this.fingerprint = fingerprint;
+      this.currentUid = this.uid = 0;
+      this.current = {};
+
+      var state = window.history.state;
+      if (this._isStateObjectDefined(state)) {
+        // This corresponds to navigating back to the document
+        // from another page in the browser history.
+        if (state.target.dest) {
+          this.initialDestination = state.target.dest;
+        } else {
+          this.initialBookmark = state.target.hash;
+        }
+        this.currentUid = state.uid;
+        this.uid = state.uid + 1;
+        this.current = state.target;
+      } else {
+        // This corresponds to the loading of a new document.
+        if (state && state.fingerprint &&
+          this.fingerprint !== state.fingerprint) {
+          // Reinitialize the browsing history when a new document
+          // is opened in the web viewer.
+          this.reInitialized = true;
+        }
+        this._pushOrReplaceState({fingerprint: this.fingerprint}, true);
+      }
+
+      var self = this;
+      window.addEventListener('popstate', function pdfHistoryPopstate(evt) {
+        evt.preventDefault();
+        evt.stopPropagation();
+
+        if (!self.historyUnlocked) {
+          return;
+        }
+        if (evt.state) {
+          // Move back/forward in the history.
+          self._goTo(evt.state);
+        } else {
+          // Handle the user modifying the hash of a loaded document.
+          self.previousHash = window.location.hash.substring(1);
+
+          // If the history is empty when the hash changes,
+          // update the previous entry in the browser history.
+          if (self.uid === 0) {
+            var previousParams = (self.previousHash && self.currentBookmark &&
+            self.previousHash !== self.currentBookmark) ?
+            {hash: self.currentBookmark, page: self.currentPage} :
+            {page: 1};
+            self.historyUnlocked = false;
+            self.allowHashChange = false;
+            window.history.back();
+            self._pushToHistory(previousParams, false, true);
+            window.history.forward();
+            self.historyUnlocked = true;
+          }
+          self._pushToHistory({hash: self.previousHash}, false, true);
+          self._updatePreviousBookmark();
+        }
+      }, false);
+
+      function pdfHistoryBeforeUnload() {
+        var previousParams = self._getPreviousParams(null, true);
+        if (previousParams) {
+          var replacePrevious = (!self.current.dest &&
+          self.current.hash !== self.previousHash);
+          self._pushToHistory(previousParams, false, replacePrevious);
+          self._updatePreviousBookmark();
+        }
+        // Remove the event listener when navigating away from the document,
+        // since 'beforeunload' prevents Firefox from caching the document.
+        window.removeEventListener('beforeunload', pdfHistoryBeforeUnload,
+                                   false);
+      }
+
+      window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false);
+
+      window.addEventListener('pageshow', function pdfHistoryPageShow(evt) {
+        // If the entire viewer (including the PDF file) is cached in
+        // the browser, we need to reattach the 'beforeunload' event listener
+        // since the 'DOMContentLoaded' event is not fired on 'pageshow'.
+        window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false);
+      }, false);
+
+      window.addEventListener('presentationmodechanged', function(e) {
+        self.isViewerInPresentationMode = !!e.detail.active;
+      });
+    },
+
+    clearHistoryState: function pdfHistory_clearHistoryState() {
+      this._pushOrReplaceState(null, true);
+    },
+
+    _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) {
+      return (state && state.uid >= 0 &&
+      state.fingerprint && this.fingerprint === state.fingerprint &&
+      state.target && state.target.hash) ? true : false;
+    },
+
+    _pushOrReplaceState: function pdfHistory_pushOrReplaceState(stateObj,
+                                                                replace) {
+      if (replace) {
+      window.history.replaceState(stateObj, '');
+      } else {
+      window.history.pushState(stateObj, '');
+      }
+    },
+
+    get isHashChangeUnlocked() {
+      if (!this.initialized) {
+        return true;
+      }
+      // If the current hash changes when moving back/forward in the history,
+      // this will trigger a 'popstate' event *as well* as a 'hashchange' event.
+      // Since the hash generally won't correspond to the exact the position
+      // stored in the history's state object, triggering the 'hashchange' event
+      // can thus corrupt the browser history.
+      //
+      // When the hash changes during a 'popstate' event, we *only* prevent the
+      // first 'hashchange' event and immediately reset allowHashChange.
+      // If it is not reset, the user would not be able to change the hash.
+
+      var temp = this.allowHashChange;
+      this.allowHashChange = true;
+      return temp;
+    },
+
+    _updatePreviousBookmark: function pdfHistory_updatePreviousBookmark() {
+      if (this.updatePreviousBookmark &&
+        this.currentBookmark && this.currentPage) {
+        this.previousBookmark = this.currentBookmark;
+        this.previousPage = this.currentPage;
+        this.updatePreviousBookmark = false;
+      }
+    },
+
+    updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark,
+                                                                    pageNum) {
+      if (this.initialized) {
+        this.currentBookmark = bookmark.substring(1);
+        this.currentPage = pageNum | 0;
+        this._updatePreviousBookmark();
+      }
+    },
+
+    updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) {
+      if (this.initialized) {
+        this.nextHashParam = param;
+      }
+    },
+
+    push: function pdfHistoryPush(params, isInitialBookmark) {
+      if (!(this.initialized && this.historyUnlocked)) {
+        return;
+      }
+      if (params.dest && !params.hash) {
+        params.hash = (this.current.hash && this.current.dest &&
+        this.current.dest === params.dest) ?
+          this.current.hash :
+          this.linkService.getDestinationHash(params.dest).split('#')[1];
+      }
+      if (params.page) {
+        params.page |= 0;
+      }
+      if (isInitialBookmark) {
+        var target = window.history.state.target;
+        if (!target) {
+          // Invoked when the user specifies an initial bookmark,
+          // thus setting initialBookmark, when the document is loaded.
+          this._pushToHistory(params, false);
+          this.previousHash = window.location.hash.substring(1);
+        }
+        this.updatePreviousBookmark = this.nextHashParam ? false : true;
+        if (target) {
+          // If the current document is reloaded,
+          // avoid creating duplicate entries in the history.
+          this._updatePreviousBookmark();
+        }
+        return;
+      }
+      if (this.nextHashParam) {
+        if (this.nextHashParam === params.hash) {
+          this.nextHashParam = null;
+          this.updatePreviousBookmark = true;
+          return;
+        } else {
+          this.nextHashParam = null;
+        }
+      }
+
+      if (params.hash) {
+        if (this.current.hash) {
+          if (this.current.hash !== params.hash) {
+            this._pushToHistory(params, true);
+          } else {
+            if (!this.current.page && params.page) {
+              this._pushToHistory(params, false, true);
+            }
+            this.updatePreviousBookmark = true;
+          }
+        } else {
+          this._pushToHistory(params, true);
+        }
+      } else if (this.current.page && params.page &&
+        this.current.page !== params.page) {
+        this._pushToHistory(params, true);
+      }
+    },
+
+    _getPreviousParams: function pdfHistory_getPreviousParams(onlyCheckPage,
+                                                              beforeUnload) {
+      if (!(this.currentBookmark && this.currentPage)) {
+        return null;
+      } else if (this.updatePreviousBookmark) {
+        this.updatePreviousBookmark = false;
+      }
+      if (this.uid > 0 && !(this.previousBookmark && this.previousPage)) {
+        // Prevent the history from getting stuck in the current state,
+        // effectively preventing the user from going back/forward in
+        // the history.
+        //
+        // This happens if the current position in the document didn't change
+        // when the history was previously updated. The reasons for this are
+        // either:
+        // 1. The current zoom value is such that the document does not need to,
+        //    or cannot, be scrolled to display the destination.
+        // 2. The previous destination is broken, and doesn't actally point to a
+        //    position within the document.
+        //    (This is either due to a bad PDF generator, or the user making a
+        //     mistake when entering a destination in the hash parameters.)
+        return null;
+      }
+      if ((!this.current.dest && !onlyCheckPage) || beforeUnload) {
+        if (this.previousBookmark === this.currentBookmark) {
+          return null;
+        }
+      } else if (this.current.page || onlyCheckPage) {
+        if (this.previousPage === this.currentPage) {
+          return null;
+        }
+      } else {
+        return null;
+      }
+      var params = {hash: this.currentBookmark, page: this.currentPage};
+      if (this.isViewerInPresentationMode) {
+        params.hash = null;
+      }
+      return params;
+    },
+
+    _stateObj: function pdfHistory_stateObj(params) {
+      return {fingerprint: this.fingerprint, uid: this.uid, target: params};
+    },
+
+    _pushToHistory: function pdfHistory_pushToHistory(params,
+                                                      addPrevious, overwrite) {
+      if (!this.initialized) {
+        return;
+      }
+      if (!params.hash && params.page) {
+        params.hash = ('page=' + params.page);
+      }
+      if (addPrevious && !overwrite) {
+        var previousParams = this._getPreviousParams();
+        if (previousParams) {
+          var replacePrevious = (!this.current.dest &&
+          this.current.hash !== this.previousHash);
+          this._pushToHistory(previousParams, false, replacePrevious);
+        }
+      }
+      this._pushOrReplaceState(this._stateObj(params),
+        (overwrite || this.uid === 0));
+      this.currentUid = this.uid++;
+      this.current = params;
+      this.updatePreviousBookmark = true;
+    },
+
+    _goTo: function pdfHistory_goTo(state) {
+      if (!(this.initialized && this.historyUnlocked &&
+        this._isStateObjectDefined(state))) {
+        return;
+      }
+      if (!this.reInitialized && state.uid < this.currentUid) {
+        var previousParams = this._getPreviousParams(true);
+        if (previousParams) {
+          this._pushToHistory(this.current, false);
+          this._pushToHistory(previousParams, false);
+          this.currentUid = state.uid;
+          window.history.back();
+          return;
+        }
+      }
+      this.historyUnlocked = false;
+
       if (state.target.dest) {
-        this.initialDestination = state.target.dest;
+        this.linkService.navigateTo(state.target.dest);
       } else {
-        linkService.setHash(state.target.hash);
+        this.linkService.setHash(state.target.hash);
       }
       this.currentUid = state.uid;
-      this.uid = state.uid + 1;
+      if (state.uid > this.uid) {
+        this.uid = state.uid;
+      }
       this.current = state.target;
-    } else {
-      // This corresponds to the loading of a new document.
-      if (state && state.fingerprint &&
-          this.fingerprint !== state.fingerprint) {
-        // Reinitialize the browsing history when a new document
-        // is opened in the web viewer.
-        this.reInitialized = true;
-      }
-      this._pushOrReplaceState({ fingerprint: this.fingerprint }, true);
-    }
-
-    var self = this;
-    window.addEventListener('popstate', function pdfHistoryPopstate(evt) {
-      evt.preventDefault();
-      evt.stopPropagation();
-
-      if (!self.historyUnlocked) {
-        return;
-      }
-      if (evt.state) {
-        // Move back/forward in the history.
-        self._goTo(evt.state);
-      } else {
-        // Handle the user modifying the hash of a loaded document.
-        self.previousHash = window.location.hash.substring(1);
-
-        // If the history is empty when the hash changes,
-        // update the previous entry in the browser history.
-        if (self.uid === 0) {
-          var previousParams = (self.previousHash && self.currentBookmark &&
-                                self.previousHash !== self.currentBookmark) ?
-            { hash: self.currentBookmark, page: self.currentPage } :
-            { page: 1 };
-          self.historyUnlocked = false;
-          self.allowHashChange = false;
+      this.updatePreviousBookmark = true;
+
+      var currentHash = window.location.hash.substring(1);
+      if (this.previousHash !== currentHash) {
+        this.allowHashChange = false;
+      }
+      this.previousHash = currentHash;
+
+      this.historyUnlocked = true;
+    },
+
+    back: function pdfHistoryBack() {
+      this.go(-1);
+    },
+
+    forward: function pdfHistoryForward() {
+      this.go(1);
+    },
+
+    go: function pdfHistoryGo(direction) {
+      if (this.initialized && this.historyUnlocked) {
+        var state = window.history.state;
+        if (direction === -1 && state && state.uid > 0) {
           window.history.back();
-          self._pushToHistory(previousParams, false, true);
+        } else if (direction === 1 && state && state.uid < (this.uid - 1)) {
           window.history.forward();
-          self.historyUnlocked = true;
-        }
-        self._pushToHistory({ hash: self.previousHash }, false, true);
-        self._updatePreviousBookmark();
-      }
-    }, false);
-
-    function pdfHistoryBeforeUnload() {
-      var previousParams = self._getPreviousParams(null, true);
-      if (previousParams) {
-        var replacePrevious = (!self.current.dest &&
-                               self.current.hash !== self.previousHash);
-        self._pushToHistory(previousParams, false, replacePrevious);
-        self._updatePreviousBookmark();
-      }
-      // Remove the event listener when navigating away from the document,
-      // since 'beforeunload' prevents Firefox from caching the document.
-      window.removeEventListener('beforeunload', pdfHistoryBeforeUnload, false);
-    }
-    window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false);
-
-    window.addEventListener('pageshow', function pdfHistoryPageShow(evt) {
-      // If the entire viewer (including the PDF file) is cached in the browser,
-      // we need to reattach the 'beforeunload' event listener since
-      // the 'DOMContentLoaded' event is not fired on 'pageshow'.
-      window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false);
-    }, false);
-
-    window.addEventListener('presentationmodechanged', function(e) {
-      self.isViewerInPresentationMode = !!e.detail.active;
-    });
-  },
-
-  _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) {
-    return (state && state.uid >= 0 &&
-            state.fingerprint && this.fingerprint === state.fingerprint &&
-            state.target && state.target.hash) ? true : false;
-  },
-
-  _pushOrReplaceState: function pdfHistory_pushOrReplaceState(stateObj,
-                                                              replace) {
-    if (replace) {
-      window.history.replaceState(stateObj, '');
-    } else {
-      window.history.pushState(stateObj, '');
-    }
-  },
-
-  get isHashChangeUnlocked() {
-    if (!this.initialized) {
-      return true;
-    }
-    // If the current hash changes when moving back/forward in the history,
-    // this will trigger a 'popstate' event *as well* as a 'hashchange' event.
-    // Since the hash generally won't correspond to the exact the position
-    // stored in the history's state object, triggering the 'hashchange' event
-    // can thus corrupt the browser history.
-    //
-    // When the hash changes during a 'popstate' event, we *only* prevent the
-    // first 'hashchange' event and immediately reset allowHashChange.
-    // If it is not reset, the user would not be able to change the hash.
-
-    var temp = this.allowHashChange;
-    this.allowHashChange = true;
-    return temp;
-  },
-
-  _updatePreviousBookmark: function pdfHistory_updatePreviousBookmark() {
-    if (this.updatePreviousBookmark &&
-        this.currentBookmark && this.currentPage) {
-      this.previousBookmark = this.currentBookmark;
-      this.previousPage = this.currentPage;
-      this.updatePreviousBookmark = false;
-    }
-  },
-
-  updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark,
-                                                                  pageNum) {
-    if (this.initialized) {
-      this.currentBookmark = bookmark.substring(1);
-      this.currentPage = pageNum | 0;
-      this._updatePreviousBookmark();
-    }
-  },
-
-  updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) {
-    if (this.initialized) {
-      this.nextHashParam = param;
-    }
-  },
-
-  push: function pdfHistoryPush(params, isInitialBookmark) {
-    if (!(this.initialized && this.historyUnlocked)) {
-      return;
-    }
-    if (params.dest && !params.hash) {
-      params.hash = (this.current.hash && this.current.dest &&
-                     this.current.dest === params.dest) ?
-        this.current.hash :
-        this.linkService.getDestinationHash(params.dest).split('#')[1];
-    }
-    if (params.page) {
-      params.page |= 0;
-    }
-    if (isInitialBookmark) {
-      var target = window.history.state.target;
-      if (!target) {
-        // Invoked when the user specifies an initial bookmark,
-        // thus setting initialBookmark, when the document is loaded.
-        this._pushToHistory(params, false);
-        this.previousHash = window.location.hash.substring(1);
-      }
-      this.updatePreviousBookmark = this.nextHashParam ? false : true;
-      if (target) {
-        // If the current document is reloaded,
-        // avoid creating duplicate entries in the history.
-        this._updatePreviousBookmark();
-      }
-      return;
-    }
-    if (this.nextHashParam) {
-      if (this.nextHashParam === params.hash) {
-        this.nextHashParam = null;
-        this.updatePreviousBookmark = true;
-        return;
-      } else {
-        this.nextHashParam = null;
+        }
       }
     }
-
-    if (params.hash) {
-      if (this.current.hash) {
-        if (this.current.hash !== params.hash) {
-          this._pushToHistory(params, true);
-        } else {
-          if (!this.current.page && params.page) {
-            this._pushToHistory(params, false, true);
-          }
-          this.updatePreviousBookmark = true;
-        }
-      } else {
-        this._pushToHistory(params, true);
-      }
-    } else if (this.current.page && params.page &&
-               this.current.page !== params.page) {
-      this._pushToHistory(params, true);
-    }
-  },
-
-  _getPreviousParams: function pdfHistory_getPreviousParams(onlyCheckPage,
-                                                            beforeUnload) {
-    if (!(this.currentBookmark && this.currentPage)) {
-      return null;
-    } else if (this.updatePreviousBookmark) {
-      this.updatePreviousBookmark = false;
-    }
-    if (this.uid > 0 && !(this.previousBookmark && this.previousPage)) {
-      // Prevent the history from getting stuck in the current state,
-      // effectively preventing the user from going back/forward in the history.
-      //
-      // This happens if the current position in the document didn't change when
-      // the history was previously updated. The reasons for this are either:
-      // 1. The current zoom value is such that the document does not need to,
-      //    or cannot, be scrolled to display the destination.
-      // 2. The previous destination is broken, and doesn't actally point to a
-      //    position within the document.
-      //    (This is either due to a bad PDF generator, or the user making a
-      //     mistake when entering a destination in the hash parameters.)
-      return null;
-    }
-    if ((!this.current.dest && !onlyCheckPage) || beforeUnload) {
-      if (this.previousBookmark === this.currentBookmark) {
-        return null;
-      }
-    } else if (this.current.page || onlyCheckPage) {
-      if (this.previousPage === this.currentPage) {
-        return null;
-      }
-    } else {
-      return null;
-    }
-    var params = { hash: this.currentBookmark, page: this.currentPage };
-    if (this.isViewerInPresentationMode) {
-      params.hash = null;
-    }
-    return params;
-  },
-
-  _stateObj: function pdfHistory_stateObj(params) {
-    return { fingerprint: this.fingerprint, uid: this.uid, target: params };
-  },
-
-  _pushToHistory: function pdfHistory_pushToHistory(params,
-                                                    addPrevious, overwrite) {
-    if (!this.initialized) {
-      return;
-    }
-    if (!params.hash && params.page) {
-      params.hash = ('page=' + params.page);
-    }
-    if (addPrevious && !overwrite) {
-      var previousParams = this._getPreviousParams();
-      if (previousParams) {
-        var replacePrevious = (!this.current.dest &&
-                               this.current.hash !== this.previousHash);
-        this._pushToHistory(previousParams, false, replacePrevious);
-      }
-    }
-    this._pushOrReplaceState(this._stateObj(params),
-                             (overwrite || this.uid === 0));
-    this.currentUid = this.uid++;
-    this.current = params;
-    this.updatePreviousBookmark = true;
-  },
-
-  _goTo: function pdfHistory_goTo(state) {
-    if (!(this.initialized && this.historyUnlocked &&
-          this._isStateObjectDefined(state))) {
-      return;
-    }
-    if (!this.reInitialized && state.uid < this.currentUid) {
-      var previousParams = this._getPreviousParams(true);
-      if (previousParams) {
-        this._pushToHistory(this.current, false);
-        this._pushToHistory(previousParams, false);
-        this.currentUid = state.uid;
-        window.history.back();
-        return;
-      }
-    }
-    this.historyUnlocked = false;
-
-    if (state.target.dest) {
-      this.linkService.navigateTo(state.target.dest);
-    } else {
-      this.linkService.setHash(state.target.hash);
-    }
-    this.currentUid = state.uid;
-    if (state.uid > this.uid) {
-      this.uid = state.uid;
-    }
-    this.current = state.target;
-    this.updatePreviousBookmark = true;
-
-    var currentHash = window.location.hash.substring(1);
-    if (this.previousHash !== currentHash) {
-      this.allowHashChange = false;
-    }
-    this.previousHash = currentHash;
-
-    this.historyUnlocked = true;
-  },
-
-  back: function pdfHistoryBack() {
-    this.go(-1);
-  },
-
-  forward: function pdfHistoryForward() {
-    this.go(1);
-  },
-
-  go: function pdfHistoryGo(direction) {
-    if (this.initialized && this.historyUnlocked) {
-      var state = window.history.state;
-      if (direction === -1 && state && state.uid > 0) {
-        window.history.back();
-      } else if (direction === 1 && state && state.uid < (this.uid - 1)) {
-        window.history.forward();
-      }
-    }
-  }
-};
+  };
+
+  return PDFHistory;
+})();
 
 
 var SecondaryToolbar = {
   opened: false,
   previousContainerHeight: null,
   newContainerHeight: null,
 
   initialize: function secondaryToolbarInitialize(options) {
@@ -4311,17 +4629,16 @@ var PDFViewer = (function pdfViewer() {
       }
 
       this.pdfDocument = pdfDocument;
       if (!pdfDocument) {
         return;
       }
 
       var pagesCount = pdfDocument.numPages;
-      var pagesRefMap = this.pagesRefMap = {};
       var self = this;
 
       var resolvePagesPromise;
       var pagesPromise = new Promise(function (resolve) {
         resolvePagesPromise = resolve;
       });
       this.pagesPromise = pagesPromise;
       pagesPromise.then(function () {
@@ -4376,30 +4693,31 @@ var PDFViewer = (function pdfViewer() {
             renderingQueue: this.renderingQueue,
             textLayerFactory: textLayerFactory,
             annotationsLayerFactory: this
           });
           bindOnAfterAndBeforeDraw(pageView);
           this._pages.push(pageView);
         }
 
+        var linkService = this.linkService;
+
         // Fetch all the pages since the viewport is needed before printing
         // starts to create the correct size canvas. Wait until one page is
         // rendered so we don't tie up too many resources early on.
         onePageRendered.then(function () {
           if (!PDFJS.disableAutoFetch) {
             var getPagesLeft = pagesCount;
             for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) {
               pdfDocument.getPage(pageNum).then(function (pageNum, pdfPage) {
                 var pageView = self._pages[pageNum - 1];
                 if (!pageView.pdfPage) {
                   pageView.setPdfPage(pdfPage);
                 }
-                var refStr = pdfPage.ref.num + ' ' + pdfPage.ref.gen + ' R';
-                pagesRefMap[refStr] = pageNum;
+                linkService.cachePageRef(pageNum, pdfPage.ref);
                 getPagesLeft--;
                 if (!getPagesLeft) {
                   resolvePagesPromise();
                 }
               }.bind(null, pageNum));
             }
           } else {
             // XXX: Printing is semi-broken with auto fetch disabled.
@@ -4876,16 +5194,21 @@ var SimpleLinkService = (function Simple
     /**
      * @param {string} hash
      */
     setHash: function (hash) {},
     /**
      * @param {string} action
      */
     executeNamedAction: function (action) {},
+    /**
+     * @param {number} pageNum - page number.
+     * @param {Object} pageRef - reference to the page.
+     */
+    cachePageRef: function (pageNum, pageRef) {}
   };
   return SimpleLinkService;
 })();
 
 
 var THUMBNAIL_SCROLL_MARGIN = -19;
 
 
@@ -5553,31 +5876,36 @@ var PDFAttachmentView = (function PDFAtt
   };
 
   return PDFAttachmentView;
 })();
 
 
 var PDFViewerApplication = {
   initialBookmark: document.location.hash.substring(1),
+  initialDestination: null,
   initialized: false,
   fellback: false,
   pdfDocument: null,
   sidebarOpen: false,
   printing: false,
   /** @type {PDFViewer} */
   pdfViewer: null,
   /** @type {PDFThumbnailViewer} */
   pdfThumbnailViewer: null,
   /** @type {PDFRenderingQueue} */
   pdfRenderingQueue: null,
   /** @type {PDFPresentationMode} */
   pdfPresentationMode: null,
   /** @type {PDFDocumentProperties} */
   pdfDocumentProperties: null,
+  /** @type {PDFLinkService} */
+  pdfLinkService: null,
+  /** @type {PDFHistory} */
+  pdfHistory: null,
   pageRotation: 0,
   updateScaleControls: true,
   isInitialViewSet: false,
   animationStartedPromise: null,
   preferenceSidebarViewOnLoad: SidebarView.NONE,
   preferencePdfBugEnabled: false,
   preferenceShowPreviousViewOnLoad: true,
   preferenceDefaultZoomValue: '',
@@ -5585,36 +5913,45 @@ var PDFViewerApplication = {
   url: '',
 
   // called once when the document is loaded
   initialize: function pdfViewInitialize() {
     var pdfRenderingQueue = new PDFRenderingQueue();
     pdfRenderingQueue.onIdle = this.cleanup.bind(this);
     this.pdfRenderingQueue = pdfRenderingQueue;
 
+    var pdfLinkService = new PDFLinkService();
+    this.pdfLinkService = pdfLinkService;
+
     var container = document.getElementById('viewerContainer');
     var viewer = document.getElementById('viewer');
     this.pdfViewer = new PDFViewer({
       container: container,
       viewer: viewer,
       renderingQueue: pdfRenderingQueue,
-      linkService: this
+      linkService: pdfLinkService
     });
     pdfRenderingQueue.setViewer(this.pdfViewer);
+    pdfLinkService.setViewer(this.pdfViewer);
 
     var thumbnailContainer = document.getElementById('thumbnailView');
     this.pdfThumbnailViewer = new PDFThumbnailViewer({
       container: thumbnailContainer,
       renderingQueue: pdfRenderingQueue,
-      linkService: this
+      linkService: pdfLinkService
     });
     pdfRenderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
 
     Preferences.initialize();
 
+    this.pdfHistory = new PDFHistory({
+      linkService: pdfLinkService
+    });
+    pdfLinkService.setHistory(this.pdfHistory);
+
     this.findController = new PDFFindController({
       pdfViewer: this.pdfViewer,
       integratedFind: this.supportsIntegratedFind
     });
     this.pdfViewer.setFindController(this.findController);
 
     this.findBar = new PDFFindBar({
       bar: document.getElementById('findbar'),
@@ -5771,21 +6108,21 @@ var PDFViewerApplication = {
     return this.pdfViewer.currentScaleValue;
   },
 
   get pagesCount() {
     return this.pdfDocument.numPages;
   },
 
   set page(val) {
-    this.pdfViewer.currentPageNumber = val;
+    this.pdfLinkService.page = val;
   },
 
-  get page() {
-    return this.pdfViewer.currentPageNumber;
+  get page() { // TODO remove
+    return this.pdfLinkService.page;
   },
 
   get supportsPrinting() {
     var canvas = document.createElement('canvas');
     var value = 'mozPrintCallback' in canvas;
 
     return PDFJS.shadow(this, 'supportsPrinting', value);
   },
@@ -5924,16 +6261,17 @@ var PDFViewerApplication = {
       return;
     }
 
     this.pdfDocument.destroy();
     this.pdfDocument = null;
 
     this.pdfThumbnailViewer.setDocument(null);
     this.pdfViewer.setDocument(null);
+    this.pdfLinkService.setDocument(null, null);
 
     if (typeof PDFBug !== 'undefined') {
       PDFBug.cleanup();
     }
   },
 
   // TODO(mack): This function signature should really be pdfViewOpen(url, args)
   open: function pdfViewOpen(file, scale, password,
@@ -6056,140 +6394,16 @@ var PDFViewerApplication = {
       function response(download) {
         if (!download) {
           return;
         }
         PDFViewerApplication.download();
       });
   },
 
-  navigateTo: function pdfViewNavigateTo(dest) {
-    var destString = '';
-    var self = this;
-
-    var goToDestination = function(destRef) {
-      self.pendingRefStr = null;
-      // dest array looks like that: <page-ref> </XYZ|FitXXX> <args..>
-      var pageNumber = destRef instanceof Object ?
-        self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] :
-        (destRef + 1);
-      if (pageNumber) {
-        if (pageNumber > self.pagesCount) {
-          pageNumber = self.pagesCount;
-        }
-        self.pdfViewer.scrollPageIntoView(pageNumber, dest);
-
-        // Update the browsing history.
-        PDFHistory.push({ dest: dest, hash: destString, page: pageNumber });
-      } else {
-        self.pdfDocument.getPageIndex(destRef).then(function (pageIndex) {
-          var pageNum = pageIndex + 1;
-          self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] = pageNum;
-          goToDestination(destRef);
-        });
-      }
-    };
-
-    var destinationPromise;
-    if (typeof dest === 'string') {
-      destString = dest;
-      destinationPromise = this.pdfDocument.getDestination(dest);
-    } else {
-      destinationPromise = Promise.resolve(dest);
-    }
-    destinationPromise.then(function(destination) {
-      dest = destination;
-      if (!(destination instanceof Array)) {
-        return; // invalid destination
-      }
-      goToDestination(destination[0]);
-    });
-  },
-
-  executeNamedAction: function pdfViewExecuteNamedAction(action) {
-    // See PDF reference, table 8.45 - Named action
-    switch (action) {
-      case 'GoToPage':
-        document.getElementById('pageNumber').focus();
-        break;
-
-      case 'GoBack':
-        PDFHistory.back();
-        break;
-
-      case 'GoForward':
-        PDFHistory.forward();
-        break;
-
-      case 'Find':
-        if (!this.supportsIntegratedFind) {
-          this.findBar.toggle();
-        }
-        break;
-
-      case 'NextPage':
-        this.page++;
-        break;
-
-      case 'PrevPage':
-        this.page--;
-        break;
-
-      case 'LastPage':
-        this.page = this.pagesCount;
-        break;
-
-      case 'FirstPage':
-        this.page = 1;
-        break;
-
-      default:
-        break; // No action according to spec
-    }
-  },
-
-  getDestinationHash: function pdfViewGetDestinationHash(dest) {
-    if (typeof dest === 'string') {
-      return this.getAnchorUrl('#' + escape(dest));
-    }
-    if (dest instanceof Array) {
-      var destRef = dest[0]; // see navigateTo method for dest format
-      var pageNumber = destRef instanceof Object ?
-        this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] :
-        (destRef + 1);
-      if (pageNumber) {
-        var pdfOpenParams = this.getAnchorUrl('#page=' + pageNumber);
-        var destKind = dest[1];
-        if (typeof destKind === 'object' && 'name' in destKind &&
-            destKind.name === 'XYZ') {
-          var scale = (dest[4] || this.currentScaleValue);
-          var scaleNumber = parseFloat(scale);
-          if (scaleNumber) {
-            scale = scaleNumber * 100;
-          }
-          pdfOpenParams += '&zoom=' + scale;
-          if (dest[2] || dest[3]) {
-            pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0);
-          }
-        }
-        return pdfOpenParams;
-      }
-    }
-    return '';
-  },
-
-  /**
-   * Prefix the full url on anchor links to make sure that links are resolved
-   * relative to the current URL instead of the one defined in <base href>.
-   * @param {String} anchor The anchor hash, including the #.
-   */
-  getAnchorUrl: function getAnchorUrl(anchor) {
-    return this.url.split('#')[0] + anchor;
-  },
-
   /**
    * Show the error box.
    * @param {String} message A message that is human readable.
    * @param {Object} moreInfo (optional) Further information about the error
    *                            that is more technical.  Should have a 'message'
    *                            and optionally a 'stack' property.
    */
   error: function pdfViewError(message, moreInfo) {
@@ -6269,45 +6483,53 @@ var PDFViewerApplication = {
     var pagesCount = pdfDocument.numPages;
     document.getElementById('numPages').textContent =
       mozL10n.get('page_of', {pageCount: pagesCount}, 'of {{pageCount}}');
     document.getElementById('pageNumber').max = pagesCount;
 
     var id = this.documentFingerprint = pdfDocument.fingerprint;
     var store = this.store = new ViewHistory(id);
 
+    var baseDocumentUrl = this.url.split('#')[0];
+    this.pdfLinkService.setDocument(pdfDocument, baseDocumentUrl);
+
     var pdfViewer = this.pdfViewer;
     pdfViewer.currentScale = scale;
     pdfViewer.setDocument(pdfDocument);
     var firstPagePromise = pdfViewer.firstPagePromise;
     var pagesPromise = pdfViewer.pagesPromise;
     var onePageRendered = pdfViewer.onePageRendered;
 
     this.pageRotation = 0;
     this.isInitialViewSet = false;
-    this.pagesRefMap = pdfViewer.pagesRefMap;
 
     this.pdfThumbnailViewer.setDocument(pdfDocument);
 
     firstPagePromise.then(function(pdfPage) {
       downloadedPromise.then(function () {
         var event = document.createEvent('CustomEvent');
         event.initCustomEvent('documentload', true, true, {});
         window.dispatchEvent(event);
       });
 
       self.loadingBar.setWidth(document.getElementById('viewer'));
 
       if (!PDFJS.disableHistory && !self.isViewerEmbedded) {
         // The browsing history is only enabled when the viewer is standalone,
         // i.e. not when it is embedded in a web page.
-        if (!self.preferenceShowPreviousViewOnLoad && window.history.state) {
-          window.history.replaceState(null, '');
-        }
-        PDFHistory.initialize(self.documentFingerprint, self);
+        if (!self.preferenceShowPreviousViewOnLoad) {
+          self.pdfHistory.clearHistoryState();
+        }
+        self.pdfHistory.initialize(self.documentFingerprint);
+
+        if (self.pdfHistory.initialDestination) {
+          self.initialDestination = self.pdfHistory.initialDestination;
+        } else if (self.pdfHistory.initialBookmark) {
+          self.initialBookmark = self.pdfHistory.initialBookmark;
+        }
       }
 
       store.initializedPromise.then(function resolved() {
         var storedHash = null;
         if (self.preferenceShowPreviousViewOnLoad &&
             store.get('exists', false)) {
           var pageNum = store.get('page', '1');
           var zoom = self.preferenceDefaultZoomValue ||
@@ -6358,17 +6580,17 @@ var PDFViewerApplication = {
     // outline depends on pagesRefMap
     var promises = [pagesPromise, this.animationStartedPromise];
     Promise.all(promises).then(function() {
       pdfDocument.getOutline().then(function(outline) {
         var container = document.getElementById('outlineView');
         self.outline = new PDFOutlineView({
           container: container,
           outline: outline,
-          linkService: self
+          linkService: self.pdfLinkService
         });
         self.outline.render();
         document.getElementById('viewOutline').disabled = !outline;
 
         if (!outline && !container.classList.contains('hidden')) {
           self.switchSidebarView('thumbs');
         }
         if (outline &&
@@ -6469,25 +6691,25 @@ var PDFViewerApplication = {
     this.isInitialViewSet = true;
 
     // When opening a new file (when one is already loaded in the viewer):
     // Reset 'currentPageNumber', since otherwise the page's scale will be wrong
     // if 'currentPageNumber' is larger than the number of pages in the file.
     document.getElementById('pageNumber').value =
       this.pdfViewer.currentPageNumber = 1;
 
-    if (PDFHistory.initialDestination) {
-      this.navigateTo(PDFHistory.initialDestination);
-      PDFHistory.initialDestination = null;
+    if (this.initialDestination) {
+      this.pdfLinkService.navigateTo(this.initialDestination);
+      this.initialDestination = null;
     } else if (this.initialBookmark) {
-      this.setHash(this.initialBookmark);
-      PDFHistory.push({ hash: this.initialBookmark }, !!this.initialBookmark);
+      this.pdfLinkService.setHash(this.initialBookmark);
+      this.pdfHistory.push({ hash: this.initialBookmark }, true);
       this.initialBookmark = null;
     } else if (storedHash) {
-      this.setHash(storedHash);
+      this.pdfLinkService.setHash(storedHash);
     } else if (scale) {
       this.setScale(scale, true);
       this.page = 1;
     }
 
     if (this.pdfViewer.currentScale === UNKNOWN_SCALE) {
       // Scale was not initialized: invalid bookmark or scale was not specified.
       // Setting the default one.
@@ -6502,94 +6724,16 @@ var PDFViewerApplication = {
   },
 
   forceRendering: function pdfViewForceRendering() {
     this.pdfRenderingQueue.printing = this.printing;
     this.pdfRenderingQueue.isThumbnailViewEnabled = this.sidebarOpen;
     this.pdfRenderingQueue.renderHighestPriority();
   },
 
-  setHash: function pdfViewSetHash(hash) {
-    if (!this.isInitialViewSet) {
-      this.initialBookmark = hash;
-      return;
-    }
-    if (!hash) {
-      return;
-    }
-
-    if (hash.indexOf('=') >= 0) {
-      var params = this.parseQueryString(hash);
-      // borrowing syntax from "Parameters for Opening PDF Files"
-      if ('nameddest' in params) {
-        PDFHistory.updateNextHashParam(params.nameddest);
-        this.navigateTo(params.nameddest);
-        return;
-      }
-      var pageNumber, dest;
-      if ('page' in params) {
-        pageNumber = (params.page | 0) || 1;
-      }
-      if ('zoom' in params) {
-        // Build the destination array.
-        var zoomArgs = params.zoom.split(','); // scale,left,top
-        var zoomArg = zoomArgs[0];
-        var zoomArgNumber = parseFloat(zoomArg);
-
-        if (zoomArg.indexOf('Fit') === -1) {
-          // If the zoomArg is a number, it has to get divided by 100. If it's
-          // a string, it should stay as it is.
-          dest = [null, { name: 'XYZ' },
-                  zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null,
-                  zoomArgs.length > 2 ? (zoomArgs[2] | 0) : null,
-                  (zoomArgNumber ? zoomArgNumber / 100 : zoomArg)];
-        } else {
-          if (zoomArg === 'Fit' || zoomArg === 'FitB') {
-            dest = [null, { name: zoomArg }];
-          } else if ((zoomArg === 'FitH' || zoomArg === 'FitBH') ||
-                     (zoomArg === 'FitV' || zoomArg === 'FitBV')) {
-            dest = [null, { name: zoomArg },
-                    zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null];
-          } else if (zoomArg === 'FitR') {
-            if (zoomArgs.length !== 5) {
-              console.error('pdfViewSetHash: ' +
-                            'Not enough parameters for \'FitR\'.');
-            } else {
-              dest = [null, { name: zoomArg },
-                      (zoomArgs[1] | 0), (zoomArgs[2] | 0),
-                      (zoomArgs[3] | 0), (zoomArgs[4] | 0)];
-            }
-          } else {
-            console.error('pdfViewSetHash: \'' + zoomArg +
-                          '\' is not a valid zoom value.');
-          }
-        }
-      }
-      if (dest) {
-        this.pdfViewer.scrollPageIntoView(pageNumber || this.page, dest);
-      } else if (pageNumber) {
-        this.page = pageNumber; // simple page
-      }
-      if ('pagemode' in params) {
-        if (params.pagemode === 'thumbs' || params.pagemode === 'bookmarks' ||
-            params.pagemode === 'attachments') {
-          this.switchSidebarView((params.pagemode === 'bookmarks' ?
-                                  'outline' : params.pagemode), true);
-        } else if (params.pagemode === 'none' && this.sidebarOpen) {
-          document.getElementById('sidebarToggle').click();
-        }
-      }
-    } else if (/^\d+$/.test(hash)) { // page number
-      this.page = hash;
-    } else { // named destination
-      PDFHistory.updateNextHashParam(unescape(hash));
-      this.navigateTo(unescape(hash));
-    }
-  },
-
   refreshThumbnailViewer: function pdfViewRefreshThumbnailViewer() {
     var pdfViewer = this.pdfViewer;
     var thumbnailViewer = this.pdfThumbnailViewer;
 
     // set thumbnail images of rendered pages
     var pagesCount = pdfViewer.pagesCount;
     for (var pageIndex = 0; pageIndex < pagesCount; pageIndex++) {
       var pageView = pdfViewer.getPageView(pageIndex);
@@ -6655,40 +6799,27 @@ var PDFViewerApplication = {
 
         if (attachmentsButton.getAttribute('disabled')) {
           return;
         }
         break;
     }
   },
 
-  // Helper function to parse query string (e.g. ?param1=value&parm2=...).
-  parseQueryString: function pdfViewParseQueryString(query) {
-    var parts = query.split('&');
-    var params = {};
-    for (var i = 0, ii = parts.length; i < ii; ++i) {
-      var param = parts[i].split('=');
-      var key = param[0].toLowerCase();
-      var value = param.length > 1 ? param[1] : null;
-      params[decodeURIComponent(key)] = decodeURIComponent(value);
-    }
-    return params;
-  },
-
   beforePrint: function pdfViewSetupBeforePrint() {
     if (!this.supportsPrinting) {
       var printMessage = mozL10n.get('printing_not_supported', null,
           'Warning: Printing is not fully supported by this browser.');
       this.error(printMessage);
       return;
     }
 
     var alertNotReady = false;
     var i, ii;
-    if (!this.pagesCount) {
+    if (!this.pdfDocument || !this.pagesCount) {
       alertNotReady = true;
     } else {
       for (i = 0, ii = this.pagesCount; i < ii; ++i) {
         if (!this.pdfViewer.getPageView(i).pdfPage) {
           alertNotReady = true;
           break;
         }
       }
@@ -6816,17 +6947,17 @@ function webViewerInitialized() {
 
   document.getElementById('openFile').setAttribute('hidden', 'true');
   document.getElementById('secondaryOpenFile').setAttribute('hidden', 'true');
 
 
   if (PDFViewerApplication.preferencePdfBugEnabled) {
     // Special debugging flags in the hash section of the URL.
     var hash = document.location.hash.substring(1);
-    var hashParams = PDFViewerApplication.parseQueryString(hash);
+    var hashParams = parseQueryString(hash);
 
     if ('disableworker' in hashParams) {
       PDFJS.disableWorker = (hashParams['disableworker'] === 'true');
     }
     if ('disablerange' in hashParams) {
       PDFJS.disableRange = (hashParams['disablerange'] === 'true');
     }
     if ('disablestream' in hashParams) {
@@ -7050,16 +7181,33 @@ document.addEventListener('textlayerrend
     console.error(mozL10n.get('document_colors_disabled', null,
       'PDF documents are not allowed to use their own colors: ' +
       '\'Allow pages to choose their own colors\' ' +
       'is deactivated in the browser.'));
     PDFViewerApplication.fallback();
   }
 }, true);
 
+document.addEventListener('namedaction', function (e) {
+  // Processing couple of named actions that might be useful.
+  // See also PDFLinkService.executeNamedAction
+  var action = e.action;
+  switch (action) {
+    case 'GoToPage':
+      document.getElementById('pageNumber').focus();
+      break;
+
+    case 'Find':
+      if (!this.supportsIntegratedFind) {
+        this.findBar.toggle();
+      }
+      break;
+  }
+}, true);
+
 window.addEventListener('presentationmodechanged', function (e) {
   var active = e.detail.active;
   var switchInProgress = e.detail.switchInProgress;
   PDFViewerApplication.pdfViewer.presentationModeState =
     switchInProgress ? PresentationModeState.CHANGING :
     active ? PresentationModeState.FULLSCREEN : PresentationModeState.NORMAL;
 });
 
@@ -7082,22 +7230,24 @@ window.addEventListener('updateviewarea'
       'page': location.pageNumber,
       'zoom': location.scale,
       'scrollLeft': location.left,
       'scrollTop': location.top
     }).catch(function() {
       // unable to write to storage
     });
   });
-  var href = PDFViewerApplication.getAnchorUrl(location.pdfOpenParams);
+  var href =
+    PDFViewerApplication.pdfLinkService.getAnchorUrl(location.pdfOpenParams);
   document.getElementById('viewBookmark').href = href;
   document.getElementById('secondaryViewBookmark').href = href;
 
   // Update the current bookmark in the browsing history.
-  PDFHistory.updateCurrentBookmark(location.pdfOpenParams, location.pageNumber);
+  PDFViewerApplication.pdfHistory.updateCurrentBookmark(location.pdfOpenParams,
+                                                        location.pageNumber);
 
   // Show/hide the loading indicator in the page number input element.
   var pageNumberInput = document.getElementById('pageNumber');
   var currentPage =
     PDFViewerApplication.pdfViewer.getPageView(PDFViewerApplication.page - 1);
 
   if (currentPage.renderingState === RenderingStates.FINISHED) {
     pageNumberInput.classList.remove(PAGE_NUMBER_LOADING_INDICATOR);
@@ -7117,18 +7267,26 @@ window.addEventListener('resize', functi
   }
   updateViewarea();
 
   // Set the 'max-height' CSS property of the secondary toolbar.
   SecondaryToolbar.setMaxHeight(document.getElementById('viewerContainer'));
 });
 
 window.addEventListener('hashchange', function webViewerHashchange(evt) {
-  if (PDFHistory.isHashChangeUnlocked) {
-    PDFViewerApplication.setHash(document.location.hash.substring(1));
+  if (PDFViewerApplication.pdfHistory.isHashChangeUnlocked) {
+    var hash = document.location.hash.substring(1);
+    if (!hash) {
+      return;
+    }
+    if (!PDFViewerApplication.isInitialViewSet) {
+      PDFViewerApplication.initialBookmark = hash;
+    } else {
+      PDFViewerApplication.pdfLinkService.setHash(hash);
+    }
   }
 });
 
 
 function selectScaleOption(value) {
   var options = document.getElementById('scaleSelect').options;
   var predefinedValueFound = false;
   for (var i = 0; i < options.length; i++) {
@@ -7471,23 +7629,23 @@ window.addEventListener('keydown', funct
       pdfViewer.focus();
     }
   }
 
   if (cmd === 2) { // alt-key
     switch (evt.keyCode) {
       case 37: // left arrow
         if (isViewerInPresentationMode) {
-          PDFHistory.back();
+          PDFViewerApplication.pdfHistory.back();
           handled = true;
         }
         break;
       case 39: // right arrow
         if (isViewerInPresentationMode) {
-          PDFHistory.forward();
+          PDFViewerApplication.pdfHistory.forward();
           handled = true;
         }
         break;
     }
   }
 
   if (handled) {
     evt.preventDefault();
--- a/browser/modules/DirectoryLinksProvider.jsm
+++ b/browser/modules/DirectoryLinksProvider.jsm
@@ -61,40 +61,184 @@ const PREF_DIRECTORY_SOURCE = "browser.n
 // The preference that tells where to send click/view pings
 const PREF_DIRECTORY_PING = "browser.newtabpage.directory.ping";
 
 // The preference that tells if newtab is enhanced
 const PREF_NEWTAB_ENHANCED = "browser.newtabpage.enhanced";
 
 // Only allow explicitly approved frecent sites with display name
 const ALLOWED_FRECENT_SITES = new Map([
-  [ 'airdroid.com,android-developers.blogspot.com,android.com,androidandme.com,androidapplications.com,androidapps.com,androidauthority.com,androidcentral.com,androidcommunity.com,androidfilehost.com,androidforums.com,androidguys.com,androidheadlines.com,androidpit.com,androidpolice.com,androidspin.com,androidtapp.com,androinica.com,droid-life.com,droidforums.net,droidviews.com,droidxforums.com,forum.xda-developers.com,phandroid.com,play.google.com,shopandroid.com,talkandroid.com,theandroidsoul.com,thedroidguy.com,videodroid.org',
-    'Technology' ],
-  [ 'assurancewireless.com,att.com,attsavings.com,boostmobile.com,budgetmobile.com,consumercellular.com,credomobile.com,gosmartmobile.com,h2owirelessnow.com,lycamobile.com,lycamobile.us,metropcs.com,myfamilymobile.com,polarmobile.com,qlinkwireless.com,republicwireless.com,sprint.com,straighttalk.com,t-mobile.com,tracfonewireless.com,verizonwireless.com,virginmobile.com,virginmobile.com.au,virginmobileusa.com,vodafone.co.uk,vodafone.com,vzwshop.com',
-    'Mobile Phone' ],
+  [ '1800petmeds.com,800petmeds.com,adopt.dogtime.com,adoptapet.com,akc.org,americanhumane.org,animal.discovery.com,animalconcerns.org,animalshelter.org,arcatapet.com,aspca.org,avma.org,bestfriends.org,blog.petmeds.com,buddydoghs.com,carealotpets.com,dailypuppy.com,dog.com,dogbar.com,dogbreedinfo.com,drsfostersmith.com,entirelypets.com,farmsanctuary.org,farmusa.org,freekibble.com,freekibblekat.com,healthypets.com,hsus.org,humanesociety.org,liveaquaria.com,marinedepot.com,medi-vet.com,nationalpetpharmacy.com,nsalamerica.org,nycacc.org,ohmydogsupplies.com,pet-dog-cat-supply-store.com,petcarerx.com,petco.com,petdiscounters.com,petedge.com,peteducation.com,petfinder.com,petfooddirect.com,petguys.com,petharbor.com,petmountain.com,petplanet.co.uk,pets911.com,petsmart.com,petsuppliesplus.com,puppyfind.com,revivalanimal.com,terrificpets.com,thatpetplace.com,theanimalrescuesite.com,theanimalrescuesite.greatergood.com,thefluffingtonpost.com,therainforestsite.com,vetdepot.com',
+    'pet' ],
+  [ '1aauto.com,autoblog.com,autoguide.com,autosite.com,autoweek.com,bimmerpost.com,bmwblog.com,boldride.com,caranddriver.com,carcomplaints.com,carspoon.com,cherokeeforum.com,classiccars.com,commercialtrucktrader.com,corvetteforum.com,dealerrater.com,ebizautos.com,ford-trucks.com,hemmings.com,jalopnik.com,jeepforum.com,jeepsunlimited.com,jk-forum.com,legendaryspeed.com,motorauthority.com,motortrend.com,motorwings.com,odometer.com,pirate4x4.com,purecars.com,roadandtrack.com,teslamotorsclub.com,topgear.com,topspeed.com,totalmini.com,truckpaper.com,wranglerforum.com',
+    'auto' ],
+  [ 'autobytel.com,autocheck.com,automotive.com,autonation.com,autos.aol.com,autos.msn.com,autos.yahoo.com,autotrader.autos.msn.com,autotrader.com,autotraderclassics.com,autoweb.com,car.com,carbuyingtips.com,carfax.com,cargurus.com,carmax.com,carprices.com,cars.com,cars.oodle.com,carsdirect.com,carsforsale.com,edmunds.com,hertzcarsales.com,imotors.com,intellichoice.com,internetautoguide.com,kbb.com,lemonfree.com,nada.com,nadaguides.com,thecarconnection.com,thetruthaboutcars.com,truecar.com,usedcars.com,usnews.rankingsandreviews.com',
+    'auto' ],
+  [ 'acura.com,audi.ca,audi.com,audiusa.com,automobiles.honda.com,bentleymotors.com,bmw.com,bmwusa.com,buick.com,buyatoyota.com,cadillac.com,cars.mclaren.com,chevrolet.com,choosenissan.com,chrysler.com,daimler.com,dodge.com,ferrari.com/en_us,fiskerautomotive.com,ford.com,gm.com,gmc.com,hummer.com,hyundai.com,hyundaiusa.com,infiniti.com,infinitiusa.com,jaguarusa.com,jeep.com,kia.com,kiamotors.com,lamborghini.com/en/home,landrover.com,landroverusa.com,lexus.com,lincoln.com,maserati.us,mazda.com,mazdausa.com,mbusa.com,mbusi.com,mercedes-amg.com,mercedes-benz.com,mercuryvehicles.com,miniusa.com,nissanusa.com,pontiac.com,porsche.com/usa,ramtrucks.com,rolls-roycemotorcars.com,saturn.com,scion.com,subaru.com,teslamotors.com,toyota.com,volkswagen.co.uk,volkswagen.com,volvocars.com/us,vw.com',
+    'auto' ],
+  [ '1010tires.com,4wheelparts.com,advanceautoparts.com,andysautosport.com,autoanything.com,autogeek.net,autopartsgiant.com,autopartswarehouse.com,autotrucktoys.com,autozone.com,autozoneinc.com,bavauto.com,bigotires.com,bilsteinus.com,brembo.com,car-part.com,carid.com,carparts.com,carquest.com,dinancars.com,discounttire.com,discounttiredirect.com,firestonecompleteautocare.com,goodyear.com,hrewheels,jcwhitney.com,kw-suspensions.com,momousa.com,napaonline.com,onlinetires.com,oreillyauto.com,oriellysautoparts.com,pepboys.com,repairpal.com,rockauto.com,shop.advanceautoparts.com,slickcar.com,stoptech.com,streetbeatcustoms.com,summitracing.com,tirebuyer.com,tirerack.com,tiresplus.com,tsw.com,velocitymotoring.com,wheelmax.com',
+    'auto parts' ],
+  [ 'abebooks.co.uk,abebooks.com,addall.com,alibris.com,allaboutcircuits.com,allbookstores.com,allyoucanbooks.com,answersingenesis.org,artnet.com,audiobooks.com,barnesandnoble.com,barnesandnobleinc.com,bartleby.com,betterworldbooks.com,biblio.com,biggerbooks.com,bncollege.com,bookbyte.com,bookdepository.com,bookfinder.com,bookrenter.com,booksamillion.com,booksite.com,boundless.com,brookstone.com,btol.com,calibre-ebook.com,campusbookrentals.com,casadellibro.com,cbomc.com,cengagebrain.com,chapters.indigo.ca,christianbook.com,ciscopress.com,coursesmart.com,cqpress.com,crafterschoice.com,crossings.com,cshlp.org,deseretbook.com,directtextbook.com,discountmags.com,doubledaybookclub.com,doubledaylargeprint.com,doverpublications.com,ebooks.com,ecampus.com,fellabooks.net,fictionwise.com,flatworldknowledge.com,goodreads.com,grolier.com,harpercollins.com,hayhouse.com,historybookclub.com,hpb.com,hpbmarketplace.com,interweave.com,iseeme.com,katiekazoo.com,knetbooks.com,learnoutloud.com,librarything.com,literaryguild.com,lulu.com,lww.com,macmillan.com,magazines.com,mbsdirect.net,militarybookclub.com,mypearsonstore.com,mysteryguild.com,netplaces.com,noble.com,novelguide.com,onespirit.com,oxfordjournals.org,paperbackswap.com,papy.co.jp,peachpit.com,penguin.com,penguingroup.com,pimsleur.com,powells.com,qpb.com,quepublishing.com,reviews.com,rhapsodybookclub.com,rodalestore.com,royalsocietypublishing.org,sagepub.com,scrubsmag.com,sfbc.com,simonandschuster.com,simonandschuster.net,simpletruths.com,teach12.net,textbooks.com,textbookx.com,thegoodcook.com,thriftbooks.com,tlsbooks.com,toshibabookplace.com,tumblebooks.com,urbookdownload.com,usedbooksearch.co.uk,valorebooks.com,valuemags.com,vialibri.net,wwnorton.com,zoobooks.com',
+    'literature' ],
+  [ '53.com,ally.com,bankofamerica.com,bbt.com,bnymellon.com,capitalone.com/bank/,chase.com,citi.com,citibank.com,citizensbank.com,citizensbankonline.com,creditonebank.com,everbank.com,hsbc.com,key.com,pnc.com,pncbank.com,rbs.co.uk,regions.com,sovereignbank.com,suntrust.com,tdbank.com,usaa.com,usbank.com,wachovia.com,wamu.com,wellsfargo.com,wsecu.org',
+    'banking' ],
+  [ '247wallst.com,bizjournals.com,bloomberg.com,businessweek.com,cnbc.com,cnnmoney.com,dowjones.com,easyhomesite.com,economist.com,entrepreneur.com,fastcompany.com,finance.yahoo.com,forbes.com,fortune.com,foxbusiness.com,ft.com,hbr.org,ibtimes.com,inc.com,manta.com,marketwatch.com,newsweek.com,online.wsj.com,qz.com,reuters.com,smartmoney.com,wsj.com',
+    'business news' ],
+  [ 'achievecard.com,americanexpress.com,barclaycardus.com,card.com,citicards.com,comparecards.com,creditcards.citi.com,discover.com,discovercard.com,experian.com,skylightpaycard.com,squareup.com,visa.com,visabuxx.com,visaextras.com',
+    'finance' ],
+  [ 'alliantcreditunion.org,connexuscu.org,lmcu.org,nasafcu.com,navyfcu.org,navyfederal.org,penfed.org,sccu.com,suncoastcreditunion.com,tinkerfcu.org,veridiancu.org',
+    'finance' ],
+  [ 'allbusiness.com,bankrate.com,buyersellertips.com,cboe.com,cnbcprime.com,coindesk.com,dailyfinance.com,dailyfx.com,dealbreaker.com,easierstreetdaily.com,economywatch.com,etfdailynews.com,etfdb.com,financeformulas.net,finviz.com,fool.com,forexpros.com,forexthreads.com,ftpress.com,fx-exchange.com,insidermonkey.com,investmentu.com,investopedia.com,investorjunkie.com,investors.com,kiplinger.com,minyanville.com,moneymorning.com,moneyning.com,moneysavingexpert.com,morningstar.com,nakedcapitalism.com,ncsoft.net,oilprice.com,realclearmarkets.com,rttnews.com,seekingalpha.com,silverdoctors.com,stockcharts.com,stockpickr.com,thefinancials.com,thestreet.com,wallstreetinsanity.com,wikinvest.com,xe.com,youngmoney.com',
+    'investing' ],
+  [ 'edwardjones.com,fidelity.com,goldmansachs.com,jpmorgan.com,ml.com,morganstanley.com,mymerrill.com,personal.vanguard.com,principal.com,schwab.com,schwabplan.com,scottrade.com,tdameritrade.com,troweprice.com,vanguard.com',
+    'investing' ],
+  [ '247lendinggroup.com,americanoneunsecured.com,avant.com,bestegg.com,chasestudentloans.com,eloan.com,gofundme.com,guidetolenders.com,kiva.org,lendacademy.com,lendingclub.com,lendingtree.com,lightstream.com,loanio.com,manageyourloans.com,meetearnest.com,microplace.com,netcredit.com,peer-lend.com,personalloans.com,prosper.com,salliemae.com,sofi.com,springleaf.com,uk.zopa.com,upstart.com',
+    'finance' ],
+  [ 'betterment.com,blooom.com,futureadvisor.com,kapitall.com,motifinvesting.com,personalcapital.com,wealthfront.com,wisebanyan.com',
+    'investing' ],
+  [ 'bancdebinary.com,cherrytrade.com,empireoption.net,etrade.com,firstrade.com,forex.com,interactivebrokers.com,ishares.com,optionsxpress.com,sharebuilder.com,thinkorswim.com,tradeking.com,trademonster.com,us.etrade.com,zecco.com',
+    'finance' ],
+  [ 'annualcreditreport.com,bluebird.com,credio.com,creditkarma.com,creditreport.com,cybersource.com,equifax.com,freecreditreport.com,freecreditscore.com,freedomdebtrelief.com,freescoreonline.com,mint.com,moneymappress.com,myfico.com,nationaldebtrelief.com,onesmartpenny.com,paypal.com,transunion.com,truecredit.com,upromise.com,vuebill.com,xpressbillpay.com,youneedabudget.com',
+    'personal finance' ],
+  [ 'angieslist.com,bloomberg.com,businessinsider.com,buydomains.com,domain.com,entrepreneur.com,fastcompany.com,forbes.com,fortune.com,godaddy.com,inc.com,manta.com,nytimes.com,openforum.com,register.com,salesforce.com,sba.gov,sbomag.com,shopsmall.americanexpress.com,smallbusiness.yahoo.com,squarespace.com,startupjournal.com,startupnation.com,weebly.com,wordpress.com,youngentrepreneur.com',
+    'business news' ],
+  [ '1040now.net,24hourtax.com,acttax.com,comparetaxsoftware.org,e-file.com,etax.com,free1040taxreturn.com,hrblock.com,intuit.com,irstaxdoctors.com,libertytax.com,octaxcol.com,pay1040.com,priortax.com,quickbooks.com,quickrefunds.com,rapidtax.com,refundschedule.com,taxact.com,taxactonline.com,taxefile.com,taxhead.com,taxhelptoday.me,taxsimple.org,turbotax.com',
+    'tax' ],
+  [ 'adeccousa.com,americasjobexchange.com,aoljobs.com,applicantpro.com,applicantstack.com,apply-4-jobs.com,apply2jobs.com,att.jobs,beyond.com,careerboutique.com,careerbuilder.com,careerflash.net,careerslocal.net,climber.com,coverlettersandresume.com,dice.com,diversityonecareers.com,employmentguide.com,everyjobforme.com,experteer.com,find.ly,findtherightjob.com,freelancer.com,gigats.com,glassdoor.com,governmentjobs.com,hrapply.com,hrdepartment.com,hrsmart.com,ihire.com,indeed.com,internships.com,itsmycareer.com,job-applications.com,job-hunt.org,job-interview-site.com,job.com,jobcentral.com,jobdiagnosis.com,jobhat.com,jobing.com,jobrapido.com,jobs.aol.com,jobs.net,jobsbucket.com,jobsflag.com,jobsgalore.com,jobsonline.com,jobsradar.com,jobster.com,jobtorch.com,jobungo.com,jobvite.com,juju.com,linkedin.com,livecareer.com,localjobster.com,mindtools.com,monster.com,myjobhelper.com,myperfectresume.com,payscale.com,pryor.com,quintcareers.com,randstad.com,recruitingcenter.net,resume-library.com,resume-now.com,roberthalf.com,salary.com,salaryexpert.com,simplyhired.com,smartrecruiters.com,snagajob.com,startwire.com,theladders.com,themuse.com,theresumator.com,thingamajob.com,usajobs.gov,ziprecruiter.com',
+    'career services' ],
+  [ 'americanheart.org,americanredcross.com,americares.org,catholiccharitiesusa.org,charitybuzz.com,charitynavigator.org,charitywater.org,directrelief.org,fao.org,habitat.org,hrw.org,imf.org,mskcc.org,ohchr.org,redcross.org,reliefweb.int,salvationarmyusa.org,savethechildren.org,un.org,undp.org,unep.org,unesco.org,unfpa.org,unhcr.org,unicef.org,unicefusa.org,unops.org,volunteermatch.org,wfp.org,who.int,worldbank.org',
+    'philanthropic' ],
+  [ 'academia.edu,albany.edu,american.edu,amity.edu,annauniv.edu,apus.edu,arizona.edu,ashford.edu,asu.edu,auburn.edu,austincc.edu,baylor.edu,bc.edu,berkeley.edu,brandeis.edu,brookings.edu,brown.edu,bu.edu,buffalo.edu,byu.edu,calpoly.edu,calstate.edu,caltech.edu,cam.ac.uk,cambridge.org,capella.edu,case.edu,clemson.edu,cmu.edu,colorado.edu,colostate-pueblo.edu,colostate.edu,columbia.edu,commnet.edu,cornell.edu,cpp.edu,csulb.edu,csun.edu,csus.edu,cuny.edu,cwru.edu,dartmouth.edu,depaul.edu,devry.edu,drexel.edu,du.edu,duke.edu,emory.edu,fau.edu,fcps.edu,fiu.edu,fordham.edu,fsu.edu,fullerton.edu,fullsail.edu,gatech.edu,gcu.edu,georgetown.edu,gmu.edu,gsu.edu,gwu.edu,harvard.edu,hawaii.edu,hbs.edu,iastate.edu,iit.edu,illinois.edu,indiana.edu,iu.edu,jhu.edu,k-state.edu,kent.edu,ku.edu,lamar.edu,liberty.edu,losrios.edu,lsu.edu,luc.edu,maine.edu,maricopa.edu,mass.edu,miami.edu,miamioh.edu,missouri.edu,mit.edu,mnscu.edu,monash.edu,msu.edu,mtu.edu,nau.edu,ncsu.edu,nd.edu,neu.edu,njit.edu,northeastern.edu,northwestern.edu,nova.edu,nyu.edu,odu.edu,ohio-state.edu,ohio.edu,okstate.edu,oregonstate.edu,osu.edu,ou.edu,ox.ac.uk,pdx.edu,pearson.com,phoenix.edu,pitt.edu,princeton.edu,psu.edu,purdue.edu,regis.edu,rice.edu,rit.edu,rochester.edu,rpi.edu,rutgers.edu,sc.edu,scu.edu,sdsu.edu,seattleu.edu,sfsu.edu,si.edu,sjsu.edu,snhu.edu,stanford.edu,stonybrook.edu,suny.edu,syr.edu,tamu.edu,temple.edu,towson.edu,ttu.edu,tufts.edu,ua.edu,uark.edu,ub.edu,uc.edu,uccs.edu,ucdavis.edu,ucf.edu,uchicago.edu,uci.edu,ucla.edu,uconn.edu,ucr.edu,ucsb.edu,ucsc.edu,ucsd.edu,ucsf.edu,udel.edu,udemy.com,ufl.edu,uga.edu,uh.edu,uic.edu,uillinois.edu,uiowa.edu,uiuc.edu,uky.edu,umass.edu,umb.edu,umbc.edu,umd.edu,umich.edu,umn.edu,umuc.edu,unc.edu,uncc.edu,unf.edu,uniminuto.edu,universityofcalifornia.edu,unl.edu,unlv.edu,unm.edu,unt.edu,uoc.edu,uoregon.edu,upc.edu,upenn.edu,upi.edu,uri.edu,usc.edu,usf.edu,usg.edu,usu.edu,uta.edu,utah.edu,utdallas.edu,utexas.edu,utk.edu,uvm.edu,uw.edu,uwm.edu,vanderbilt.edu,vccs.edu,vcu.edu,virginia.edu,vt.edu,waldenu.edu,washington.edu,wayne.edu,wednet.edu,wgu.edu,wisc.edu,wisconsin.edu,wm.edu,wmich.edu,wsu.edu,wustl.edu,wvu.edu,yale.edu',
+    'college' ],
+  [ 'collegeboard.com,collegeconfidential.com,collegeview.com,ecollege.com,finaid.org,find-colleges-now.com,ratemyprofessors.com,ratemyteachers.com,studentsreview.com',
+    'college' ],
+  [ 'actstudent.org,adaptedmind.com,aesoponline.com,archives.com,bibme.org,blackboard.com,bookrags.com,cengage.com,chegg.com,classdojo.com,classzone.com,cliffsnotes.com,coursecompass.com,educationconnection.com,educationdynamics.com,ets.org,familysearch.org,fastweb.com,genealogy.com,gradesaver.com,instructure.com,khanacademy.org,learn4good.com,mathway.com,mathxl.com,mcgraw-hill.com,merriam-webster.com,mheducation.com,niche.com,openstudy.com,pearsoned.com,pearsonmylabandmastering.com,pearsonsuccessnet.com,poptropica.com,powerschool.com,proprofs.com,purplemath.com,quizlet.com,readwritethink.org,renlearn.com,rhymezone.com,schoolloop.com,schoology.com,smithsonianmag.com,sparknotes.com,study.com,studyisland.com,studymode.com,synonym.com,teacherprobs.com,teacherspayteachers.com,tutorvista.com,vocabulary.com,yourschoolmatch.com',
+    'education' ],
+  [ 'browardschools.com,k12.ca.us,k12.fl.us,k12.ga.us,k12.in.us,k12.mn.us,k12.mo.us,k12.nc.us,k12.nj.us,k12.oh.us,k12.va.us,k12.wi.us',
+    'education' ],
+  [ 'coolmath-games.com,coolmath.com,coolmath4kids.com,coolquiz.com,funbrain.com,funtrivia.com,gamesforthebrain.com,girlsgogames.com,hoodamath.com,lumosity.com,math.com,mathsisfun.com,trivia.com,wizard101.com',
+    'learning games' ],
+  [ 'askmen.com,boredomtherapy.com,buzzfeed.com,complex.com,dailymotion.com,elitedaily.com,gawker.com,howstuffworks.com,instagram.com,madamenoire.com,polygon.com,ranker.com,rollingstone.com,ted.com,theblaze.com,thechive.com,thecrux.com,thedailybeast.com,thoughtcatalog.com,uproxx.com,upworthy.com,zergnet.com',
+    'entertainment' ],
+  [ '11points.com,7gid.com,adultswim.com,break.com,cheezburger.com,collegehumor.com,cracked.com,dailydawdle.com,damnlol.com,dumb.com,dumblaws.com,ebaumsworld.com,explosm.net,failblog.org,fun-gallery.com,funnygig.com,funnyjunk.com,funnymama.com,funnyordie.com,funnytear.com,funplus.com,glassgiant.com,goingviralposts.com,gorillamask.net,i-am-bored.com,icanhascheezburger.com,ifunny.com,imjussayin.co,inherentlyfunny.com,izismile.com,jokes.com,keenspot.com,knowyourmeme.com,laughstub.com,memebase.com,mememaker.net,metacafe.com,mylol.com,picslist.com,punoftheday.com,queendom.com,rajnikantvscidjokes.in,regretfulmorning.com,shareonfb.com,somethingawful.com,stupidvideos.com,superfunnyimages.com,thedailywh.at,theonion.com,tosh.comedycentral.com,uberhumor.com,welltimedphotos.com',
+    'humor' ],
+  [ 'air.tv,amctheatres.com,boxofficemojo.com,cinapalace.com,cinaplay.com,cinemablend.com,cinemark.com,cinematical.com,collider.com,comicbookmovie.com,comingsoon.net,crackle.com,denofgeek.us,dreamworks.com,empireonline.com,enstarz.com,fandango.com,filmschoolrejects.com,flickeringmyth.com,flixster.com,fullmovie2k.com,g2g.fm,galleryhip.com,hollywood.com,hollywoodreporter.com,iglomovies.com,imdb.com,indiewire.com,instantwatcher.com,joblo.com,kickass.to,kissdrama.net,marcustheatres.com,megashare9.com,moviefone.com,movieinsider.com,moviemistakes.com,moviepilot.com,movierulz.com,movies.com,movies.yahoo.com,movieseum.com,movietickets.com,movieweb.com,mrmovietimes.com,mymovieshub.com,netflix.com,onlinemovies.pro,pelis24.com,projectfreetv.ch,redbox.com,regmovies.com,repelis.tv,rogerebert.suntimes.com,ropeofsilicon.com,rottentomatoes.com,sidereel.com,slashfilm.com,solarmovie.is,starwars.com,superherohype.com,tcm.com,twomovies.us,variety.com,vimeo.com,viooz.ac,warnerbros.com,watchfree.to,wbredirect.com,youtubeonfire.com,zmovie.tw,zumvo.com',
+    'movie' ],
+  [ '1079ishot.com,2dopeboyz.com,8tracks.com,acdc.com,allaccess.com,allhiphop.com,allmusic.com,audiofanzine.com,audiomack.com,azlyrics.com,baeblemusic.com,bandsintown.com,billboard.com,brooklynvegan.com,brunomars.com,buzznet.com,cmt.com,coachella.com,consequenceofsound.net,contactmusic.com,countryweekly.com,dangerousminds.net,datpiff.com,ddotomen.com,diffuser.fm,directlyrics.com,djbooth.net,eventful.com,fireflyfestival.com,genius.com,guitartricks.com,harmony-central.com,hiphopdx.com,hiphopearly.com,hypem.com,idolator.com,iheart.com,jambase.com,kanyetothe.com,knue.com,lamusica.com,last.fm,livemixtapes.com,loudwire.com,lyricinterpretations.com,lyrics.net,lyricsbox.com,lyricsmania.com,lyricsmode.com,metal-archives.com,metrolyrics.com,mp3.com,mtv.co.uk,myspace.com,newnownext.com,noisetrade.com,okayplayer.com,pandora.com,phish.com,pigeonsandplanes.com,pitchfork.com,popcrush.com,radio.com,rap-up.com,rdio.com,reverbnation.com,revolvermag.com,rockhall.com,saavn.com,songlyrics.com,soundcloud.com,spin.com,spinrilla.com,spotify.com,stereogum.com,stereotude.com,talkbass.com,tasteofcountry.com,thebacklot.com,theboombox.com,theboot.com,thissongissick.com,tunesbaby.com,ultimate-guitar.com,ultimateclassicrock.com,vevo.com,vibe.com,vladtv.com,whosampled.com,wikibit.me,worldstarhiphop.com,wyrk.com,xxlmag.com',
+    'music' ],
+  [ 'aceshowbiz.com,aintitcoolnews.com,allkpop.com,askkissy.com,atraf.co.il,audioboom.com,beamly.com,beyondhollywood.com,blastr.com,blippitt.com,bollywoodlife.com,bossip.com,buzzlamp.com,buzzsugar.com,cambio.com,celebdirtylaundry.com,celebrity-gossip.net,celebuzz.com,chisms.net,comicsalliance.com,concertboom.com,crushable.com,cultbox.co.uk,dailyentertainmentnews.com,dayscafe.com,deadline.com,deathandtaxesmag.com,diaryofahollywoodstreetking.com,digitalspy.com,dlisted.com,egotastic.com,empirenews.net,enelbrasero.com,eonline.com,etonline.com,ew.com,extratv.com,facade.com,famousfix.com,fanaru.com,fanpop.com,fansshare.com,fhm.com,geektyrant.com,glamourpage.com,gossipcenter.com,gossipcop.com,heatworld.com,hlntv.com,hollyscoop.com,hollywoodlife.com,hollywoodtuna.com,hypable.com,infotransfer.net,insideedition.com,interaksyon.com,jezebel.com,justjared.buzznet.com,justjared.com,justjaredjr.com,komando.com,koreaboo.com,laineygossip.com,maxgo.com,maxim.com,maxviral.com,mediatakeout.com,mosthappy.com,moviestalk.com,my.ology.com,nationalenquirer.com,necolebitchie.com,ngoisao.net,nofilmschool.com,nolocreo.com,octane.tv,okmagazine.com,ouchpress.com,people.com,peopleenespanol.com,perezhilton.com,pinkisthenewblog.com,platotv.tv,playbill.com,playbillvault.com,playgroundmag.net,popsugar.com,purepeople.com,radaronline.com,rantchic.com,realitytea.com,reshareworthy.com,rinkworks.com,ripbird.com,sara-freder.com,screencrush.com,screenjunkies.com,soapcentral.com,soapoperadigest.com,sobadsogood.com,socialitelife.com,sourcefednews.com,splitsider.com,starcasm.net,starmagazine.com,starpulse.com,straightfromthea.com,stupiddope.com,tbn.org,theawesomedaily.com,theawl.com,thefrisky.com,thefw.com,thehollywoodgossip.com,theresacaputo.com,thesuperficial.com,thezooom.com,tmz.com,tvnotas.com.mx,twanatells.com,usmagazine.com,vanityfair.com,vanswarpedtour.com,vietgiaitri.com,viral.buzz,vulture.com,wakavision.com,worthytales.net,wwtdd.com',
+    'entertainment news' ],
+  [ 'abc.go.com,abcfamily.go.com,abclocal.go.com,accesshollywood.com,aetv.com,amctv.com,animalplanet.com,bbcamerica.com,bet.com,biography.com,bravotv.com,cartoonnetwork.com,cbn.com,cbs.com,cc.com,centrictv.com,cinemax.com,comedycentral.com,ctv.ca,cwtv.com,daytondailynews.com,drphil.com,dsc.discovery.com,fox.com,fox23.com,fox4news.com,fxnetworks.com,hbo.com,history.com,hulu.com,ifc.com,iqiyi.com,jeopardy.com,kfor.com,logotv.com,mtv.com,myfoxchicago.com,myfoxdc.com,myfoxmemphis.com,myfoxphilly.com,nbc.com,nbcchicago.com,oxygen.com,pbs.org,pbskids.org,rachaelrayshow.com,rtve.es,scifi.com,sho.com,showtimeanytime.com,spike.com,sundance.tv,syfy.com,tbs.com,teamcoco.com,telemundo.com,thedoctorstv.com,titantv.com,tlc.com,tlc.discovery.com,tnt.tv,tntdrama.com,tv.com,tvguide.com,tvseriesfinale.com,usanetwork.com,uvidi.com,vh1.com,viki.com,watchcartoononline.com,watchseries-online.ch,wetv.com,wheeloffortune.com,whio.com,wnep.com,wral.com,wtvr.com,xfinitytv.com,yidio.com',
+    'TV show' ],
+  [ 'americanhiking.org,appalachiantrail.org,canadiangeographic.ca,defenders.org,discovermagazine.com,discoveroutdoors.com,dsc.discovery.com,earthtouchnews.com,edf.org,epa.gov,ewg.org,fishngame.org,foe.org,fs.fed.us,geography.about.com,landtrustalliance.org,nationalgeographic.com,nature.com,nrdc.org,nwf.org,outdoorchannel.com,outdoors.org,seedmagazine.com,trcp.org,usda.gov,worldwildlife.org',
+    'environment' ],
+  [ 'abbreviations.com,abcmouse.com,abcya.com,achieve3000.com,ancestry.com,animaljam.com,babble.com,babycenter.com,babynamespedia.com,behindthename.com,bestmomstv.com,brainyquote.com,cafemom.com,citationmachine.net,clubpenguin.com,cutemunchkins.com,discoveryeducation.com,disney.com,easybib.com,education.com,enotes.com,everydayfamily.com,familyeducation.com,gamefaqs.com,greatschools.org,hrw.com,imvu.com,infoplease.com,itsybitsysteps.com,justmommies.com,k12.com,kidsactivitiesblog.com,mathwarehouse.com,mom.me,mom365.com,mommyshorts.com,momswhothink.com,momtastic.com,monsterhigh.com,myheritage.com,nameberry.com,nickmom.com,pampers.com,parenthood.com,parenting.com,parenting.ivillage.com,parents.com,parentsociety.com,raz-kids.com,regentsprep.org,scarymommy.com,scholastic.com,shmoop.com,softschools.com,spanishdict.com,starfall.com,thebump.com,thefreedictionary.com,thenest.com,thinkbabynames.com,todaysparent.com,webkinz.com,whattoexpect.com',
+    'family' ],
+  [ 'americangirl.com,barbie.com,barbiecollectibles.com,cartoonnetworkshop.com,chuckecheese.com,coloring.ws,disney.co.uk,disney.com.au,disney.go.com,disney.in,disneychannel-asia.com,disneyinternational.com,disneyjunior.com,disneylatino.com,disneyme.com,dltk-kids.com,dressupone.com,fantage.com,funbrainjr.com,hotwheels.com,icarly.com,kiwicrate.com,marvel.com,marvelkids.com,mattelgames.com,maxsteel.com,monkeyquest.com,nick-asia.com,nick.co.uk,nick.com,nick.tv,nickelodeon.com.au,nickjr.co.uk,nickjr.com,ninjakiwi.com,notdoppler.com,powerrangers.com,sciencekids.co.nz,search.disney.com,seventeen.com,teennick.com,theslap.com,yepi.com',
+    'family' ],
+  [ 'alabama.gov,archives.gov,bls.gov,ca.gov,cancer.gov,cdc.gov,census.gov,cia.gov,cms.gov,commerce.gov,ct.gov,delaware.gov,dhs.gov,doi.gov,dol.gov,dot.gov,ed.gov,eftps.gov,epa.gov,fbi.gov,fda.gov,fema.gov,flhsmv.gov,ftc.gov,ga.gov,georgia.gov,gpo.gov,hhs.gov,house.gov,hud.gov,illinois.gov,in.gov,irs.gov,justice.gov,ky.gov,loc.gov,louisiana.gov,maryland.gov,mass.gov,michigan.gov,mo.gov,nih.gov,nj.gov,nps.gov,ny.gov,nyc.gov,ohio.gov,ok.gov,opm.gov,oregon.gov,pa.gov,recreation.gov,sba.gov,sc.gov,sec.gov,senate.gov,state.fl.us,state.gov,state.il.us,state.ma.us,state.mi.us,state.mn.us,state.nc.us,state.ny.us,state.oh.us,state.pa.us,studentloans.gov,telldc.com,texas.gov,tn.gov,travel.state.gov,tsa.gov,usa.gov,uscis.gov,uscourts.gov,usda.gov,usdoj.gov,usembassy.gov,usgs.gov,utah.gov,va.gov,virginia.gov,wa.gov,whitehouse.gov,wi.gov,wisconsin.gov',
+    'government' ],
+  [ 'beachbody.com,bodybuilding.com,caloriecount.com,extremefitness.com,fitbit.com,fitday.com,fitnessmagazine.com,fitnessonline.com,fitwatch.com,livestrong.com,maxworkouts.com,mensfitness.com,menshealth.com,muscleandfitness.com,muscleandfitnesshers.com,myfitnesspal.com,shape.com,womenshealthmag.com',
+    'health & fitness' ],
+  [ 'activebeat.com,alliancehealth.com,beyonddiet.com,caring.com,complete-health-and-happiness.com,diabeticconnect.com,doctoroz.com,everydayhealth.com,followmyhealth.com,greatist.com,health.com,healthboards.com,healthcaresource.com,healthgrades.com,healthguru.com,healthination.com,healthtap.com,helpguide.org,iherb.com,kidshealth.org,lifescript.com,lovelivehealth.com,medicaldaily.com,mercola.com,perfectorigins.com,prevention.com,qualityhealth.com,questdiagnostics.com,realself.com,sharecare.com,sparkpeople.com,spryliving.com,steadyhealth.com,symptomfind.com,ucomparehealthcare.com,vitals.com,webmd.com,weightwatchers.com,wellness.com,zocdoc.com',
+    'health & wellness' ],
+  [ 'aetna.com,anthem.com,athenahealth.com,bcbs.com,bluecrossca.com,cigna.benefitnation.net,cigna.com,cigna.healthplan.com,ehealthcare.com,ehealthinsurance.com,empireblue.com,goldenrule.com,healthcare.gov,healthnet.com,humana-medicare.com,humana.com,kaiserpermanente.org,metlife.com,my.cigna.com,mybenefits.metlife.com,myuhc.com,uhc.com,unitedhealthcareonline.com,walterrayholt.com',
+    'health insurance' ],
+  [ 'aafp.org,americanheart.org,apa.org,cancer.org,cancercenter.com,caremark.com,clevelandclinic.org,diabetesfree.org,drugs.com,emedicinehealth.com,express-scripts.com,familydoctor.org,goodrx.com,healthcaremagic.com,healthfinder.gov,healthline.com,ieee.org,intelihealth.com,labcorp.com,livecellresearch.com,mayoclinic.com,mayoclinic.org,md.com,medcohealth.com,medhelp.org,medicalnewstoday.com,medicare.gov,medicaresupplement.com,medicinenet.com,medscape.com,memorialhermann.org,merckmanuals.com,patient.co.uk,psychcentral.com,psychology.org,psychologytoday.com,rightdiagnosis.com,rxlist.com,socialpsychology.org,spine-health.com,who.int',
+    'health & wellness' ],
+  [ 'aaa.com,aig.com,allianz-assistance.com,allstate.com,allstateagencies.com,amfam.com,amica.com,autoquotesdirect.com,esurance.com,farmers.com,farmersagent.com,geico.com,general-car-insurance-quotes.net,insurance.com,libertymutual.com,libertymutualgroup.com,mercuryinsurance.com,nationwide.com,progressive.com,progressiveagent.com,progressiveinsurance.com,provide-insurance.com,safeco.com,statefarm.com,thehartford.com,travelers.com,usaa.com',
+    'insurance' ],
+  [ '101cookbooks.com,allrecipes.com,bettycrocker.com,bonappetit.com,chocolateandzucchini.com,chow.com,chowhound.chow.com,cookinglight.com,cooks.com,cooksillustrated.com,cooksrecipes.com,delish.com,eater.com,eatingwell.com,epicurious.com,food.com,foodandwine.com,foodgawker.com,foodnetwork.com,gourmet.com,grouprecipes.com,homemaderecipes.co,iheartnaptime.net,kraftfoods.com,kraftrecipes.com,myrecipes.com,opentable.com,pillsbury.com,recipe.com,recipesource.com,recipezaar.com,saveur.com,seriouseats.com,simplyrecipes.com,smittenkitchen.com,southernliving.com,supercook.com,tasteofhome.com,tastespotting.com,technicpack.net,thekitchn.com,urbanspoon.com,wonderhowto.com,yelp.com,yummly.com,zagat.com',
+    'food & lifestyle' ],
+  [ 'aarp.org,allure.com,bustle.com,cosmopolitan.com,diply.com,eharmony.com,elle.com,glamour.com,grandascent.com,harpersbazaar.com,hellogiggles.com,instructables.com,instyle.com,marieclaire.com,match.com,mindbodygreen.com,nymag.com,okcupid.com,petco.com,photobucket.com,pof.com,rantlifestyle.com,redbookmag.com,reddit.com,sheknows.com,style.com,stylebistro.com,theilovedogssite.com,theknot.com,thescene.com,thrillist.com,vogue.com,womansday.com,youngcons.com,yourdictionary.com',
+    'lifestyle' ],
+  [ 'apartmentratings.com,apartmenttherapy.com,architectmagazine.com,architecturaldigest.com,askthebuilder.com,bhg.com,bobvila.com,countryhome.com,countryliving.com,davesgarden.com,decor8blog.com,decorpad.com,diycozyhome.com,diyideas.com,diynetwork.com,doityourself.com,domainehome.com,dwell.com,elledecor.com,familyhandyman.com,frontdoor.com,gardenguides.com,gardenweb.com,getdecorating.com,goodhousekeeping.com,hgtv.com,hgtvgardens.com,hobbylobby.com,homeadvisor.com,homerepair.about.com,hometalk.com,hometime.com,hometips.com,housebeautiful.com,houzz.com,inhabitat.com,lonny.com,makingitlovely.com,marthastewart.com,michaels.com,myhomeideas.com,realsimple.com,remodelista.com,shanty-2-chic.com,styleathome.com,thehandmadehome.net,thehealthyhomeeconomist.com,thisoldhouse.com,traditionalhome.com,trulia.com,younghouselove.com',
+    'home & lifestyle' ],
+  [ '10best.com,10tv.com,11alive.com,19actionnews.com,9news.com,abcnews.com,abcnews.go.com,adweek.com,ajc.com,anchorfree.us,arcamax.com,austin360.com,azcentral.com,bbc.co.uk,boston.com,bostonglobe.com,capecodonline.com,cbsnews.com,cheatsheet.com,chicagotribune.com,chron.com,citylab.com,cnn.com,csmonitor.com,dailyitem.com,dailymail.co.uk,dallasnews.com,eleconomista.es,examiner.com,fastcolabs.com,fivethirtyeight.com,foursquare.com,foxcarolina.com,foxnews.com,globalnews.ca,greatergood.com,guardian.co.uk,historynet.com,huffingtonpost.co.uk,huffingtonpost.com,ijreview.com,independent.co.uk,journal-news.com,kare11.com,kcra.com,kctv5.com,kgw.com,khou.com,king5.com,kirotv.com,kitv.com,kmbc.com,knoxnews.com,kpho.com,kptv.com,kron4.com,ksdk.com,ksl.com,ktvb.com,kvue.com,kxan.com,latimes.com,lifehack.org,littlethings.com,mailtribune.com,mic.com,mirror.co.uk,msn.com,msnbc.com,msnbc.msn.com,myfoxboston.com,nbcnews.com,nbcnewyork.com,newburyportnews.com,news.bbc.co.uk,news.yahoo.com,news12.com,newschannel10.com,newsday.com,newser.com,newsmax.com,newyorker.com,nj.com,nj1015.com,npr.org,nydailynews.com,nypost.com,nytimes.com,palmbeachpost.com,patch.com,philly.com,phys.org,poconorecord.com,prnewswire.com,rare.us,realclearworld.com,record-eagle.com,richmond.com,rt.com,salemnews.com,salon.com,sfgate.com,slate.com,statesman.com,suntimes.com,takepart.com,telegraph.co.uk,theatlantic.com,thedailystar.com,theguardian.com,theroot.com,theverge.com,time.com,timesonline.co.uk,topix.com,usatoday.com,usatoday30.usatoday.com,usnews.com,vice.com,vox.com,wane.com,washingtonpost.com,washingtontimes.com,wave3.com,wavy.com,wbaltv.com,wbir.com,wcnc.com,wdbj7.com,westernjournalism.com,wfaa.com,wfsb.com,wftv.com,wgal.com,wishtv.com,wisn.com,wistv.com,wivb.com,wkyc.com,wlwt.com,wmur.com,woodtv.com,wpxi.com,wsbtv.com,wsfa.com,wsmv.com,wsoctv.com,wthr.com,wtnh.com,wtsp.com,wwltv.com,wyff4.com,wzzm13.com',
+    'news' ],
+  [ 'aei.org,breitbart.com,conservativetalknow.com,conservativetribune.com,dailykos.com,ddo.com,drudgereport.com,dscc.org,foreignpolicy.com,franklinprosperityreport.com,freedomworks.org,macleans.ca,mediamatters.org,militarytimes.com,nationaljournal.com,nationalreview.com,politicalwire.com,politico.com,pressrepublican.com,qpolitical.com,realclearpolitics.com,talkingpointsmemo.com,thehill.com,thenation.com,thinkprogress.org,tnr.com,worldoftanks.eu',
+    'news' ],
+  [ 'americanscientist.org,discovermagazine.com,iflscience.com,livescience.com,nasa.gov,nationalgeographic.com,nature.com,newscientist.com,popsci.com,sciencedaily.com,sciencemag.org,sciencenews.org,scientificamerican.com,space.com,zmescience.com',
+    'science' ],
+  [ 'accuweather.com,intellicast.com,noaa.gov,ssa.gov,theweathernetwork.com,weather.com,weather.gov,weather.yahoo.com,weatherbug.com,weatherunderground.com,weatherzone.com.au,wunderground.com,www.weather.com',
+    'weather' ],
+  [ 'bhphotovideo.com,bigfolio.com,bigstockphoto.com,cameralabs.com,canonrumors.com,canstockphoto.com,digitalcamerareview.com,dpreview.com,expertphotography.com,gettyimages.com,icp.org,imaging-resource.com,intothedarkroom.com,istockphoto.com,nikonusa.com,photos.com,shutterstock.com,slrgear.com,the-digital-picture.com,thephotoargus.com,usa.canon.com,whatdigitalcamera.com,zenfolio.com',
+    'photography' ],
+  [ 'abercrombie.com,ae.com,aeropostale.com,anthropologie.com,bananarepublic.com,buycostumes.com,chadwicks.com,express.com,forever21.com,freepeople.com,hm.com,hollisterco.com,jcrew.com,jessicalondon.com,kingsizemen.com,lordandtaylor.com,lulus.com,metrostyle.com,nomorerack.com,oldnavy.com,oldnavy.gap.com,polyvore.com,rackroomshoes.com,ralphlauren.com,refinery29.com,roamans.com,sammydress.com,shop.nordstrom.com,shopbop.com,topshop.com,urbanoutfitters.com,victoriassecret.com,wetseal.com,womanwithin.com',
+    'shopping' ],
+  [ 'bizrate.com,compare99.com,coupons.com,dealtime.com,epinions.com,junglee.com,kijiji.ca,pricegrabber.com,pronto.com,redplum.com,retailmenot.com,shopping.com,shopzilla.com,smarter.com,valpak.com',
+    'shopping' ],
+  [ '123greetings.com,1800baskets.com,1800flowers.com,americangreetings.com,birthdayexpress.com,bluemountain.com,e-cards.com,egreetings.com,florists.com,ftd.com,gifts.com,groupcard.com,harryanddavid.com,hipstercards.com,kabloom.com,personalcreations.com,proflowers.com,redenvelope.com,someecards.com',
+    'flowers & gifts' ],
+  [ '6pm.com,alibaba.com,aliexpress.com,amazon.co.uk,amazon.com,asos.com,bathandbodyworks.com,bloomingdales.com,bradsdeals.com,buy.com,cafepress.com,circuitcity.com,clarkhoward.com,consumeraffairs.com,costco.com,cvs.com,dhgate.com,diapers.com,dillards.com,ebates.com,ebay.com,ebaystores.com,etsy.com,fingerhut.com,groupon.com,hsn.com,jcpenney.com,kmart.com,kohls.com,kroger.com,lowes.com,macys.com,menards.com,nextag.com,nordstrom.com,orientaltrading.com,overstock.com,qvc.com,racked.com,rewardsnetwork.com,samsclub.com,sears.com,sephora.com,shopathome.com,shopify.com,shopstyle.com,slickdeals.net,soap.com,staples.com,target.com,toptenreviews.com,vistaprint.com,walgreens.com,walmart.ca,walmart.com,wayfair.com,zappos.com,zazzle.com,zulily.com',
+    'shopping' ],
+  [ 'acehardware.com,ashleyfurniture.com,bedbathandbeyond.com,brylanehome.com,casa.com,cb2.com,crateandbarrel.com,dwr.com,ethanallen.com,furniture.com,harborfreight.com,hayneedle.com,homedecorators.com,homedepot.com,ikea.com,info.ikea-usa.com,landofnod.com,pier1.com,plowhearth.com,potterybarn.com,restorationhardware.com,roomandboard.com,westelm.com,williams-sonoma.com',
+    'home shopping' ],
+  [ 'alexandermcqueen.com,bergdorfgoodman.com,bottegaveneta.com,burberry-bluelabel.com,burberry.com,chanel.com,christianlouboutin.com,coach.com,diesel.com,dior.com,dolcegabbana.com,dolcegabbana.it,fendi.com,ferragamo.com,giorgioarmani.com,givenchy.com,gucci.com,guess.com,hermes.com,jeanpaulgaultier.com,jimmychoo.com,juicycouture.com,katespade.com,louisvuitton.com,manoloblahnik.com,marcjacobs.com,neimanmarcus.com,net-a-porter.com,paulsmith.co.uk,prada.com,robertocavalli.com,saksfifthavenue.com,toryburch.com,valentino.com,versace.com,vuitton.com,ysl.com,yslbeautyus.com',
+    'luxury shopping' ],
+  [ 'bargainseatsonline.com,livenation.com,stubhub.com,ticketfly.com,ticketliquidator.com,ticketmaster.com,tickets.com,ticketsnow.com,ticketweb.com,vividseats.com',
+    'events & tickets' ],
+  [ 'babiesrus.com,brothers-brick.com,etoys.com,fao.com,fisher-price.com,hasbro.com,hasbrotoyshop.com,lego.com,legoland.com,mattel.com,toys.com,toysrus.com,toystogrowon.com,toywiz.com',
+    'toys & games' ],
+  [ 'challengegames.nfl.com,fantasy.nfl.com,fantasyfootballblog.net,fantasyfootballcafe.com,fantasyfootballnerd.com,fantasysmarts.com,fftoday.com,fftoolbox.com,football.fantasysports.yahoo.com,footballsfuture.com,mrfootball.com,officefootballpool.com,thehuddle.com',
+    'fantasy football' ],
+  [ 'dailyjoust.com,draftday.com,draftking.com,draftkings.com,draftstreet.com,fanduel.com,realmoneyfantasyleagues.com,thedailyaudible.com',
+    'fantasy sports' ],
+  [ 'cdmsports.com,fanball.com,fantasyguru.com,fantasynews.cbssports.com,fantasyquestions.com,fantasyrundown.com,fantasysharks.com,fantasysports.yahoo.com,fantazzle.com,fantrax.com,fleaflicker.com,junkyardjake.com,kffl.com,mockdraftcentral.com,myfantasyleague.com,rototimes.com,rotowire.com,rotoworld.com,rtsports.com,whatifsports.com',
+    'fantasy sports' ],
+  [ 'football.about.com,football.com,footballoutsiders.com,nationalfootballpost.com,nflalumni.org,nflpa.com,nfltraderumors.co,profootballhof.com,profootballtalk.com,profootballtalk.nbcsports.com,profootballweekly.com',
+    'football' ],
+  [ '49ers.com,atlantafalcons.com,azcardinals.com,baltimoreravens.com,bengals.com,buccaneers.com,buffalobills.com,chargers.com,chicagobears.com,clevelandbrowns.com,colts.com,dallascowboys.com,denverbroncos.com,detroitlions.com,giants.com,houstontexans.com,jaguars.com,kcchiefs.com,miamidolphins.com,neworleanssaints.com,newyorkjets.com,packers.com,panthers.com,patriots.com,philadelphiaeagles.com,raiders.com,redskins.com,seahawks.com,steelers.com,stlouisrams.com,titansonline.com,vikings.com',
+    'football' ],
+  [ 'baseball-reference.com,baseballamerica.com,europeantour.com,golf.com,golfdigest.com,lpga.com,milb.com,minorleagueball.com,mlb.com,mlb.mlb.com,nascar.com,nba.com,ncaa.com,nhl.com,pga.com,pgatour.com,prowrestling.com,surfermag.com,surfline.com,surfshot.com,thehockeynews.com,tsn.com,ufc.com,worldgolfchampionships.com,wwe.com',
+    'sports' ],
+  [ '247sports.com,active.com,armslist.com,basketball-reference.com,bigten.org,bleacherreport.com,bleedinggreennation.com,bloodyelbow.com,cagesideseats.com,cbssports.com,cinesport.com,collegespun.com,cricbuzz.com,crictime.com,csnphilly.com,csnwashington.com,cstv.com,eastbay.com,espn.com,espn.go.com,espncricinfo.com,espnfc.com,espnfc.us,espnradio.com,eteamz.com,fanatics.com,fansided.com,fbschedules.com,fieldandstream.com,flightclub.com,foxsports.com,givemesport.com,goduke.com,goheels.com,golfchannel.com,golfnow.com,grantland.com,grindtv.com,hoopshype.com,icc-cricket.com,imleagues.com,kentuckysportsradio.com,larrybrownsports.com,leaguelineup.com,maxpreps.com,mlbtraderumors.com,mmafighting.com,mmajunkie.com,mmamania.com,msn.foxsports.com,myscore.com,nbcsports.com,nbcsports.msnbc.com,nesn.com,rantsports.com,realclearsports.com,reserveamerica.com,rivals.com,runnersworld.com,sbnation.com,scout.com,sherdog.com,si.com,speedsociety.com,sportingnews.com,sports.yahoo.com,sportsillustrated.cnn.com,sportsmanias.com,sportsmonster.us,sportsonearth.com,stack.com,teamworkonline.com,thebiglead.com,thescore.com,trails.com,triblive.com,upickem.net,usatodayhss.com,watchcric.net,yardbarker.com',
+    'sports news' ],
+  [ 'adidas.com,backcountry.com,backcountrygear.com,cabelas.com,champssports.com,competitivecyclist.com,dickssportinggoods.com,finishline.com,footlocker.com,ladyfootlocker.com,modells.com,motosport.com,mountaingear.com,newbalance.com,nike.com,patagonia.com,puma.com,reebok.com,sportsmansguide.com,steepandcheap.com,tgw.com,thenorthface.com',
+    'sports & outdoor goods' ],
+  [ 'airdroid.com,android-developers.blogspot.com,android.com,androidandme.com,androidapplications.com,androidapps.com,androidauthority.com,androidcommunity.com,androidfilehost.com,androidforums.com,androidguys.com,androidheadlines.com,androidpit.com,androidspin.com,androidtapp.com,androinica.com,droid-life.com,droidforums.net,droidviews.com,droidxforums.com,forum.xda-developers.com,phandroid.com,play.google.com,shopandroid.com,talkandroid.com,theandroidsoul.com,thedroidguy.com,videodroid.org',
+    'technology' ],
+  [ '9to5mac.com,appadvice.com,apple.com,appleinsider.com,appleturns.com,appsafari.com,cultofmac.com,everymac.com,insanelymac.com,iphoneunlockspot.com,isource.com,itunes.apple.com,lowendmac.com,mac-forums.com,macdailynews.com,macenstein.com,macgasm.net,macintouch.com,maclife.com,macnews.com,macnn.com,macobserver.com,macosx.com,macpaw.com,macrumors.com,macsales.com,macstories.net,macupdate.com,macuser.co.uk,macworld.co.uk,macworld.com,maxiapple.com,spymac.com,theapplelounge.com',
+    'technology' ],
+  [ 'adobe.com,asus.com,avast.com,data.com,formstack.com,gboxapp.com,gotomeeting.com,hp.com,htc.com,ibm.com,intel.com,java.com,logme.in,mcafee.com,mcafeesecure.com,microsoftstore.com,norton.com,office.com,office365.com,opera.com,oracle.com,proboards.com,samsung.com,sourceforge.net,squarespace.com,techtarget.com,ultipro.com,uniblue.com,web.com,winzip.com',
+    'technology' ],
+  [ '3dprint.com,4sysops.com,access-programmers.co.uk,accountingweb.com,afterdawn.com,akihabaranews.com,appsrumors.com,avg.com,belkin.com,besttechinfo.com,betanews.com,botcrawl.com,breakingmuscle.com,cheap-phones.com,chip.de,chip.eu,citeworld.com,cleanpcremove.com,commentcamarche.net,computer.org,computerhope.com,computershopper.com,computerweekly.com,contextures.com,coolest-gadgets.com,csoonline.com,daniweb.com,datacenterknowledge.com,ddj.com,devicemag.com,digitaltrends.com,dottech.org,dslreports.com,edugeek.net,eetimes.com,epic.com,eurekalert.org,eweek.com,experts-exchange.com,fosshub.com,freesoftwaremagazine.com,funkyspacemonkey.com,futuremark.com,gadgetreview.com,gizmodo.co.uk,globalsecurity.org,gunup.com,guru3d.com,head-fi.org,hexus.net,hothardware.com,howtoforge.com,idg.com.au,idownloadblog.com,ihackmyi.com,ilounge.com,infomine.com,intellireview.com,intomobile.com,iphonehacks.com,ismashphone.com,it168.com,itechpost.com,itpro.co.uk,jailbreaknation.com,laptoping.com,lightreading.com,malwaretips.com,mediaroom.com,mobilemag.com,modmyi.com,modmymobile.com,mophie.com,mozillazine.org,neoseeker.com,neowin.net,newsoxy.com,nextadvisor.com,notebookcheck.com,notebookreview.com,nvidia.com,orafaq.com,osdir.com,osxdaily.com,our-hometown.com,pchome.net,pconline.com.cn,pcpop.com,pcpro.co.uk,pcreview.co.uk,pcrisk.com,pcwelt.de,phonerebel.com,phonescoop.com,physorg.com,pocket-lint.com,post-theory.com,prnewswire.co.uk,programming4.us,quickpwn.com,redmondpie.com,redorbit.com,safer-networking.org,scientificblogging.com,sciverse.com,servicerow.com,sinfuliphone.com,singularityhub.com,slashgear.com,softonic.com,softonic.com.br,softonic.fr,sophos.com,sparkfun.com,speedguide.net,stuff.tv,symantec.com,taplikahome.com,techdailynews.net,techeblog.com,techie-buzz.com,techniqueworld.com,technobuffalo.com,technologyreview.com,technologytell.com,techpowerup.com,techpp.com,techradar.com,techshout.com,techworld.com,techworld.com.au,techworldtweets.com,telecomfile.com,tgdaily.com,theinquirer.net,thenextweb.com,theregister.co.uk,thermofisher.com,thewindowsclub.com,tomsitpro.com,trustedreviews.com,tuaw.com,tweaktown.com,unwiredview.com,wccftech.com,webmonkey.com,webpronews.com,windows7codecs.com,windowscentral.com,windowsitpro.com,windowstechies.com,winsupersite.com,wired.co.uk,wp-themes.com,xml.com,zol.com.cn',
+    'technology' ],
   [ 'addons.mozilla.org,air.mozilla.org,blog.mozilla.org,bugzilla.mozilla.org,developer.mozilla.org,etherpad.mozilla.org,forums.mozillazine.org,hacks.mozilla.org,hg.mozilla.org,mozilla.org,planet.mozilla.org,quality.mozilla.org,support.mozilla.org,treeherder.mozilla.org,wiki.mozilla.org',
     'Mozilla' ],
-  [ '3dprint.com,4sysops.com,access-programmers.co.uk,accountingweb.com,addictivetips.com,adweek.com,afterdawn.com,akihabaranews.com,anandtech.com,appsrumors.com,arstechnica.com,belkin.com,besttechinfo.com,betanews.com,bgr.com,botcrawl.com,breakingmuscle.com,canonrumors.com,cheap-phones.com,chip.de,chip.eu,cio.com,citeworld.com,cleanpcremove.com,cnet.com,commentcamarche.net,computer.org,computerhope.com,computershopper.com,computerweekly.com,contextures.com,coolest-gadgets.com,crn.com,csoonline.com,daniweb.com,data.com,datacenterknowledge.com,ddj.com,devicemag.com,digitaltrends.com,dottech.org,dpreview.com,dslreports.com,edugeek.net,eetimes.com,engadget.com,epic.com,eurekalert.org,eweek.com,experts-exchange.com,extremetech.com,fosshub.com,freesoftwaremagazine.com,funkyspacemonkey.com,futuremark.com,gadgetreview.com,ghacks.net,gizmodo.co.uk,gizmodo.com,globalsecurity.org,greenbot.com,gunup.com,guru3d.com,head-fi.org,hexus.net,hothardware.com,howtoforge.com,idg.com.au,idigitaltimes.com,idownloadblog.com,ihackmyi.com,ilounge.com,infomine.com,informationweek.com,intellireview.com,intomobile.com,iphonehacks.com,ismashphone.com,isource.com,it168.com,itechpost.com,itpro.co.uk,itworld.com,jailbreaknation.com,kioskea.net,laptoping.com,laptopmag.com,lightreading.com,livescience.com,malwaretips.com,mediaroom.com,mobilemag.com,modmyi.com,modmymobile.com,mophie.com,mozillazine.org,neoseeker.com,neowin.net,newscientist.com,newsoxy.com,nextadvisor.com,notebookcheck.com,notebookreview.com,nvidia.com,nwc.com,orafaq.com,osdir.com,osxdaily.com,our-hometown.com,pcadvisor.co.uk,pchome.net,pcmag.com,pconline.com.cn,pcpop.com,pcpro.co.uk,pcreview.co.uk,pcrisk.com,pcwelt.de,phonerebel.com,phonescoop.com,physorg.com,pocket-lint.com,post-theory.com,prnewswire.co.uk,prnewswire.com,programming4.us,quickpwn.com,readwrite.com,redmondpie.com,redorbit.com,reviewed.com,safer-networking.org,sciencedaily.com,sciencenews.org,scientificamerican.com,scientificblogging.com,sciverse.com,servicerow.com,sinfuliphone.com,singularityhub.com,slashdot.org,slashgear.com,softonic.com,softonic.com.br,softonic.fr,sophos.com,space.com,sparkfun.com,speedguide.net,stuff.tv,techdailynews.net,techdirt.com,techeblog.com,techhive.com,techie-buzz.com,technewsworld.com,techniqueworld.com,technobuffalo.com,technologyreview.com,technologytell.com,techpowerup.com,techpp.com,techrepublic.com,techshout.com,techweb.com,techworld.com,techworld.com.au,techworldtweets.com,telecomfile.com,tgdaily.com,theinquirer.net,thenextweb.com,theregister.co.uk,thermofisher.com,theverge.com,thewindowsclub.com,tomsguide.com,tomshardware.com,tomsitpro.com,toptenreviews.com,trustedreviews.com,tuaw.com,tweaktown.com,ubergizmo.com,unwiredview.com,venturebeat.com,wccftech.com,webmonkey.com,webpronews.com,windows7codecs.com,windowscentral.com,windowsitpro.com,windowstechies.com,winsupersite.com,wired.co.uk,wired.com,wp-themes.com,xda-developers.com,xml.com,zdnet.com,zmescience.com,zol.com.cn',
-    'Technology' ],
-  [ '9to5mac.com,appadvice.com,apple.com,appleinsider.com,appleturns.com,appsafari.com,cultofmac.com,everymac.com,insanelymac.com,iphoneunlockspot.com,isource.com,itunes.apple.com,lowendmac.com,mac-forums.com,macdailynews.com,macenstein.com,macgasm.net,macintouch.com,maclife.com,macnews.com,macnn.com,macobserver.com,macosx.com,macpaw.com,macrumors.com,macsales.com,macstories.net,macupdate.com,macuser.co.uk,macworld.co.uk,macworld.com,maxiapple.com,spymac.com,theapplelounge.com',
-    'Technology' ],
-  [ 'alistapart.com,answers.microsoft.com,backpack.openbadges.org,blog.chromium.org,caniuse.com,codefirefox.com,codepen.io,css-tricks.com,css3generator.com,cssdeck.com,csswizardry.com,devdocs.io,docs.angularjs.org,ghacks.net,github.com,html5demos.com,html5rocks.com,html5test.com,iojs.org,khanacademy.org,l10n.mozilla.org,learn.jquery.com,marketplace.firefox.com,mozilla-hispano.org,mozillians.org,news.ycombinator.com,npmjs.com,packagecontrol.io,quirksmode.org,readwrite.com,reps.mozilla.org,smashingmagazine.com,stackoverflow.com,status.modern.ie,teamtreehouse.com,tutorialspoint.com,udacity.com,validator.w3.org,w3.org,w3cschool.cc,w3schools.com,whatcanidoformozilla.org',
-    'Web Development' ],
-  [ 'classroom.google.com,codecademy.com,elearning.ut.ac.id,khanacademy.org,learn.jquery.com,teamtreehouse.com,tutorialspoint.com,udacity.com,w3cschool.cc,w3schools.com',
-    'Web Education' ],
-  [ 'abebooks.co.uk,abebooks.com,alibris.com,allaboutcircuits.com,allyoucanbooks.com,answersingenesis.org,artnet.com,audiobooks.com,barnesandnoble.com,barnesandnobleinc.com,bartleby.com,betterworldbooks.com,biggerbooks.com,bncollege.com,bookbyte.com,bookdepository.com,bookfinder.com,bookrenter.com,booksamillion.com,booksite.com,boundless.com,brookstone.com,btol.com,calibre-ebook.com,campusbookrentals.com,casadellibro.com,cbomc.com,cengagebrain.com,chapters.indigo.ca,christianbook.com,ciscopress.com,coursesmart.com,cqpress.com,crafterschoice.com,crossings.com,cshlp.org,deseretbook.com,directtextbook.com,discountmags.com,doubledaybookclub.com,doubledaylargeprint.com,doverpublications.com,ebooks.com,ecampus.com,fellabooks.net,fictionwise.com,flatworldknowledge.com,grolier.com,harpercollins.com,hayhouse.com,historybookclub.com,hpb.com,hpbmarketplace.com,interweave.com,iseeme.com,katiekazoo.com,knetbooks.com,learnoutloud.com,librarything.com,literaryguild.com,lulu.com,lww.com,macmillan.com,magazines.com,mbsdirect.net,militarybookclub.com,mypearsonstore.com,mysteryguild.com,netplaces.com,noble.com,novelguide.com,onespirit.com,oxfordjournals.org,paperbackswap.com,papy.co.jp,peachpit.com,penguin.com,penguingroup.com,pimsleur.com,powells.com,qpb.com,quepublishing.com,reviews.com,rhapsodybookclub.com,rodalestore.com,royalsocietypublishing.org,sagepub.com,scrubsmag.com,sfbc.com,simonandschuster.com,simonandschuster.net,simpletruths.com,teach12.net,textbooks.com,textbookx.com,thegoodcook.com,thriftbooks.com,tlsbooks.com,toshibabookplace.com,tumblebooks.com,urbookdownload.com,valorebooks.com,valuemags.com,wwnorton.com,zoobooks.com',
-    'Literature' ],
-  [ 'aceshowbiz.com,aintitcoolnews.com,askkissy.com,askmen.com,atraf.co.il,audioboom.com,beamly.com,blippitt.com,bollywoodlife.com,bossip.com,buzzlamp.com,celebdirtylaundry.com,celebfocus.com,celebitchy.com,celebrity-gossip.net,celebrityabout.com,celebwild.com,chisms.net,concertboom.com,crushable.com,cultbox.co.uk,dailyentertainmentnews.com,dayscafe.com,deadline.com,deathandtaxesmag.com,diaryofahollywoodstreetking.com,digitalspy.com,egotastic.com,empirenews.net,enelbrasero.com,everydaycelebs.com,ew.com,extratv.com,facade.com,fanaru.com,fhm.com,geektyrant.com,glamourpage.com,heatworld.com,hlntv.com,hollyscoop.com,hollywoodreporter.com,hollywoodtuna.com,hypable.com,infotransfer.net,insideedition.com,interaksyon.com,jezebel.com,justjared.com,justjaredjr.com,komando.com,koreaboo.com,maxgo.com,maxim.com,maxviral.com,mediatakeout.com,mosthappy.com,moviestalk.com,my.ology.com,ngoisao.net,nofilmschool.com,nolocreo.com,octane.tv,ouchpress.com,people.com,peopleenespanol.com,perezhilton.com,pinkisthenewblog.com,platotv.tv,playbill.com,playbillvault.com,playgroundmag.net,popeater.com,popnhop.com,popsugar.co.uk,popsugar.com,purepeople.com,radaronline.com,rantchic.com,reshareworthy.com,rinkworks.com,ripbird.com,sara-freder.com,screenjunkies.com,soapcentral.com,soapoperadigest.com,sobadsogood.com,splitsider.com,starcasm.net,starpulse.com,straightfromthea.com,stupidcelebrities.net,stupiddope.com,tbn.org,theawesomedaily.com,theawl.com,thefrisky.com,thefw.com,theresacaputo.com,thezooom.com,tvnotas.com.mx,twanatells.com,vanswarpedtour.com,vietgiaitri.com,viral.buzz,vulture.com,wakavision.com,worthytales.net,wwtdd.com,younghollywood.com',
-    'Entertainment News' ],
-  [ '247wallst.com,4-traders.com,advfn.com,agweb.com,allbusiness.com,barchart.com,barrons.com,beckershospitalreview.com,benzinga.com,bizjournals.com,bizsugar.com,bloomberg.com,bloomberglaw.com,business-standard.com,businessinsider.com,businessinsider.com.au,businesspundit.com,businessweek.com,businesswire.com,cboe.com,cheatsheet.com,chicagobusiness.com,cjonline.com,cnbc.com,cnnmoney.com,cqrcengage.com,dailyfinance.com,dailyfx.com,dealbreaker.com,djindexes.com,dowjones.com,easierstreetdaily.com,economist.com,economyandmarkets.com,economywatch.com,edweek.org,eleconomista.es,entrepreneur.com,etfdailynews.com,etfdb.com,ewallstreeter.com,fastcolabs.com,fastcompany.com,financeformulas.net,financialpost.com,flife.de,forbes.com,forexpros.com,fortune.com,foxbusiness.com,ft.com,ftpress.com,fx-exchange.com,hbr.org,howdofinance.com,ibtimes.com,inc.com,investopedia.com,investors.com,investorwords.com,journalofaccountancy.com,kiplinger.com,lendingandcredit.net,lfb.org,mainstreet.com,markettraders.com,marketwatch.com,maxkeiser.com,minyanville.com,ml.com,moneycontrol.com,moneymappress.com,moneynews.com,moneysavingexpert.com,morningstar.com,mortgagenewsdaily.com,motleyfool.com,mt.co.kr,nber.org,nyse.com,oilprice.com,pewsocialtrends.org,principal.com,qz.com,rantfinance.com,realclearmarkets.com,recode.net,reuters.ca,reuters.co.in,reuters.co.uk,reuters.com,rttnews.com,seekingalpha.com,smallbiztrends.com,streetinsider.com,thecheapinvestor.com,theeconomiccollapseblog.com,themoneyconverter.com,thestreet.com,tickertech.com,tradingeconomics.com,updown.com,valuewalk.com,wikinvest.com,wsj.com,zacks.com',
-    'Financial News' ],
-  [ '10tv.com,8newsnow.com,9news.com,abc.net.au,abc7.com,abc7chicago.com,abcnews.go.com,aclu.org,activistpost.com,ajc.com,al.com,alan.com,alarab.net,aljazeera.com,americanthinker.com,app.com,aristeguinoticias.com,azcentral.com,baltimoresun.com,becomingminimalist.com,beforeitsnews.com,bigstory.ap.org,blackamericaweb.com,bloomberg.com,bloombergview.com,boston.com,bostonherald.com,breitbart.com,buffalonews.com,c-span.org,canada.com,cbs46.com,cbsnews.com,chicagotribune.com,chron.com,citizensvoice.com,citylab.com,cleveland.com,cnn.com,coed.com,countercurrentnews.com,courant.com,ctvnews.ca,dailyherald.com,dailynews.com,dallasnews.com,delawareonline.com,democratandchronicle.com,democraticunderground.com,democrats.org,denverpost.com,desmoinesregister.com,dispatch.com,elcomercio.pe,english.aljazeera.net,examiner.com,farsnews.com,firstcoastnews.com,firstpost.com,firsttoknow.com,foreignpolicy.com,foxnews.com,freebeacon.com,freep.com,fresnobee.com,gazette.com,global.nytimes.com,heraldtribune.com,hindustantimes.com,hngn.com,humanevents.com,huzlers.com,indiatimes.com,indystar.com,irishtimes.com,jacksonville.com,jpost.com,jsonline.com,kansascity.com,kctv5.com,kentucky.com,kickerdaily.com,king5.com,kmov.com,knoxnews.com,kpho.com,kvue.com,kwqc.com,kxan.com,lainformacion.com,latimes.com,ldnews.com,lex18.com,linternaute.com,livemint.com,lostateminor.com,m24.ru,macleans.ca,manchestereveningnews.co.uk,marinecorpstimes.com,masslive.com,mavikocaeli.com.tr,mcall.com,medium.com,mentalfloss.com,mercurynews.com,metro.us,miamiherald.com,militarytimes.com,mk.ru,mlive.com,mondotimes.com,montrealgazette.com,msnbc.com,msnewsnow.com,mynews13.com,mysanantonio.com,mysuncoast.com,nbclosangeles.com,nbcnewyork.com,nbcphiladelphia.com,ndtv.com,newindianexpress.com,news.cincinnati.com,news.google.com,news.msn.com,news.yahoo.com,news10.net,news8000.com,newsday.com,newsdaymarketing.net,newsen.com,newsmax.com,newsobserver.com,newsok.com,newsru.ua,newstatesman.com,newszoom.com,nj.com,nola.com,northjersey.com,nouvelobs.com,npr.org,nwfdailynews.com,nwitimes.com,nydailynews.com,nytimes.com,observer.com,ocregister.com,okcfox.com,omaha.com,onenewspage.com,ontheissues.org,oregonlive.com,orlandosentinel.com,palmbeachpost.com,pe.com,pennlive.com,philly.com,pilotonline.com,polar.com,post-gazette.com,postandcourier.com,presstelegram.com,presstv.ir,propublica.org,providencejournal.com,realclearpolitics.com,recorderonline.com,reporterdock.com,reporterherald.com,respublica.al,reuters.com,rg.ru,roanoke.com,sacbee.com,scmp.com,scnow.com,sdpnoticias.com,seattletimes.com,semana.com,sfgate.com,sharepowered.com,sinembargo.mx,slate.com,sltrib.com,sotomayortv.com,sourcewatch.org,spectator.co.uk,squaremirror.com,star-telegram.com,staradvertiser.com,startribune.com,statesman.com,stltoday.com,streetwise.co,stuff.co.nz,success.com,suffolknewsherald.com,sun-sentinel.com,sunnewsnetwork.ca,suntimes.com,supernewschannel.com,surenews.com,svoboda.org,syracuse.com,tampabay.com,tbd.com,telegram.com,telegraph.co.uk,tennessean.com,the-open-mind.com,theadvocate.com,theage.com.au,theatlantic.com,thebarefootwriter.com,theblaze.com,thecalifornian.com,thedailysheeple.com,thefix.com,theintelligencer.net,thelocal.com,thenational.ae,thenewstribune.com,theparisreview.org,thereporter.com,therepublic.com,thestar.com,thetelegram.com,thetimes.co.uk,theuspatriot.com,time.com,timescall.com,timesdispatch.com,timesleaderonline.com,timesofisrael.com,toledoblade.com,toprightnews.com,townhall.com,tpnn.com,trendolizer.com,triblive.com,tribune.com.pk,tricities.com,troymessenger.com,trueactivist.com,truthandaction.org,tsn.ua,tulsaworld.com,twincities.com,upi.com,usatoday.com,utsandiego.com,vagazette.com,viralwomen.com,vitalworldnews.com,voasomali.com,vox.com,washingtonexaminer.com,washingtonpost.com,watchdog.org,wave3.com,wavy.com,wbay.com,wbtw.com,wcpo.com,wctrib.com,wdtn.com,weeklystandard.com,westernjournalism.com,wfsb.com,wgrz.com,whas11.com,winonadailynews.com,wishtv.com,wistv.com,wkbn.com,wkow.com,wlfi.com,wmtw.com,wmur.com,wopular.com,world-top-news.com,worldnews.com,wplol.us,wpsdlocal6.com,wptz.com,wric.com,wsmv.com,wthitv.com,wthr.com,wtnh.com,wtol.com,wtsp.com,wvec.com,wwlp.com,wwltv.com,wyff4.com,yonhapnews.co.kr,yourbreakingnews.com',
-    'News' ],
-  [ '2k.com,360game.vn,4399.com,a10.com,activision.com,addictinggames.com,alawar.com,alienwarearena.com,anagrammer.com,andkon.com,aq.com,arcadeprehacks.com,arcadeyum.com,arcgames.com,archeagegame.com,armorgames.com,askmrrobot.com,battle.net,battlefieldheroes.com,bigfishgames.com,bigpoint.com,bioware.com,bluesnews.com,boardgamegeek.com,bollyheaven.com,bubblebox.com,bukkit.org,bungie.net,buycraft.net,callofduty.com,candystand.com,cda.pl,challonge.com,championselect.net,cheapassgamer.com,cheatcc.com,cheatengine.org,cheathappens.com,chess.com,civfanatics.com,clashofclans-tools.com,clashofclansbuilder.com,comdotgame.com,commonsensemedia.org,coolrom.com,crazygames.com,csgolounge.com,curse.com,d20pfsrd.com,destructoid.com,diablofans.com,diablowiki.net,didigames.com,dota2.com,dota2lounge.com,dressupgames.com,dulfy.net,ebog.com,elderscrollsonline.com,elitedangerous.com,elitepvpers.com,emuparadise.me,enjoydressup.com,escapegames24.com,escapistmagazine.com,eventhubs.com,eveonline.com,farming-simulator.com,feed-the-beast.com,flashgames247.com,flightrising.com,flipline.com,flonga.com,freegames.ws,freeonlinegames.com,fresh-hotel.org,friv.com,friv.today,fullypcgames.net,funny-games.biz,funtrivia.com,futhead.com,g2a.com,gamasutra.com,game-debate.com,game-oldies.com,game321.com,gamebaby.com,gamebaby.net,gamebanana.com,gamefaqs.com,gamefly.com,gamefront.com,gamegape.com,gamehouse.com,gameinformer.com,gamejolt.com,gamemazing.com,gamemeteor.com,gamerankings.com,gamersgate.com,games-msn.com,games-workshop.com,games.com,games2girls.com,gamesbox.com,gamesfreak.net,gametop.com,gametracker.com,gametrailers.com,gamezhero.com,gbatemp.net,geforce.com,gematsu.com,giantbomb.com,girl.me,girlsgames123.com,girlsplay.com,gog.com,gogames.me,gonintendo.com,goodgamestudios.com,gosugamers.net,greenmangaming.com,gtaforums.com,gtainside.com,guildwars2.com,hackedarcadegames.com,hearthpwn.com,hirezstudios.com,hitbox.tv,hltv.org,howrse.com,icy-veins.com,indiedb.com,jayisgames.com,jigzone.com,joystiq.com,juegosdechicas.com,kabam.com,kbhgames.com,kerbalspaceprogram.com,king.com,kixeye.com,kizi.com,kogama.com,kongregate.com,kotaku.com,lolcounter.com,lolking.net,lolnexus.com,lolpro.com,lolskill.net,lootcrate.com,lumosity.com,mafa.com,mangafox.me,mangapark.com,mariowiki.com,maxgames.com,megagames.com,metacritic.com,mindjolt.com,minecraft.net,minecraftforum.net,minecraftservers.org,minecraftskins.com,mineplex.com,miniclip.com,mmo-champion.com,mmobomb.com,mmohuts.com,mmorpg.com,mmosite.com,mobafire.com,moddb.com,modxvm.com,mojang.com,moshimonsters.com,mousebreaker.com,moviestarplanet.com,mtgsalvation.com,muchgames.com,myonlinearcade.com,myplaycity.com,myrealgames.com,mythicspoiler.com,n4g.com,newgrounds.com,nexon.net,nexusmods.com,ninjakiwi.com,nintendo.com,nintendoeverything.com,nintendolife.com,nitrome.com,nosteam.ro,notdoppler.com,noxxic.com,operationsports.com,origin.com,ownedcore.com,pacogames.com,pathofexile.com,pcgamer.com,pch.com,pcsx2.net,penny-arcade.com,planetminecraft.com,plarium.com,playdota.com,playpink.com,playsides.com,playstationlifestyle.net,playstationtrophies.org,pog.com,pokemon.com,polygon.com,popcap.com,primarygames.com,probuilds.net,ps3hax.net,psnprofiles.com,psu.com,qq.com,r2games.com,resourcepack.net,retrogamer.com,rewardtv.com,riotgames.com,robertsspaceindustries.com,roblox.com,robocraftgame.com,rockpapershotgun.com,rockstargames.com,roosterteeth.com,runescape.com,schoolofdragons.com,screwattack.com,scufgaming.com,segmentnext.com,shacknews.com,shockwave.com,shoryuken.com,siliconera.com,silvergames.com,skydaz.com,smashbros.com,solomid.net,starcitygames.com,starsue.net,steamcommunity.com,steamgifts.com,strategywiki.org,supercheats.com,surrenderat20.net,swtor.com,tankionline.com,tcgplayer.com,teamfortress.com,teamliquid.net,tetrisfriends.com,thesims3.com,thesimsresource.com,thetechgame.com,topg.org,totaljerkface.com,toucharcade.com,transformice.com,trueachievements.com,twcenter.net,twitch.tv,twoplayergames.org,unity3d.com,vg247.com,vgchartz.com,videogamesblogger.com,warframe.com,warlight.net,warthunder.com,watchcartoononline.com,websudoku.com,wildstar-online.com,wildtangent.com,wineverygame.com,wizards.com,worldofsolitaire.com,worldoftanks.com,wowhead.com,wowprogress.com,wowwiki.com,xbox.com,xbox360iso.com,xboxachievements.com,xfire.com,xtremetop100.com,y8.com,yoyogames.com,zybez.net,zynga.com',
-    'Video Game' ],
+  [ 'addictivetips.com,allthingsd.com,anandtech.com,androidcentral.com,androidpolice.com,arstechnica.com,bgr.com,boygeniusreport.com,cio.com,cnet.com,computerworld.com,crn.com,electronista.com,engadget.com,extremetech.com,fastcocreate.com,fastcodesign.com,fastcoexist.com,frontlinek12.com,gigaom.com,gizmag.com,gizmodo.com,greenbot.com,howtogeek.com,idigitaltimes.com,imore.com,informationweek.com,infoworld.com,itworld.com,kioskea.net,laptopmag.com,leadpages.net,lifehacker.com,mashable.com,networkworld.com,news.cnet.com,nwc.com,pastebin.com,pcadvisor.co.uk,pcmag.com,pcworld.com,phonearena.com,reviewed.com,serverfault.com,siteadvisor.com,slashdot.org,techcrunch.com,techdirt.com,techhive.com,technewsworld.com,techrepublic.com,techweb.com,tomsguide.com,tomshardware.com,ubergizmo.com,venturebeat.com,wired.com,xda-developers.com,zdnet.com',
+    'technology news' ],
+  [ 'bestbuy.ca,bestbuy.com,cdw.com,compusa.com,computerlivehelp.co,cyberguys.com,dell.com,digitalinsight.com,directron.com,ebuyer.com,frontierpc.com,frys-electronics-ads.com,frys.com,geeks.com,gyazo.com,homestead.com,lenovo.com,macmall.com,microcenter.com,miniinthebox.com,mwave.com,newegg.com,officedepot.com,outletpc.com,outpost.com,radioshack.com,rakuten.com,tigerdirect.com',
+    'tech retail' ],
+  [ 'chat.com,fring.com,hello.firefox.com,oovoo.com,viber.com',
+    'video chat' ],
+  [ 'alistapart.com,answers.microsoft.com,backpack.openbadges.org,blog.chromium.org,caniuse.com,codefirefox.com,codepen.io,css-tricks.com,css3generator.com,cssdeck.com,csswizardry.com,devdocs.io,docs.angularjs.org,ghacks.net,github.com,html5demos.com,html5rocks.com,html5test.com,iojs.org,l10n.mozilla.org,marketplace.firefox.com,mozilla-hispano.org,mozillians.org,news.ycombinator.com,npmjs.com,packagecontrol.io,quirksmode.org,readwrite.com,reps.mozilla.org,smashingmagazine.com,speckyboy.com,stackoverflow.com,status.modern.ie,validator.w3.org,w3.org,webreference.com,whatcanidoformozilla.org',
+    'web development' ],
+  [ 'classroom.google.com,codeacademy.org,codecademy.com,codeschool.com,codeyear.com,elearning.ut.ac.id,how-to-build-websites.com,htmlcodetutorial.com,htmldog.com,htmlplayground.com,learn.jquery.com,quackit.com,roseindia.net,teamtreehouse.com,tizag.com,tutorialspoint.com,udacity.com,w3schools.com,webdevelopersnotes.com',
+    'webdev education' ],
+  [ 'att.com,att.net,attonlineoffers.com,bell.ca,bellsouth.com,cableone.net,cablevision.com,centurylink.com,centurylink.net,centurylinkquote.com,charter-business.com,charter.com,charter.net,chartercabledeals.com,chartermedia.com,comcast.com,comcast.net,cox.com,cox.net,coxnewsweb.com,directv.com,dish.com,dishnetwork.com,freeconferencecall.com,frontier.com,hughesnet.com,liveitwithcharter.com,mycenturylink.com,mydish.com,net10.com,officialtvstream.com.es,optimum.com,optimum.net,paygonline.com,paytm.com,qwest.com,rcn.com,rebtel.com,ringcentral.com,straighttalkbyop.com,swappa.com,textem.net,timewarner.com,timewarnercable.com,tracfone.com,verizon.com,verizon.net,voipo.com,vonagebusiness.com,wayport.net,whistleout.com,wildblue.net,windstream.net,windstreambusiness.net,wowway.com,ww2.cox.com,xfinity.com',
+    'telecommunication' ],
+  [ 'alltel.com,assurancewireless.com,attsavings.com,boostmobile.com,boostmobilestore.com,budgetmobile.com,consumercellular.com,credomobile.com,gosmartmobile.com,h2owirelessnow.com,lycamobile.com,lycamobile.us,metropcs.com,motorola.com,mycricket.com,myfamilymobile.com,nextel.com,nokia.com,nokiausa.com,polarmobile.com,qlinkwireless.com,republicwireless.com,sprint.com,sprintpcs.com,straighttalk.com,t-mobile.co.uk,t-mobile.com,tmobile.com,tracfonewireless.com,uscellular.com,verizonwireless.com,virginmobile.com,virginmobile.com.au,virginmobileusa.com,vodafone.co.uk,vodafone.com,vodaphone.co.uk,vonange.com,vzwshop.com,wireless.att.com',
+    'mobile carrier' ],
+  [ 'aa.com,aerlingus.com,airasia.com,aircanada.com,airfrance.com,airindia.com,alaskaair.com,alaskaairlines.com,allegiantair.com,britishairways.com,cathaypacific.com,china-airlines.com,continental.com,delta.com,deltavacations.com,dragonair.com,easyjet.com,elal.co.il,emirates.com,flightaware.com,flyfrontier.com,frontierairlines.com,hawaiianair.com,iberia.com,jetairways.com,jetblue.com,klm.com,koreanair.com,kuwait-airways.com,lan.com,lufthansa.com,malaysiaairlines.com,mihinlanka.com,nwa.com,qantas.com.au,qatarairways.com,ryanair.com,singaporeair.com,smartfares.com,southwest.com,southwestvacations.com,spiritair.com,spiritairlines.com,thaiair.com,united.com,usairways.com,virgin-atlantic.com,virginamerica.com,virginblue.com.au',
+    'travel & airline' ],
+  [ 'carnival.com,celebrity-cruises.com,celebritycruises.com,costacruise.com,cruise.com,cruiseamerica.com,cruisecritic.com,cruisedirect.com,cruisemates.com,cruises.com,cruisesonly.com,crystalcruises.com,cunard.com,disneycruise.disney.go.com,hollandamerica.com,ncl.com,pocruises.com,princess.com,royalcaribbean.com,royalcaribbean.cruiselines.com,rssc.com,seabourn.com,silversea.com,starcruises.com,vikingrivercruises.com,windstarcruises.com',
+    'travel & cruise' ],
+  [ 'agoda.com,airbnb.com,beaches.com,bedandbreakfast.com,bestwestern.com,booking.com,caesars.com,choicehotels.com,comfortinn.com,daysinn.com,dealbase.com,doubletree3.hilton.com,embassysuites.com,fairmont.com,flipkey.com,fourseasons.com,greatwolf.com,hamptoninn.hilton.com,hamptoninn3.hilton.com,hhonors3.hilton.com,hilton.com,hiltongardeninn3.hilton.com,hiltonworldwide.com,holidayinn.com,homeaway.com,hotelclub.com,hotelopia.com,hotels.com,hotelscombined.com,hyatt.com,ihg.com,laterooms.com,lhw.com,lq.com,mandarinoriental.com,marriott.com,motel6.com,omnihotels.com,radisson.com,ramada.com,rci.com,reservationcounter.com,resortvacationstogo.com,ritzcarlton.com,roomkey.com,sheraton.com,starwoodhotels.com,starwoodhotelshawaii.com,super8.com,thetrain.com,vacationhomerentals.com,vacationrentals.com,vrbo.com,wyndhamrewards.com',
+    'hotel & resort' ],
+  [ 'airfarewatchdog.com,airliners.net,atlanta-airport.com,budgettravel.com,cntraveler.com,cntraveller.com,destination360.com,flightstats.com,flyertalk.com,fodors.com,frommers.com,letsgo.com,lonelyplanet.com,matadornetwork.com,perfectvacation.co,ricksteves.com,roughguides.com,timeout.com,travelalberta.us,travelandleisure.com,travelchannel.com,traveler.nationalgeographic.com,travelmath.com,traveltune.com,tripadvisor.com,vegas.com,viator.com,virtualtourist.com,wikitravel.org,worldtravelguide.net',
+    'travel' ],
+  [ 'aavacations.com,applevacations.com,avianca.com,bookingbuddy.com,bookit.com,cheapair.com,cheapcaribbean.com,cheapflights.com,cheapoair.com,cheaptickets.com,chinahighlights.com,costcotravel.com,ctrip.com,despegar.com,edreams.net,expedia.ca,expedia.com,fareboom.com,farebuzz.com,farecast.live.com,farecompare.com,faregeek.com,flightnetwork.com,funjet.com,golastminute.com,hipmunk.com,hotwire.com,ifly.com,justairticket.com,kayak.com,lastminute.com,lastminutetravel.com,lowestfare.com,lowfares.com,momondo.com,onetime.com,onetravel.com,orbitz.com,otel.com,priceline.com,pricelinevisa.com,sidestep.com,skyscanner.com,smartertravel.com,statravel.com,tigerair.com,travelocity.com,travelonbids.com,travelzoo.com,tripsta.com,trivago.com,universalorlando.com,universalstudioshollywood.com,vacationexpress.com,venere.com,webjet.com,yatra.com',
+    'travel' ],
+  [ 'airportrentalcars.com,alamo.com,amtrak.com,anytransitguide.com,avis.com,boltbus.com,budget.com,carrentalexpress.com,carrentals.com,coachusa.com,dollar.com,e-zrentacar.com,enterprise.com,europcar.com,foxrentacar.com,gotobus.com,greyhound.com,hertz.com,hertzondemand.com,indianrail.gov.in,irctc.co.in,megabus.com,mta.info,nationalcar.com,nationalrail.co.uk,njtransit.com,paylesscar.com,paylesscarrental.com,peterpanbus.com,raileurope.com,rentalcars.com,rideuta.com,stagecoachbus.com,thrifty.com,uber.com,wanderu.com,zipcar.com',
+    'travel & transit' ],
+  [ 'bulbagarden.net,cheatcc.com,cheatmasters.com,cheats.ign.com,comicvine.com,computerandvideogames.com,counter-strike.net,escapistmagazine.com,gamedaily.com,gamefront.com,gameinformer.com,gamerankings.com,gamespot.com,gamesradar.com,gamestop.com,gametrailers.com,gamezone.com,giantbomb.com,ign.com,kotaku.com,metacritic.com,minecraft-server-list.com,minecraftforge.net,minecraftforum.net,minecraftservers.org,minecraftskins.com,mmo-champion.com,mojang.com,pcgamer.com,planetminecraft.com,supercheats.com,thesims.com,totaljerkface.com,unity3d.com,vg247.com,wowhead.com',
+    'gaming' ],
+  [ 'a10.com,absolutist.com,addictinggames.com,aeriagames.com,agame.com,alpha-wars.com,arcadeyum.com,armorgames.com,ballerarcade.com,battle.net,battlefield.com,bigfishgames.com,bioware.com,bitrhymes.com,candystand.com,conjurorthegame.com,crazymonkeygames.com,crusharcade.com,curse.com,cuttherope.net,dreammining.com,dressupgames.com,ea.com,easports.com,fps-pb.com,freearcade.com,freeonlinegames.com,friv.com,funplusgame.com,gamefly.com,gameforge.com,gamehouse.com,gamejolt.com,gameloft.com,gameoapp.com,gamepedia.com,gamersfirst.com,games.com,games.yahoo.com,gamesgames.com,gamezhero.com,gamingwonderland.com,ganymede.eu,goodgamestudios.com,gpotato.com,gsn.com,guildwars2.com,hirezstudios.com,igg.com,iwin.com,kahoot.it,king.com,kizi.com,kongregate.com,leagueoflegends.com,lolking.net,maxgames.com,minecraft-mp.com,minecraft.net,miniclip.com,mmo-play.com,mmorpg.com,mobafire.com,moviestarplanet.com,myonlinearcade.com,needforspeed.com,newgrounds.com,nexusmods.com,nintendo.com,noxxic.com,onrpg.com,origin.com,pch.com,peakgames.net,playstation.com,pogo.com,pokemon.com,popcap.com,primarygames.com,r2games.com,railnation.us,riotgames.com,roblox.com,rockstargames.com,runescape.com,shockwave.com,silvergames.com,spore.com,steamcommunity.com,steampowered.com,stickpage.com,swtor.com,tetrisfriends.com,thegamerstop.com,thesims3.com,twitch.tv,warthunder.com,wildtangent.com,worldoftanks.com,worldofwarcraft.com,worldofwarplanes.com,worldofwarships.com,xbox.com,y8.com,zone.msn.com,zynga.com,zyngawithfriends.com',
+    'online gaming' ],
 ]);
 
 // Only allow link urls that are http(s)
 const ALLOWED_LINK_SCHEMES = new Set(["http", "https"]);
 
 // Only allow link image urls that are https or data
 const ALLOWED_IMAGE_SCHEMES = new Set(["https", "data"]);
 
@@ -641,16 +785,28 @@ let DirectoryLinksProvider = {
       // URIs without base domains will be allowed
       base = Services.eTLD.getBaseDomain(uri);
     }
     catch(ex) {}
     // Require a scheme match and the base only if desired
     return allowed.has(scheme) && (!checkBase || ALLOWED_URL_BASE.has(base));
   },
 
+  _escapeChars(text) {
+    let charMap = {
+      '&': '&amp;',
+      '<': '&lt;',
+      '>': '&gt;',
+      '"': '&quot;',
+      "'": '&#039;'
+    };
+
+    return text.replace(/[&<>"']/g, (character) => charMap[character]);
+  },
+
   /**
    * Gets the current set of directory links.
    * @param aCallback The function that the array of links is passed to.
    */
   getLinks: function DirectoryLinksProvider_getLinks(aCallback) {
     this._readDirectoryLinksFile().then(rawLinks => {
       // Reset the cache of suggested tiles and enhanced images for this new set of links
       this._enhancedLinks.clear();
@@ -673,18 +829,18 @@ let DirectoryLinksProvider = {
         if (name == undefined) {
           return;
         }
 
         let sanitizeFlags = ParserUtils.SanitizerCidEmbedsOnly |
           ParserUtils.SanitizerDropForms |
           ParserUtils.SanitizerDropNonCSSPresentation;
 
-        link.explanation = link.explanation ? ParserUtils.convertToPlainText(link.explanation, sanitizeFlags, 0) : "";
-        link.targetedName = ParserUtils.convertToPlainText(link.adgroup_name, sanitizeFlags, 0) || name;
+        link.explanation = this._escapeChars(link.explanation ? ParserUtils.convertToPlainText(link.explanation, sanitizeFlags, 0) : "");
+        link.targetedName = this._escapeChars(ParserUtils.convertToPlainText(link.adgroup_name, sanitizeFlags, 0) || name);
         link.lastVisitDate = rawLinks.suggested.length - position;
         // check if link wants to avoid inadjacent sites
         if (link.check_inadjacency) {
           this._avoidInadjacentSites = true;
         }
 
         // We cache suggested tiles here but do not push any of them in the links list yet.
         // The decision for which suggested tile to include will be made separately.
--- a/build/autoconf/android.m4
+++ b/build/autoconf/android.m4
@@ -255,16 +255,26 @@ if test -n "$MOZ_NATIVE_DEVICES" ; then
     AC_MSG_CHECKING([for v7 appcompat library])
     if ! test -e $ANDROID_APPCOMPAT_LIB ; then
         AC_MSG_ERROR([You must download the v7 app compat Android support library when targeting Android with native video casting support enabled.  Run the Android SDK tool and install Android Support Library under Extras.  See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_APPCOMPAT_LIB)])
     fi
     AC_MSG_RESULT([$ANDROID_APPCOMPAT_LIB])
     AC_SUBST(ANDROID_APPCOMPAT_LIB)
     AC_SUBST(ANDROID_APPCOMPAT_RES)
 
+    ANDROID_RECYCLERVIEW_LIB="$ANDROID_COMPAT_DIR_BASE/v7/recyclerview/libs/android-support-v7-recyclerview.jar"
+    ANDROID_RECYCLERVIEW_RES="$ANDROID_COMPAT_DIR_BASE/v7/recyclerview/res"
+    AC_MSG_CHECKING([for v7 recyclerview library])
+    if ! test -e $ANDROID_RECYCLERVIEW_LIB ; then
+        AC_MSG_ERROR([You must download the v7 recyclerview Android support library.  Run the Android SDK tool and install Android Support Library under Extras.  See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_RECYCLERVIEW_LIB)])
+    fi
+    AC_MSG_RESULT([$ANDROID_RECYCLERVIEW_LIB])
+    AC_SUBST(ANDROID_RECYCLERVIEW_LIB)
+    AC_SUBST(ANDROID_RECYCLERVIEW_RES)
+
     ANDROID_MEDIAROUTER_LIB="$ANDROID_COMPAT_DIR_BASE/v7/mediarouter/libs/android-support-v7-mediarouter.jar"
     ANDROID_MEDIAROUTER_RES="$ANDROID_COMPAT_DIR_BASE/v7/mediarouter/res"
     AC_MSG_CHECKING([for v7 mediarouter library])
     if ! test -e $ANDROID_MEDIAROUTER_LIB ; then
         AC_MSG_ERROR([You must download the v7 media router Android support library when targeting Android with native video casting support enabled.  Run the Android SDK tool and install Android Support Library under Extras.  See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_MEDIAROUTER_LIB)])
     fi
     AC_MSG_RESULT([$ANDROID_MEDIAROUTER_LIB])
     AC_SUBST(ANDROID_MEDIAROUTER_LIB)
--- a/js/public/GCAPI.h
+++ b/js/public/GCAPI.h
@@ -280,18 +280,18 @@ class GarbageCollectionEvent
     // Reference to a nullable, non-owned, statically allocated C string. If the
     // collection was forced to be non-incremental, this is a short reason of
     // why the GC could not perform an incremental collection.
     const char* nonincrementalReason;
 
     // Represents a single slice of a possibly multi-slice incremental garbage
     // collection.
     struct Collection {
-        int64_t startTimestamp;
-        int64_t endTimestamp;
+        double startTimestamp;
+        double endTimestamp;
     };
 
     // The set of garbage collection slices that made up this GC cycle.
     mozilla::Vector<Collection> collections;
 
     GarbageCollectionEvent(const GarbageCollectionEvent& rhs) = delete;
     GarbageCollectionEvent& operator=(const GarbageCollectionEvent& rhs) = delete;
 
--- a/js/src/gc/Statistics.cpp
+++ b/js/src/gc/Statistics.cpp
@@ -926,17 +926,17 @@ Statistics::beginSlice(const ZoneGCStats
                        SliceBudget budget, JS::gcreason::Reason reason)
 {
     this->zoneStats = zoneStats;
 
     bool first = !runtime->gc.isIncrementalGCInProgress();
     if (first)
         beginGC(gckind);
 
-    SliceData data(budget, reason, PRMJ_Now(), GetPageFaultCount());
+    SliceData data(budget, reason, PRMJ_Now(), JS_GetCurrentEmbedderTime(), GetPageFaultCount());
     if (!slices.append(data)) {
         // OOM testing fails if we CrashAtUnhandlableOOM here.
         aborted = true;
         return;
     }
 
     runtime->addTelemetry(JS_TELEMETRY_GC_REASON, reason);
 
@@ -949,16 +949,17 @@ Statistics::beginSlice(const ZoneGCStats
     }
 }
 
 void
 Statistics::endSlice()
 {
     if (!aborted) {
         slices.back().end = PRMJ_Now();
+        slices.back().endTimestamp = JS_GetCurrentEmbedderTime();
         slices.back().endFaults = GetPageFaultCount();
 
         int64_t sliceTime = slices.back().end - slices.back().start;
         runtime->addTelemetry(JS_TELEMETRY_GC_SLICE_MS, t(sliceTime));
         runtime->addTelemetry(JS_TELEMETRY_GC_RESET, !!slices.back().resetReason);
 
         if (slices.back().budget.isTimeBudget()) {
             int64_t budget = slices.back().budget.timeBudget.budget;
--- a/js/src/gc/Statistics.h
+++ b/js/src/gc/Statistics.h
@@ -206,29 +206,32 @@ struct Statistics
         if (phaseNestingDepth == 1)
             return phaseNesting[0] == PHASE_MUTATOR ? PHASE_NONE : phaseNesting[0];
         return phaseNesting[phaseNestingDepth - 1];
     }
 
     static const size_t MAX_NESTING = 20;
 
     struct SliceData {
-        SliceData(SliceBudget budget, JS::gcreason::Reason reason, int64_t start, size_t startFaults)
+        SliceData(SliceBudget budget, JS::gcreason::Reason reason, int64_t start,
+                  double startTimestamp, size_t startFaults)
           : budget(budget), reason(reason),
             resetReason(nullptr),
-            start(start), startFaults(startFaults)
+            start(start), startTimestamp(startTimestamp),
+            startFaults(startFaults)
         {
             for (auto i : mozilla::MakeRange(NumTimingArrays))
                 mozilla::PodArrayZero(phaseTimes[i]);
         }
 
         SliceBudget budget;
         JS::gcreason::Reason reason;
         const char* resetReason;
         int64_t start, end;
+        double startTimestamp, endTimestamp;
         size_t startFaults, endFaults;
         PhaseTimeTable phaseTimes;
 
         int64_t duration() const { return end - start; }
     };
 
     typedef Vector<SliceData, 8, SystemAllocPolicy> SliceDataVector;
     typedef SliceDataVector::ConstRange SliceRange;
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -8051,18 +8051,18 @@ GarbageCollectionEvent::Create(JSRuntime
             // same.
             data->reason = gcstats::ExplainReason(range.front().reason);
             MOZ_ASSERT(data->reason);
         }
 
         if (!data->collections.growBy(1))
             return nullptr;
 
-        data->collections.back().startTimestamp = range.front().start;
-        data->collections.back().endTimestamp = range.front().end;
+        data->collections.back().startTimestamp = range.front().startTimestamp;
+        data->collections.back().endTimestamp = range.front().endTimestamp;
     }
 
 
     return data;
 }
 
 static bool
 DefineStringProperty(JSContext* cx, HandleObject obj, PropertyName* propName, const char* strVal)
--- a/layout/xul/test/browser.ini
+++ b/layout/xul/test/browser.ini
@@ -1,7 +1,8 @@
 [DEFAULT]
 
 [browser_bug685470.js]
 [browser_bug703210.js]
 [browser_bug706743.js]
+skip-if = (os == 'linux') || e10s # Bug 1157576
 [browser_bug1163304.js]
 skip-if = os != 'linux' && os != 'win' // Due to testing menubar behavior with keyboard
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -59,32 +59,35 @@ GARBAGE_DIRS += classes db jars res sync
 # over changes in behaviour between versions.
 JAVA_BOOTCLASSPATH := \
     $(ANDROID_SDK)/android.jar \
     $(ANDROID_COMPAT_LIB) \
     $(NULL)
 
 JAVA_BOOTCLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_BOOTCLASSPATH)))
 
+JAVA_CLASSPATH += $(ANDROID_RECYCLERVIEW_LIB)
+
 # If native devices are enabled, add Google Play Services and some of the v7
 # compat libraries.
 ifdef MOZ_NATIVE_DEVICES
     JAVA_CLASSPATH += \
         $(GOOGLE_PLAY_SERVICES_LIB) \
         $(ANDROID_MEDIAROUTER_LIB) \
         $(ANDROID_APPCOMPAT_LIB) \
         $(NULL)
 endif
 
 JAVA_CLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_CLASSPATH)))
 
 # Library jars that we're bundling: these are subject to Proguard before inclusion
 # into classes.dex.
 java_bundled_libs := \
     $(ANDROID_COMPAT_LIB) \
+    $(ANDROID_RECYCLERVIEW_LIB) \
     $(NULL)
 
 ifdef MOZ_NATIVE_DEVICES
     java_bundled_libs += \
         $(GOOGLE_PLAY_SERVICES_LIB) \
         $(ANDROID_MEDIAROUTER_LIB) \
         $(ANDROID_APPCOMPAT_LIB) \
         $(NULL)
@@ -364,29 +367,33 @@ geckoview_resources.zip: $(all_resources
 # Make to treat the target differently, in a way that defeats our
 # dependencies.
 
 generated/org/mozilla/gecko/R.java: .aapt.deps ;
 
 # If native devices are enabled, add Google Play Services, build their resources
 generated/android/support/v7/appcompat/R.java: .aapt.deps ;
 generated/android/support/v7/mediarouter/R.java: .aapt.deps ;
+generated/android/support/v7/recyclerview/R.java: .aapt.deps ;
 generated/com/google/android/gms/R.java: .aapt.deps ;
 
 ifdef MOZ_NATIVE_DEVICES
     extra_packages += android.support.v7.appcompat
     extra_res_dirs += $(ANDROID_APPCOMPAT_RES)
 
     extra_packages += android.support.v7.mediarouter
     extra_res_dirs += $(ANDROID_MEDIAROUTER_RES)
 
     extra_packages += com.google.android.gms
     extra_res_dirs += $(GOOGLE_PLAY_SERVICES_RES)
 endif
 
+extra_packages += android.support.v7.recyclerview
+extra_res_dirs += $(ANDROID_RECYCLERVIEW_RES)
+
 gecko.ap_: .aapt.deps ;
 R.txt: .aapt.deps ;
 
 # [Comment 2/3] This tom-foolery provides a target that forces a
 # rebuild of gecko.ap_.  This is used during packaging to ensure that
 # resources are fresh.  The alternative would be complicated; see
 # [Comment 1/3].
 
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -39,16 +39,18 @@ resjar.generated_sources += [
     'org/mozilla/gecko/R.java',
 ]
 
 if CONFIG['MOZ_NATIVE_DEVICES']:
     resjar.generated_sources += ['com/google/android/gms/R.java']
     resjar.generated_sources += ['android/support/v7/appcompat/R.java']
     resjar.generated_sources += ['android/support/v7/mediarouter/R.java']
 
+resjar.generated_sources += ['android/support/v7/recyclerview/R.java']
+
 resjar.javac_flags += ['-Xlint:all']
 
 mgjar = add_java_jar('gecko-mozglue')
 mgjar.sources += [
     'mozglue/ByteBufferInputStream.java',
     'mozglue/ContextUtils.java',
     'mozglue/DirectBufferAllocator.java',
     'mozglue/GeckoLoader.java',
@@ -611,16 +613,17 @@ moz_native_devices_sources = [
     'ChromeCast.java',
     'GeckoMediaPlayer.java',
     'MediaPlayerManager.java',
     'PresentationMediaPlayerManager.java',
 ]
 if CONFIG['MOZ_NATIVE_DEVICES']:
     gbjar.extra_jars += moz_native_devices_jars
     gbjar.sources += moz_native_devices_sources
+gbjar.extra_jars += [CONFIG['ANDROID_RECYCLERVIEW_LIB']]
 
 gbjar.javac_flags += ['-Xlint:all,-deprecation,-fallthrough', '-J-Xmx512m', '-J-Xms128m']
 
 # gecko-thirdparty is a good place to put small independent libraries
 gtjar = add_java_jar('gecko-thirdparty')
 gtjar.sources += [ thirdparty_source_dir + f for f in [
     'com/nineoldandroids/animation/Animator.java',
     'com/nineoldandroids/animation/AnimatorInflater.java',
--- a/mobile/android/gradle/base/build.gradle
+++ b/mobile/android/gradle/base/build.gradle
@@ -43,16 +43,17 @@ android {
                 }
             }
         }
     }
 }
 
 dependencies {
     compile 'com.android.support:support-v4:22.2.0'
+    compile 'com.android.support:recyclerview-v7:22.2.0'
 
     if (mozconfig.substs.MOZ_NATIVE_DEVICES) {
         compile 'com.android.support:appcompat-v7:22.2.0'
         compile 'com.android.support:mediarouter-v7:22.2.0'
         compile 'com.google.android.gms:play-services-base:6.5.+'
         compile 'com.google.android.gms:play-services-cast:6.5.+'
     }
 
--- a/mobile/android/tests/browser/robocop/robocop.ini
+++ b/mobile/android/tests/browser/robocop/robocop.ini
@@ -94,18 +94,18 @@ skip-if = android_version == "10" || and
 skip-if = android_version == "10" || android_version == "18"
 [testSessionOOMSave.java]
 # disabled on 2.3, bug 945395; on 4.3, bug 1144888
 skip-if = android_version == "10" || android_version == "18"
 [testSessionOOMRestore.java]
 # disabled on Android 2.3, bug 979600; on 4.3, bug 1145879
 skip-if = android_version == "10" || android_version == "18"
 [testSettingsMenuItems.java]
-# disabled on 4.3, bug 1144898
-skip-if = android_version == "18"
+# disabled on Android 2.3, bug 979552; on 4.3, bug 1144898
+skip-if = android_version == "10" || android_version == "18"
 # [testShareLink.java] # see bug 915897
 [testSystemPages.java]
 # disabled on 2.3, bug 979603; on 4.3, bug 1142811
 skip-if = android_version == "10" || android_version == "18"
 # [testThumbnails.java] # see bug 813107
 [testTitleBar.java]
 # disabled on Android 2.3, bug 979552; on 4.3, bug 1145881
 skip-if = android_version == "10" || android_version == "18"
--- a/mobile/android/tests/browser/robocop/testSettingsMenuItems.java
+++ b/mobile/android/tests/browser/robocop/testSettingsMenuItems.java
@@ -139,17 +139,17 @@ public class testSettingsMenuItems exten
         // Set special handling for Settings items that are conditionally built.
         updateConditionalSettings(settingsMenuItems);
 
         selectMenuItem(mStringHelper.SETTINGS_LABEL);
         mAsserter.ok(mSolo.waitForText(mStringHelper.SETTINGS_LABEL),
                 "The Settings menu did not load", mStringHelper.SETTINGS_LABEL);
 
         // Dismiss the Settings screen and verify that the view is returned to about:home page
-        mSolo.goBack();
+        mActions.sendSpecialKey(Actions.SpecialKey.BACK);
 
         // Waiting for page title to appear to be sure that is fully loaded before opening the menu
         mAsserter.ok(mSolo.waitForText(mStringHelper.TITLE_PLACE_HOLDER), "about:home did not load",
                 mStringHelper.TITLE_PLACE_HOLDER);
         verifyUrl(mStringHelper.ABOUT_HOME_URL);
 
         selectMenuItem(mStringHelper.SETTINGS_LABEL);
         mAsserter.ok(mSolo.waitForText(mStringHelper.SETTINGS_LABEL),
@@ -276,24 +276,26 @@ public class testSettingsMenuItems exten
                                      "The " + itemChoice + " choice is present in section " + section);
                     }
 
                     // Leave submenu after checking.
                     if (waitForText("^Cancel$")) {
                         mSolo.clickOnText("^Cancel$");
                     } else {
                         // Some submenus aren't dialogs, but are nested screens; exit using "back".
-                        mSolo.goBack();
+                        mActions.sendSpecialKey(Actions.SpecialKey.BACK);
                     }
                 }
             }
 
             // Navigate back if on a phone. Tablets shouldn't do this because they use headers and fragments.
             if (mDevice.type.equals("phone")) {
                 int menuDepth = menuPath.length;
                 while (menuDepth > 0) {
-                    mSolo.goBack();
+                    mActions.sendSpecialKey(Actions.SpecialKey.BACK);
                     menuDepth--;
+                    // Sleep so subsequent back actions aren't lost.
+                    mSolo.sleep(150);
                 }
             }
         }
     }
 }
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -106,22 +106,16 @@ AccountState.prototype = {
       this.whenKeysReadyDeferred = null;
     }
 
     this.cert = null;
     this.keyPair = null;
     this.signedInUser = null;
     this.uid = null;
     this.fxaInternal = null;
-    this.initProfilePromise = null;
-
-    if (this.profile) {
-      this.profile.tearDown();
-      this.profile = null;
-    }
   },
 
   // Clobber all cached data and write that empty data to storage.
   signOut() {
     this.cert = null;
     this.keyPair = null;
     this.signedInUser = null;
     this.oauthTokens = {};
@@ -289,51 +283,16 @@ AccountState.prototype = {
       };
       log.debug("got keyPair");
       delete this.cert;
       d.resolve(this.keyPair.keyPair);
     });
     return d.promise.then(result => this.resolve(result));
   },
 
-  // Get the account's profile image URL from the profile server
-  getProfile: function () {
-    return this.initProfile()
-      .then(() => this.profile.getProfile());
-  },
-
-  // Instantiate a FxAccountsProfile with a fresh OAuth token if needed
-  initProfile: function () {
-
-    let profileServerUrl = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.profile.uri");
-
-    let oAuthOptions = {
-      scope: "profile"
-    };
-
-    if (this.initProfilePromise) {
-      return this.initProfilePromise;
-    }
-
-    this.initProfilePromise = this.fxaInternal.getOAuthToken(oAuthOptions)
-      .then(token => {
-        this.profile = new FxAccountsProfile(this, {
-          profileServerUrl: profileServerUrl,
-          token: token
-        });
-        this.initProfilePromise = null;
-      })
-      .then(null, err => {
-        this.initProfilePromise = null;
-        throw err;
-      });
-
-    return this.initProfilePromise;
-  },
-
   resolve: function(result) {
     if (!this.isCurrent) {
       log.info("An accountState promise was resolved, but was actually rejected" +
                " due to a different user being signed in. Originally resolved" +
                " with: " + result);
       return Promise.reject(new Error("A different user signed in"));
     }
     return Promise.resolve(result);
@@ -589,16 +548,29 @@ FxAccountsInternal.prototype = {
 
   get fxAccountsClient() {
     if (!this._fxAccountsClient) {
       this._fxAccountsClient = new FxAccountsClient();
     }
     return this._fxAccountsClient;
   },
 
+  // The profile object used to fetch the actual user profile.
+  _profile: null,
+  get profile() {
+    if (!this._profile) {
+      let profileServerUrl = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.profile.uri");
+      this._profile = new FxAccountsProfile({
+        fxa: this,
+        profileServerUrl: profileServerUrl,
+      });
+    }
+    return this._profile;
+  },
+
   /**
    * Return the current time in milliseconds as an integer.  Allows tests to
    * manipulate the date to simulate certificate expiration.
    */
   now: function() {
     return this.fxAccountsClient.now();
   },
 
@@ -844,16 +816,20 @@ FxAccountsInternal.prototype = {
   },
 
   /**
    * This function should be called in conjunction with a server-side
    * signOut via FxAccountsClient.
    */
   _signOutLocal: function signOutLocal() {
     let currentAccountState = this.currentAccountState;
+    if (this._profile) {
+      this._profile.tearDown();
+      this._profile = null;
+    }
     return currentAccountState.signOut().then(() => {
       this.abortExistingFlow(); // this resets this.currentAccountState.
     });
   },
 
   _signOutServer: function signOutServer(sessionToken) {
     return this.fxAccountsClient.signOut(sessionToken);
   },
@@ -1425,27 +1401,27 @@ FxAccountsInternal.prototype = {
    *          INVALID_PARAMETER
    *          NO_ACCOUNT
    *          UNVERIFIED_ACCOUNT
    *          NETWORK_ERROR
    *          AUTH_ERROR
    *          UNKNOWN_ERROR
    */
   getSignedInUserProfile: function () {
-    let accountState = this.currentAccountState;
-    return accountState.getProfile()
-      .then((profileData) => {
+    let currentState = this.currentAccountState;
+    return this.profile.getProfile().then(
+      profileData => {
         let profile = JSON.parse(JSON.stringify(profileData));
-        return accountState.resolve(profile);
+        return currentState.resolve(profile);
       },
-      (error) => {
+      error => {
         log.error("Could not retrieve profile data", error);
-        return accountState.reject(error);
-      })
-      .then(null, err => Promise.reject(this._errorToErrorClass(err)));
+        return currentState.reject(error);
+      }
+    ).catch(err => Promise.reject(this._errorToErrorClass(err)));
   },
 };
 
 /**
  * JSONStorage constructor that creates instances that may set/get
  * to a specified file, in a directory that will be created if it
  * doesn't exist.
  *
--- a/services/fxaccounts/FxAccountsProfile.jsm
+++ b/services/fxaccounts/FxAccountsProfile.jsm
@@ -12,19 +12,19 @@
  * the user's profile in open browser tabs, and cacheing/invalidating profile data.
  */
 
 this.EXPORTED_SYMBOLS = ["FxAccountsProfile"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://gre/modules/FxAccounts.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileClient",
   "resource://gre/modules/FxAccountsProfileClient.jsm");
 
 // Based off of deepEqual from Assert.jsm
 function deepEqual(actual, expected) {
   if (actual === expected) {
     return true;
@@ -66,61 +66,61 @@ function objEquiv(a, b) {
   }
   return true;
 }
 
 function hasChanged(oldData, newData) {
   return !deepEqual(oldData, newData);
 }
 
-this.FxAccountsProfile = function (accountState, options = {}) {
-  this.currentAccountState = accountState;
+this.FxAccountsProfile = function (options = {}) {
+  this._cachedProfile = null;
+  this.fxa = options.fxa || fxAccounts;
   this.client = options.profileClient || new FxAccountsProfileClient({
+    fxa: this.fxa,
     serverURL: options.profileServerUrl,
-    token: options.token
   });
 
   // for testing
   if (options.channel) {
     this.channel = options.channel;
   }
 }
 
 this.FxAccountsProfile.prototype = {
 
   tearDown: function () {
-    this.currentAccountState = null;
+    this.fxa = null;
     this.client = null;
+    this._cachedProfile = null;
   },
 
   _getCachedProfile: function () {
-    let currentState = this.currentAccountState;
-    return currentState.getUserAccountData()
-      .then(cachedData => cachedData.profile);
+    // The cached profile will end up back in the generic accountData
+    // once bug 1157529 is fixed.
+    return Promise.resolve(this._cachedProfile);
   },
 
   _notifyProfileChange: function (uid) {
     Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid);
   },
 
   // Cache fetched data if it is different from what's in the cache.
   // Send out a notification if it has changed so that UI can update.
   _cacheProfile: function (profileData) {
-    let currentState = this.currentAccountState;
-    if (!currentState) {
-      return;
+    if (!hasChanged(this._cachedProfile, profileData)) {
+      log.debug("fetched profile matches cached copy");
+      return Promise.resolve(null); // indicates no change (but only tests care)
     }
-    return currentState.getUserAccountData()
-      .then(data => {
-        if (!hasChanged(data.profile, profileData)) {
-          return;
-        }
-        data.profile = profileData;
-        return currentState.setUserAccountData(data)
-          .then(() => this._notifyProfileChange(data.uid));
+    this._cachedProfile = profileData;
+    return this.fxa.getSignedInUser()
+      .then(userData => {
+        log.debug("notifying profile changed for user ${uid}", userData);
+        this._notifyProfileChange(userData.uid);
+        return profileData;
       });
   },
 
   _fetchAndCacheProfile: function () {
     return this.client.fetchProfile()
       .then(profile => {
         return this._cacheProfile(profile).then(() => profile);
       });
@@ -128,17 +128,21 @@ this.FxAccountsProfile.prototype = {
 
   // Returns cached data right away if available, then fetches the latest profile
   // data in the background. After data is fetched a notification will be sent
   // out if the profile has changed.
   getProfile: function () {
     return this._getCachedProfile()
       .then(cachedProfile => {
         if (cachedProfile) {
-          this._fetchAndCacheProfile();
+          // Note that _fetchAndCacheProfile isn't returned, so continues
+          // in the background.
+          this._fetchAndCacheProfile().catch(err => {
+            log.error("Background refresh of profile failed", err);
+          });
           return cachedProfile;
         }
         return this._fetchAndCacheProfile();
       })
       .then(profile => {
         return profile;
       });
   },
--- a/services/fxaccounts/FxAccountsProfileClient.jsm
+++ b/services/fxaccounts/FxAccountsProfileClient.jsm
@@ -1,89 +1,139 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /**
  * A client to fetch profile information for a Firefox Account.
  */
+ "use strict;"
 
 this.EXPORTED_SYMBOLS = ["FxAccountsProfileClient", "FxAccountsProfileClientError"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://services-common/rest.js");
 
 Cu.importGlobalProperties(["URL"]);
 
 /**
  * Create a new FxAccountsProfileClient to be able to fetch Firefox Account profile information.
  *
  * @param {Object} options Options
  *   @param {String} options.serverURL
  *   The URL of the profile server to query.
  *   Example: https://profile.accounts.firefox.com/v1
  *   @param {String} options.token
  *   The bearer token to access the profile server
  * @constructor
  */
 this.FxAccountsProfileClient = function(options) {
-  if (!options || !options.serverURL || !options.token) {
-    throw new Error("Missing 'serverURL' or 'token' configuration option");
+  if (!options || !options.serverURL) {
+    throw new Error("Missing 'serverURL' configuration option");
   }
 
+  this.fxa = options.fxa || fxAccounts;
+  // This is a work-around for loop that manages its own oauth tokens.
+  // * If |token| is in options we use it and don't attempt any token refresh
+  //  on 401. This is for loop.
+  // * If |token| doesn't exist we will fetch our own token. This is for the
+  //   normal FxAccounts methods for obtaining the profile.
+  // We should nuke all |this.token| support once loop moves closer to FxAccounts.
+  this.token = options.token;
+
   try {
     this.serverURL = new URL(options.serverURL);
   } catch (e) {
     throw new Error("Invalid 'serverURL'");
   }
-  this.token = options.token;
+  this.oauthOptions = {
+    scope: "profile",
+  };
   log.debug("FxAccountsProfileClient: Initialized");
 };
 
 this.FxAccountsProfileClient.prototype = {
   /**
    * {nsIURI}
    * The server to fetch profile information from.
    */
   serverURL: null,
 
   /**
-   * {String}
-   * Profile server bearer OAuth token.
-   */
-  token: null,
-
-  /**
    * Interface for making remote requests.
    */
   _Request: RESTRequest,
 
   /**
-   * Remote request helper
+   * Remote request helper which abstracts authentication away.
    *
    * @param {String} path
    *        Profile server path, i.e "/profile".
    * @param {String} [method]
    *        Type of request, i.e "GET".
    * @return Promise
    *         Resolves: {Object} Successful response from the Profile server.
    *         Rejects: {FxAccountsProfileClientError} Profile client error.
    * @private
    */
-  _createRequest: function(path, method = "GET") {
+  _createRequest: Task.async(function* (path, method = "GET") {
+    let token = this.token;
+    if (!token) {
+      // tokens are cached, so getting them each request is cheap.
+      token = yield this.fxa.getOAuthToken(this.oauthOptions);
+    }
+    try {
+      return (yield this._rawRequest(path, method, token));
+    } catch (ex if ex instanceof FxAccountsProfileClientError && ex.code == 401) {
+      // If this object was instantiated with a token then we don't refresh it.
+      if (this.token) {
+        throw ex;
+      }
+      // it's an auth error - assume our token expired and retry.
+      log.info("Fetching the profile returned a 401 - revoking our token and retrying");
+      yield this.fxa.removeCachedOAuthToken({token});
+      token = yield this.fxa.getOAuthToken(this.oauthOptions);
+      // and try with the new token - if that also fails then we fail after
+      // revoking the token.
+      try {
+        return (yield this._rawRequest(path, method, token));
+      } catch (ex if ex instanceof FxAccountsProfileClientError && ex.code == 401) {
+        log.info("Retry fetching the profile still returned a 401 - revoking our token and failing");
+        yield this.fxa.removeCachedOAuthToken({token});
+        throw ex;
+      }
+    }
+  }),
+
+  /**
+   * Remote "raw" request helper - doesn't handle auth errors and tokens.
+   *
+   * @param {String} path
+   *        Profile server path, i.e "/profile".
+   * @param {String} method
+   *        Type of request, i.e "GET".
+   * @param {String} token
+   * @return Promise
+   *         Resolves: {Object} Successful response from the Profile server.
+   *         Rejects: {FxAccountsProfileClientError} Profile client error.
+   * @private
+   */
+  _rawRequest: function(path, method, token) {
     return new Promise((resolve, reject) => {
       let profileDataUrl = this.serverURL + path;
       let request = new this._Request(profileDataUrl);
       method = method.toUpperCase();
 
-      request.setHeader("Authorization", "Bearer " + this.token);
+      request.setHeader("Authorization", "Bearer " + token);
       request.setHeader("Accept", "application/json");
 
       request.onComplete = function (error) {
         if (error) {
           return reject(new FxAccountsProfileClientError({
             error: ERROR_NETWORK,
             errno: ERRNO_NETWORK,
             message: error.toString(),
@@ -101,17 +151,22 @@ this.FxAccountsProfileClient.prototype =
             message: request.response.body,
           }));
         }
 
         // "response.success" means status code is 200
         if (request.response.success) {
           return resolve(body);
         } else {
-          return reject(new FxAccountsProfileClientError(body));
+          return reject(new FxAccountsProfileClientError({
+            error: body.error || ERROR_UNKNOWN,
+            errno: body.errno || ERRNO_UNKNOWN_ERROR,
+            code: request.response.status,
+            message: body.message || body,
+          }));
         }
       };
 
       if (method === "GET") {
         request.get();
       } else {
         // method not supported
         return reject(new FxAccountsProfileClientError({
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -955,114 +955,75 @@ add_test(function test_getOAuthToken_unk
     fxa.getOAuthToken({ scope: "profile", client: client })
       .then(null, err => {
          do_check_eq(err.message, "UNKNOWN_ERROR");
          run_next_test();
       });
   });
 });
 
-add_test(function test_accountState_initProfile() {
-  let fxa = new MockFxAccounts();
-  let alice = getTestUser("alice");
-  alice.verified = true;
-
-  fxa.internal.getOAuthToken = function (opts) {
-    return Promise.resolve("token");
-  };
-
-  fxa.setSignedInUser(alice).then(() => {
-    let accountState = fxa.internal.currentAccountState;
-
-    accountState.initProfile(options)
-      .then(result => {
-         do_check_true(!!accountState.profile);
-         run_next_test();
-      });
-  });
-
-});
-
-add_test(function test_accountState_getProfile() {
-  let fxa = new MockFxAccounts();
+add_test(function test_getSignedInUserProfile() {
   let alice = getTestUser("alice");
   alice.verified = true;
 
   let mockProfile = {
     getProfile: function () {
       return Promise.resolve({ avatar: "image" });
     }
   };
+  let fxa = new FxAccounts({
+    _profile: mockProfile,
+  });
 
   fxa.setSignedInUser(alice).then(() => {
-    let accountState = fxa.internal.currentAccountState;
-    accountState.profile = mockProfile;
-    accountState.initProfilePromise = new Promise((resolve, reject) => resolve(mockProfile));
-
-    accountState.getProfile()
+    fxa.getSignedInUserProfile()
       .then(result => {
          do_check_true(!!result);
          do_check_eq(result.avatar, "image");
          run_next_test();
       });
   });
-
-});
-
-add_test(function test_getSignedInUserProfile_ok() {
-  let fxa = new MockFxAccounts();
-  let alice = getTestUser("alice");
-  alice.verified = true;
-
-  fxa.setSignedInUser(alice).then(() => {
-    let accountState = fxa.internal.currentAccountState;
-    accountState.getProfile = function () {
-      return Promise.resolve({ avatar: "image" });
-    };
-
-    fxa.getSignedInUserProfile()
-      .then(result => {
-         do_check_eq(result.avatar, "image");
-         run_next_test();
-      });
-  });
-
 });
 
 add_test(function test_getSignedInUserProfile_error_uses_account_data() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   alice.verified = true;
 
   fxa.internal.getSignedInUser = function () {
     return Promise.resolve({ email: "foo@bar.com" });
   };
 
+  let teardownCalled = false;
   fxa.setSignedInUser(alice).then(() => {
-    let accountState = fxa.internal.currentAccountState;
-    accountState.getProfile = function () {
-      return Promise.reject("boom");
+    fxa.internal._profile = {
+      getProfile: function () {
+        return Promise.reject("boom");
+      },
+      tearDown: function() {
+        teardownCalled = true;
+      }
     };
 
     fxa.getSignedInUserProfile()
       .catch(error => {
-         do_check_eq(error.message, "UNKNOWN_ERROR");
-         fxa.signOut().then(run_next_test);
+        do_check_eq(error.message, "UNKNOWN_ERROR");
+        fxa.signOut().then(() => {
+          do_check_true(teardownCalled);
+          run_next_test();
+        });
       });
   });
-
 });
 
 add_test(function test_getSignedInUserProfile_unverified_account() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
 
   fxa.setSignedInUser(alice).then(() => {
-    let accountState = fxa.internal.currentAccountState;
-
     fxa.getSignedInUserProfile()
       .catch(error => {
          do_check_eq(error.message, "UNVERIFIED_ACCOUNT");
          fxa.signOut().then(run_next_test);
       });
   });
 
 });
--- a/services/fxaccounts/tests/xpcshell/test_profile.js
+++ b/services/fxaccounts/tests/xpcshell/test_profile.js
@@ -6,22 +6,16 @@
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 Cu.import("resource://gre/modules/FxAccountsProfileClient.jsm");
 Cu.import("resource://gre/modules/FxAccountsProfile.jsm");
 
 const URL_STRING = "https://example.com";
 Services.prefs.setCharPref("identity.fxaccounts.settings.uri", "https://example.com/settings");
 
-const PROFILE_CLIENT_OPTIONS = {
-  token: "123ABC",
-  serverURL: "http://127.0.0.1:1111/v1",
-  profileServerUrl: "http://127.0.0.1:1111/v1"
-};
-
 const STATUS_SUCCESS = 200;
 
 /**
  * Mock request responder
  * @param {String} response
  *        Mocked raw response from the server
  * @returns {Function}
  */
@@ -53,152 +47,166 @@ let mockResponseError = function (error)
       setHeader: function () {},
       head: function () {
         this.onComplete(error);
       }
     };
   };
 };
 
-let mockClient = function () {
-  let client = new FxAccountsProfileClient(PROFILE_CLIENT_OPTIONS);
-  return client;
+let mockClient = function (fxa) {
+  let options = {
+    serverURL: "http://127.0.0.1:1111/v1",
+    fxa: fxa,
+  }
+  return new FxAccountsProfileClient(options);
 };
 
 const ACCOUNT_DATA = {
   uid: "abc123"
 };
 
-function AccountData () {
+function FxaMock() {
 }
-AccountData.prototype = {
-  getUserAccountData: function () {
+FxaMock.prototype = {
+  currentAccountState: {
+    profile: null,
+    get isCurrent() true,
+  },
+
+  getSignedInUser: function () {
     return Promise.resolve(ACCOUNT_DATA);
   }
 };
 
-let mockAccountData = function () {
-  return new AccountData();
+let mockFxa = function() {
+  return new FxaMock();
 };
 
+function CreateFxAccountsProfile(fxa = null, client = null) {
+  if (!fxa) {
+    fxa = mockFxa();
+  }
+  let options = {
+    fxa: fxa,
+    profileServerUrl: "http://127.0.0.1:1111/v1"
+  }
+  if (client) {
+    options.profileClient = client;
+  }
+  return new FxAccountsProfile(options);
+}
+
 add_test(function getCachedProfile() {
-  let accountData = mockAccountData();
-  accountData.getUserAccountData = function () {
-    return Promise.resolve({
-      profile: { avatar: "myurl" }
-    });
-  };
-  let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS);
+  let profile = CreateFxAccountsProfile();
+  // a little pointless until bug 1157529 is fixed...
+  profile._cachedProfile = { avatar: "myurl" };
 
   return profile._getCachedProfile()
     .then(function (cached) {
       do_check_eq(cached.avatar, "myurl");
       run_next_test();
     });
 });
 
 add_test(function cacheProfile_change() {
-  let accountData = mockAccountData();
+  let fxa = mockFxa();
+/* Saving profile data disabled - bug 1157529
   let setUserAccountDataCalled = false;
-  accountData.setUserAccountData = function (data) {
+  fxa.setUserAccountData = function (data) {
     setUserAccountDataCalled = true;
     do_check_eq(data.profile.avatar, "myurl");
     return Promise.resolve();
   };
-  let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS);
+*/
+  let profile = CreateFxAccountsProfile(fxa);
 
   makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
     do_check_eq(data, ACCOUNT_DATA.uid);
-    do_check_true(setUserAccountDataCalled);
+//    do_check_true(setUserAccountDataCalled); - bug 1157529
     run_next_test();
   });
 
   return profile._cacheProfile({ avatar: "myurl" });
 });
 
 add_test(function cacheProfile_no_change() {
-  let accountData = mockAccountData();
-  accountData.getUserAccountData = function () {
-    return Promise.resolve({
-      profile: { avatar: "myurl" }
-    });
-  };
-  accountData.setUserAccountData = function (data) {
+  let fxa = mockFxa();
+  let profile = CreateFxAccountsProfile(fxa)
+  profile._cachedProfile = { avatar: "myurl" };
+// XXX - saving is disabled (but we can leave that in for now as we are
+// just checking it is *not* called)
+  fxa.setSignedInUser = function (data) {
     throw new Error("should not update account data");
   };
-  let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS);
 
   return profile._cacheProfile({ avatar: "myurl" })
     .then((result) => {
       do_check_false(!!result);
       run_next_test();
     });
 });
 
 add_test(function fetchAndCacheProfile_ok() {
-  let client = mockClient();
+  let client = mockClient(mockFxa());
   client.fetchProfile = function () {
     return Promise.resolve({ avatar: "myimg"});
   };
-  let profile = new FxAccountsProfile(mockAccountData(), {
-    profileClient: client
-  });
+  let profile = CreateFxAccountsProfile(null, client);
 
   profile._cacheProfile = function (toCache) {
     do_check_eq(toCache.avatar, "myimg");
     return Promise.resolve();
   };
 
   return profile._fetchAndCacheProfile()
     .then(result => {
       do_check_eq(result.avatar, "myimg");
       run_next_test();
     });
 });
 
 add_test(function tearDown_ok() {
-  let profile = new FxAccountsProfile(mockAccountData(), PROFILE_CLIENT_OPTIONS);
+  let profile = CreateFxAccountsProfile();
 
   do_check_true(!!profile.client);
-  do_check_true(!!profile.currentAccountState);
+  do_check_true(!!profile.fxa);
 
   profile.tearDown();
-  do_check_null(profile.currentAccountState);
+  do_check_null(profile.fxa);
   do_check_null(profile.client);
 
   run_next_test();
 });
 
 add_test(function getProfile_ok() {
   let cachedUrl = "myurl";
-  let accountData = mockAccountData();
   let didFetch = false;
 
-  let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS);
+  let profile = CreateFxAccountsProfile();
   profile._getCachedProfile = function () {
     return Promise.resolve({ avatar: cachedUrl });
   };
 
   profile._fetchAndCacheProfile = function () {
     didFetch = true;
+    return Promise.resolve();
   };
 
   return profile.getProfile()
     .then(result => {
       do_check_eq(result.avatar, cachedUrl);
       do_check_true(didFetch);
       run_next_test();
     });
 });
 
 add_test(function getProfile_no_cache() {
   let fetchedUrl = "newUrl";
-  let accountData = mockAccountData();
-
-  let profile = new FxAccountsProfile(accountData, PROFILE_CLIENT_OPTIONS);
+  let profile = CreateFxAccountsProfile();
   profile._getCachedProfile = function () {
     return Promise.resolve();
   };
 
   profile._fetchAndCacheProfile = function () {
     return Promise.resolve({ avatar: fetchedUrl });
   };
 
@@ -207,33 +215,33 @@ add_test(function getProfile_no_cache() 
       do_check_eq(result.avatar, fetchedUrl);
       run_next_test();
     });
 });
 
 add_test(function getProfile_has_cached_fetch_deleted() {
   let cachedUrl = "myurl";
 
-  let client = mockClient();
+  let fxa = mockFxa();
+  let client = mockClient(fxa);
   client.fetchProfile = function () {
     return Promise.resolve({ avatar: null });
   };
 
-  let accountData = mockAccountData();
-  accountData.getUserAccountData = function () {
-    return Promise.resolve({ profile: { avatar: cachedUrl } });
-  };
-  accountData.setUserAccountData = function (data) {
-    do_check_null(data.profile.avatar);
-    run_next_test();
-    return Promise.resolve();
-  };
+  let profile = CreateFxAccountsProfile(fxa, client);
+  profile._cachedProfile = { avatar: cachedUrl };
 
-  let profile = new FxAccountsProfile(accountData, {
-    profileClient: client
+// instead of checking this in a mocked "save" function, just check after the
+// observer
+  makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
+    profile.getProfile()
+      .then(profileData => {
+        do_check_null(profileData.avatar);
+        run_next_test();
+      });
   });
 
   return profile.getProfile()
     .then(result => {
       do_check_eq(result.avatar, "myurl");
     });
 });
 
--- a/services/fxaccounts/tests/xpcshell/test_profile_client.js
+++ b/services/fxaccounts/tests/xpcshell/test_profile_client.js
@@ -1,21 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 Cu.import("resource://gre/modules/FxAccountsProfileClient.jsm");
 
-const PROFILE_OPTIONS = {
-  token: "123ABC",
-  serverURL: "http://127.0.0.1:1111/v1",
-};
-
 const STATUS_SUCCESS = 200;
 
 /**
  * Mock request responder
  * @param {String} response
  *        Mocked raw response from the server
  * @returns {Function}
  */
@@ -30,16 +25,31 @@ let mockResponse = function (response) {
         this.onComplete();
       }
     };
   };
 
   return Request;
 };
 
+// A simple mock FxA that hands out tokens without checking them and doesn't
+// expect tokens to be revoked. We have specific token tests further down that
+// has more checks here.
+let mockFxa = {
+  getOAuthToken(options) {
+    do_check_eq(options.scope, "profile");
+    return "token";
+  }
+}
+
+const PROFILE_OPTIONS = {
+  serverURL: "http://127.0.0.1:1111/v1",
+  fxa: mockFxa,
+};
+
 /**
  * Mock request error responder
  * @param {Error} error
  *        Error object
  * @returns {Function}
  */
 let mockResponseError = function (error) {
   return function () {
@@ -93,39 +103,178 @@ add_test(function parseErrorResponse () 
         run_next_test();
       }
     );
 });
 
 add_test(function serverErrorResponse () {
   let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
   let response = {
-    status: 401,
-    body: "{ \"code\": 401, \"errno\": 100, \"error\": \"Bad Request\", \"message\": \"Unauthorized\", \"reason\": \"Bearer token not provided\" }",
+    status: 500,
+    body: "{ \"code\": 500, \"errno\": 100, \"error\": \"Bad Request\", \"message\": \"Something went wrong\", \"reason\": \"Because the internet\" }",
   };
 
   client._Request = new mockResponse(response);
   client.fetchProfile()
     .then(
     null,
     function (e) {
       do_check_eq(e.name, "FxAccountsProfileClientError");
+      do_check_eq(e.code, 500);
+      do_check_eq(e.errno, 100);
+      do_check_eq(e.error, "Bad Request");
+      do_check_eq(e.message, "Something went wrong");
+      run_next_test();
+    }
+  );
+});
+
+// Test that we get a token, then if we get a 401 we revoke it, get a new one
+// and retry.
+add_test(function server401ResponseThenSuccess () {
+  // The last token we handed out.
+  let lastToken = -1;
+  // The number of times our removeCachedOAuthToken function was called.
+  let numTokensRemoved = 0;
+
+  let mockFxa = {
+    getOAuthToken(options) {
+      do_check_eq(options.scope, "profile");
+      return "" + ++lastToken; // tokens are strings.
+    },
+    removeCachedOAuthToken(options) {
+      // This test never has more than 1 token alive at once, so the token
+      // being revoked must always be the last token we handed out.
+      do_check_eq(parseInt(options.token), lastToken);
+      ++numTokensRemoved;
+    }
+  }
+  let profileOptions = {
+    serverURL: "http://127.0.0.1:1111/v1",
+    fxa: mockFxa,
+  };
+  let client = new FxAccountsProfileClient(profileOptions);
+
+  // 2 responses - first one implying the token has expired, second works.
+  let responses = [
+    {
+      status: 401,
+      body: "{ \"code\": 401, \"errno\": 100, \"error\": \"Token expired\", \"message\": \"That token is too old\", \"reason\": \"Because security\" }",
+    },
+    {
+      success: true,
+      status: STATUS_SUCCESS,
+      body: "{\"avatar\":\"http://example.com/image.jpg\",\"id\":\"0d5c1a89b8c54580b8e3e8adadae864a\"}",
+    },
+  ];
+
+  let numRequests = 0;
+  let numAuthHeaders = 0;
+  // Like mockResponse but we want access to headers etc.
+  client._Request = function(requestUri) {
+    return {
+      setHeader: function (name, value) {
+        if (name == "Authorization") {
+          numAuthHeaders++;
+          do_check_eq(value, "Bearer " + lastToken);
+        }
+      },
+      get: function () {
+        this.response = responses[numRequests];
+        ++numRequests;
+        this.onComplete();
+      }
+    };
+  }
+
+  client.fetchProfile()
+    .then(result => {
+      do_check_eq(result.avatar, "http://example.com/image.jpg");
+      do_check_eq(result.id, "0d5c1a89b8c54580b8e3e8adadae864a");
+      // should have been exactly 2 requests and exactly 2 auth headers.
+      do_check_eq(numRequests, 2);
+      do_check_eq(numAuthHeaders, 2);
+      // and we should have seen one token revoked.
+      do_check_eq(numTokensRemoved, 1);
+
+      run_next_test();
+    }
+  );
+});
+
+// Test that we get a token, then if we get a 401 we revoke it, get a new one
+// and retry - but we *still* get a 401 on the retry, so the caller sees that.
+add_test(function server401ResponsePersists () {
+  // The last token we handed out.
+  let lastToken = -1;
+  // The number of times our removeCachedOAuthToken function was called.
+  let numTokensRemoved = 0;
+
+  let mockFxa = {
+    getOAuthToken(options) {
+      do_check_eq(options.scope, "profile");
+      return "" + ++lastToken; // tokens are strings.
+    },
+    removeCachedOAuthToken(options) {
+      // This test never has more than 1 token alive at once, so the token
+      // being revoked must always be the last token we handed out.
+      do_check_eq(parseInt(options.token), lastToken);
+      ++numTokensRemoved;
+    }
+  }
+  let profileOptions = {
+    serverURL: "http://127.0.0.1:1111/v1",
+    fxa: mockFxa,
+  };
+  let client = new FxAccountsProfileClient(profileOptions);
+
+  let response = {
+      status: 401,
+      body: "{ \"code\": 401, \"errno\": 100, \"error\": \"It's not your token, it's you!\", \"message\": \"I don't like you\", \"reason\": \"Because security\" }",
+  };
+
+  let numRequests = 0;
+  let numAuthHeaders = 0;
+  client._Request = function(requestUri) {
+    return {
+      setHeader: function (name, value) {
+        if (name == "Authorization") {
+          numAuthHeaders++;
+          do_check_eq(value, "Bearer " + lastToken);
+        }
+      },
+      get: function () {
+        this.response = response;
+        ++numRequests;
+        this.onComplete();
+      }
+    };
+  }
+
+  client.fetchProfile().then(
+    null,
+    function (e) {
+      do_check_eq(e.name, "FxAccountsProfileClientError");
       do_check_eq(e.code, 401);
       do_check_eq(e.errno, 100);
-      do_check_eq(e.error, "Bad Request");
-      do_check_eq(e.message, "Unauthorized");
+      do_check_eq(e.error, "It's not your token, it's you!");
+      // should have been exactly 2 requests and exactly 2 auth headers.
+      do_check_eq(numRequests, 2);
+      do_check_eq(numAuthHeaders, 2);
+      // and we should have seen both tokens revoked.
+      do_check_eq(numTokensRemoved, 2);
       run_next_test();
     }
   );
 });
 
 add_test(function networkErrorResponse () {
   let client = new FxAccountsProfileClient({
-    token: "123ABC",
-    serverURL: "http://"
+    serverURL: "http://",
+    fxa: mockFxa,
   });
   client.fetchProfile()
     .then(
       null,
       function (e) {
         do_check_eq(e.name, "FxAccountsProfileClientError");
         do_check_eq(e.code, null);
         do_check_eq(e.errno, ERRNO_NETWORK);
@@ -186,28 +335,22 @@ add_test(function fetchProfileImage_succ
         do_check_eq(result.id, "0d5c1a89b8c54580b8e3e8adadae864a");
         run_next_test();
       }
     );
 });
 
 add_test(function constructorTests() {
   validationHelper(undefined,
-    "Error: Missing 'serverURL' or 'token' configuration option");
+    "Error: Missing 'serverURL' configuration option");
 
   validationHelper({},
-    "Error: Missing 'serverURL' or 'token' configuration option");
-
-  validationHelper({ serverURL: "http://example.com" },
-    "Error: Missing 'serverURL' or 'token' configuration option");
+    "Error: Missing 'serverURL' configuration option");
 
-  validationHelper({ token: "123ABC" },
-    "Error: Missing 'serverURL' or 'token' configuration option");
-
-  validationHelper({ token: "123ABC", serverURL: "badUrl" },
+  validationHelper({ serverURL: "badUrl" },
     "Error: Invalid 'serverURL'");
 
   run_next_test();
 });
 
 add_test(function errorTests() {
   let error1 = new FxAccountsProfileClientError();
   do_check_eq(error1.name, "FxAccountsProfileClientError");
@@ -250,15 +393,19 @@ function run_test() {
  *
  * @param {Object} options
  *        FxAccountsProfileClient constructor options
  * @param {String} expected
  *        Expected error message
  * @returns {*}
  */
 function validationHelper(options, expected) {
+  // add fxa to options - that missing isn't what we are testing here.
+  if (options) {
+    options.fxa = mockFxa;
+  }
   try {
     new FxAccountsProfileClient(options);
   } catch (e) {
     return do_check_eq(e.toString(), expected);
   }
   throw new Error("Validation helper error");
 }
--- a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
+++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
@@ -877,16 +877,33 @@ LoginManagerPrompter.prototype = {
       passwordField.selectionStart = selectionStart;
       passwordField.selectionEnd = selectionEnd;
     };
 
     let onPasswordBlur = () => {
       chromeDoc.getElementById("password-notification-password").type = "password";
     };
 
+    let onNotificationClick = (clickEvent) => {
+      // Removes focus from textboxes when we click elsewhere on the doorhanger.
+      let focusedElement = Services.focus.focusedElement;
+      if (!focusedElement || focusedElement.nodeName != "html:input") {
+        // No input is focused so we don't need to blur
+        return;
+      }
+
+      let focusedBindingParent = chromeDoc.getBindingParent(focusedElement);
+      if (!focusedBindingParent || focusedBindingParent.nodeName != "textbox" ||
+          clickEvent.explicitOriginalTarget == focusedBindingParent) {
+        // The focus wasn't in a textbox or the click was in the focused textbox.
+        return;
+      }
+      focusedBindingParent.blur();
+    };
+
     let persistData = () => {
       let foundLogins = Services.logins.findLogins({}, login.hostname,
                                                    login.formSubmitURL,
                                                    login.httpRealm);
       let logins = foundLogins.filter(l => l.username == login.username);
       if (logins.length == 0) {
         // The original login we have been provided with might have its own
         // metadata, but we don't want it propagated to the newly created one.
@@ -961,23 +978,27 @@ LoginManagerPrompter.prototype = {
               chromeDoc.getElementById("password-notification-password")
                        .addEventListener("input", onInput);
               chromeDoc.getElementById("password-notification-password")
                        .addEventListener("focus", onPasswordFocus);
               chromeDoc.getElementById("password-notification-password")
                        .addEventListener("blur", onPasswordBlur);
               break;
             case "shown":
+              chromeDoc.getElementById("notification-popup")
+                         .addEventListener("click", onNotificationClick);
               writeDataToUI();
               break;
             case "dismissed":
               readDataFromUI();
               // Fall through.
             case "removed":
               currentNotification = null;
+              chromeDoc.getElementById("notification-popup")
+                       .removeEventListener("click", onNotificationClick);
               chromeDoc.getElementById("password-notification-username")
                        .removeEventListener("input", onInput);
               chromeDoc.getElementById("password-notification-password")
                        .removeEventListener("input", onInput);
               chromeDoc.getElementById("password-notification-password")
                        .removeEventListener("focus", onPasswordFocus);
               chromeDoc.getElementById("password-notification-password")
                        .removeEventListener("blur", onPasswordBlur);
--- a/toolkit/devtools/server/actors/highlighter.js
+++ b/toolkit/devtools/server/actors/highlighter.js
@@ -1,56 +1,64 @@
 /* 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/. */
+/* globals LayoutHelpers, DOMUtils, CssLogic, setIgnoreLayoutChanges */
 
 "use strict";
 
 const {Cu, Cc, Ci} = require("chrome");
-const Services = require("Services");
 const protocol = require("devtools/server/protocol");
 const {Arg, Option, method, RetVal} = protocol;
 const events = require("sdk/event/core");
 const Heritage = require("sdk/core/heritage");
-const {CssLogic} = require("devtools/styleinspector/css-logic");
 const EventEmitter = require("devtools/toolkit/event-emitter");
-const {setIgnoreLayoutChanges} = require("devtools/server/actors/layout");
-
-Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
+
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+loader.lazyRequireGetter(this, "CssLogic",
+  "devtools/styleinspector/css-logic", true);
+loader.lazyRequireGetter(this, "setIgnoreLayoutChanges",
+  "devtools/server/actors/layout", true);
+loader.lazyGetter(this, "DOMUtils", function() {
+  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+loader.lazyImporter(this, "LayoutHelpers",
+  "resource://gre/modules/devtools/LayoutHelpers.jsm");
 
 // FIXME: add ":visited" and ":link" after bug 713106 is fixed
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
+// Note that the order of items in this array is important because it is used
+// for drawing the BoxModelHighlighter's path elements correctly.
 const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"];
 const BOX_MODEL_SIDES = ["top", "right", "bottom", "left"];
 const SVG_NS = "http://www.w3.org/2000/svg";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-const HIGHLIGHTER_STYLESHEET_URI = "resource://gre/modules/devtools/server/actors/highlighter.css";
+const STYLESHEET_URI = "resource://gre/modules/devtools/server/actors/" +
+                       "highlighter.css";
 const HIGHLIGHTER_PICKED_TIMER = 1000;
-// How high is the nodeinfobar
-const NODE_INFOBAR_HEIGHT = 34; //px
-const NODE_INFOBAR_ARROW_SIZE = 9; // px
+// How high is the nodeinfobar (px).
+const NODE_INFOBAR_HEIGHT = 34;
+// What's the size of the nodeinfobar arrow (px).
+const NODE_INFOBAR_ARROW_SIZE = 9;
 // Width of boxmodelhighlighter guides
 const GUIDE_STROKE_WIDTH = 1;
 // The minimum distance a line should be before it has an arrow marker-end
 const ARROW_LINE_MIN_DISTANCE = 10;
 // How many maximum nodes can be highlighted at the same time by the
 // SelectorHighlighter
 const MAX_HIGHLIGHTED_ELEMENTS = 100;
 // SimpleOutlineHighlighter's stylesheet
 const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
 const SIMPLE_OUTLINE_SHEET = ".__fx-devtools-hide-shortcut__ {" +
                              "  visibility: hidden !important" +
                              "}" +
                              HIGHLIGHTED_PSEUDO_CLASS + " {" +
                              "  outline: 2px dashed #F06!important;" +
                              "  outline-offset: -2px!important;" +
                              "}";
-// Distance of the width or height handles from the node's edge.
-const GEOMETRY_SIZE_ARROW_OFFSET = .25; // 25%
 const GEOMETRY_LABEL_SIZE = 6;
 
 // Maximum size, in pixel, for the horizontal ruler and vertical ruler
 // used by RulersHighlighter
 const RULERS_MAX_X_AXIS = 10000;
 const RULERS_MAX_Y_AXIS = 15000;
 // Number of steps after we add a graduation, marker and text in
 // RulersHighliter; currently the unit is in pixel.
@@ -78,21 +86,21 @@ exports.isTypeRegistered = isTypeRegiste
 
 /**
  * Registers a given constructor as highlighter, for the `typeName` given.
  * If no `typeName` is provided, is looking for a `typeName` property in
  * the prototype's constructor.
  */
 const register = (constructor, typeName=constructor.prototype.typeName) => {
   if (!typeName) {
-    throw Error("No type's name found, or provided.")
+    throw Error("No type's name found, or provided.");
   }
 
   if (highlighterTypes.has(typeName)) {
-    throw Error(`${typeName} is already registered.`)
+    throw Error(`${typeName} is already registered.`);
   }
 
   highlighterTypes.set(typeName, constructor);
 };
 exports.register = register;
 
 /**
  * The Highlighter is the server-side entry points for any tool that wishes to
@@ -105,18 +113,19 @@ exports.register = register;
  * - <something>Highlighter classes aren't actors, they're just JS classes that
  *   know how to create and attach the actual highlighter elements on top of the
  *   content
  *
  * The most used highlighter actor is the HighlighterActor which can be
  * conveniently retrieved via the InspectorActor's 'getHighlighter' method.
  * The InspectorActor will always return the same instance of
  * HighlighterActor if asked several times and this instance is used in the
- * toolbox to highlighter elements's box-model from the markup-view, layout-view,
- * console, debugger, ... as well as select elements with the pointer (pick).
+ * toolbox to highlighter elements's box-model from the markup-view,
+ * layout-view, console, debugger, ... as well as select elements with the
+ * pointer (pick).
  *
  * Other types of highlighter actors exist and can be accessed via the
  * InspectorActor's 'getHighlighterByType' method.
  */
 
 /**
  * The HighlighterActor class
  */
@@ -277,17 +286,17 @@ let HighlighterActor = exports.Highlight
       }
       events.emit(this._walker, "picker-node-picked", this._currentNode);
     };
 
     this._onHovered = event => {
       this._preventContentEvent(event);
       this._currentNode = this._findAndAttachElement(event);
       if (this._hoveredNode !== this._currentNode.node) {
-        this._highlighter.show( this._currentNode.node.rawNode);
+        this._highlighter.show(this._currentNode.node.rawNode);
         events.emit(this._walker, "picker-node-hovered", this._currentNode);
         this._hoveredNode = this._currentNode.node;
       }
     };
 
     this._onKey = event => {
       if (!this._currentNode || !this._isPicking) {
         return;
@@ -298,25 +307,27 @@ let HighlighterActor = exports.Highlight
 
       /**
        * KEY: Action/scope
        * LEFT_KEY: wider or parent
        * RIGHT_KEY: narrower or child
        * ENTER/CARRIAGE_RETURN: Picks currentNode
        * ESC: Cancels picker, picks currentNode
        */
-      switch(event.keyCode) {
-        case Ci.nsIDOMKeyEvent.DOM_VK_LEFT: // wider
+      switch (event.keyCode) {
+        // Wider.
+        case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
           if (!currentNode.parentElement) {
             return;
           }
           currentNode = currentNode.parentElement;
           break;
 
-        case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: // narrower
+        // Narrower.
+        case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT:
           if (!currentNode.children.length) {
             return;
           }
 
           // Set firstElementChild by default
           let child = currentNode.firstElementChild;
           // If currentNode is parent of hoveredNode, then
           // previously selected childNode is set
@@ -325,21 +336,23 @@ let HighlighterActor = exports.Highlight
             if (sibling.contains(hoveredNode) || sibling === hoveredNode) {
               child = sibling;
             }
           }
 
           currentNode = child;
           break;
 
-        case Ci.nsIDOMKeyEvent.DOM_VK_RETURN: // select element
+        // Select the element.
+        case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
           this._onPick(event);
           return;
 
-        case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE: // cancel picking
+        // Cancel pick mode.
+        case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE:
           this.cancelPick();
           events.emit(this._walker, "picker-node-canceled");
           return;
 
         default: return;
       }
 
       // Store currently attached element
@@ -453,25 +466,27 @@ let CustomHighlighterActor = exports.Cus
   },
 
   /**
    * Show the highlighter.
    * This calls through to the highlighter instance's |show(node, options)|
    * method.
    *
    * Most custom highlighters are made to highlight DOM nodes, hence the first
-   * NodeActor argument (NodeActor as in toolkit/devtools/server/actor/inspector).
+   * NodeActor argument (NodeActor as in
+   * toolkit/devtools/server/actor/inspector).
    * Note however that some highlighters use this argument merely as a context
    * node: the RectHighlighter for instance uses it to calculate the absolute
    * position of the provided rect. The SelectHighlighter uses it as a base node
    * to run the provided CSS selector on.
    *
-   * @param NodeActor The node to be highlighted
-   * @param Object Options for the custom highlighter
-   * @return Boolean True, if the highlighter has been successfully shown (FF41+)
+   * @param {NodeActor} The node to be highlighted
+   * @param {Object} Options for the custom highlighter
+   * @return {Boolean} True, if the highlighter has been successfully shown
+   * (FF41+)
    */
   show: method(function(node, options) {
     if (!node || !isNodeValid(node.rawNode) || !this._highlighter) {
       return false;
     }
 
     return this._highlighter.show(node.rawNode, options);
   }, {
@@ -504,17 +519,16 @@ let CustomHighlighterActor = exports.Cus
       this._highlighterEnv.destroy();
       this._highlighterEnv = null;
     }
 
     if (this._highlighter) {
       this._highlighter.destroy();
       this._highlighter = null;
     }
-
   }, {
     oneway: true
   })
 });
 
 let CustomHighlighterFront = protocol.FrontClass(CustomHighlighterActor, {});
 
 /**
@@ -536,36 +550,38 @@ let CustomHighlighterFront = protocol.Fr
  *        A function that, when executed, returns a DOM node to be inserted into
  *        the canvasFrame.
  */
 function CanvasFrameAnonymousContentHelper(highlighterEnv, nodeBuilder) {
   this.highlighterEnv = highlighterEnv;
   this.nodeBuilder = nodeBuilder;
   this.anonymousContentDocument = this.highlighterEnv.document;
   // XXX the next line is a wallpaper for bug 1123362.
-  this.anonymousContentGlobal = Cu.getGlobalForObject(this.anonymousContentDocument);
+  this.anonymousContentGlobal = Cu.getGlobalForObject(
+                                this.anonymousContentDocument);
 
   this._insert();
 
   this._onNavigate = this._onNavigate.bind(this);
   this.highlighterEnv.on("navigate", this._onNavigate);
 
   this.listeners = new Map();
 }
 
 exports.CanvasFrameAnonymousContentHelper = CanvasFrameAnonymousContentHelper;
 
 CanvasFrameAnonymousContentHelper.prototype = {
   destroy: function() {
-    // If the current window isn't the one the content was inserted into, this
-    // will fail, but that's fine.
     try {
       let doc = this.anonymousContentDocument;
       doc.removeAnonymousContent(this._content);
-    } catch (e) {}
+    } catch (e) {
+      // If the current window isn't the one the content was inserted into, this
+      // will fail, but that's fine.
+    }
     this.highlighterEnv.off("navigate", this._onNavigate);
     this.highlighterEnv = this.nodeBuilder = this._content = null;
     this.anonymousContentDocument = null;
     this.anonymousContentGlobal = null;
 
     this._removeAllListeners();
   },
 
@@ -592,17 +608,17 @@ CanvasFrameAnonymousContentHelper.protot
       return;
     }
 
     // For now highlighter.css is injected in content as a ua sheet because
     // <style scoped> doesn't work inside anonymous content (see bug 1086532).
     // If it did, highlighter.css would be injected as an anonymous content
     // node using CanvasFrameAnonymousContentHelper instead.
     installHelperSheet(this.highlighterEnv.window,
-      "@import url('" + HIGHLIGHTER_STYLESHEET_URI + "');");
+      "@import url('" + STYLESHEET_URI + "');");
     let node = this.nodeBuilder();
     this._content = doc.insertAnonymousContent(node);
   },
 
   _onNavigate: function(e, {isTopLevel}) {
     if (isTopLevel) {
       this._removeAllListeners();
       this._insert();
@@ -684,31 +700,30 @@ CanvasFrameAnonymousContentHelper.protot
         " got: " + id);
     }
 
     // If no one is listening for this type of event yet, add one listener.
     if (!this.listeners.has(type)) {
       let target = this.highlighterEnv.pageListenerTarget;
       target.addEventListener(type, this, true);
       // Each type entry in the map is a map of ids:handlers.
-      this.listeners.set(type, new Map);
+      this.listeners.set(type, new Map());
     }
 
     let listeners = this.listeners.get(type);
     listeners.set(id, handler);
   },
 
   /**
    * Remove an event listener from one of the elements inserted in the
    * canvasFrame native anonymous container.
    * @param {String} id
    * @param {String} type
-   * @param {Function} handler
    */
-  removeEventListenerForElement: function(id, type, handler) {
+  removeEventListenerForElement: function(id, type) {
     let listeners = this.listeners.get(type);
     if (!listeners) {
       return;
     }
     listeners.delete(id);
 
     // If no one is listening for event type anymore, remove the listener.
     if (!this.listeners.has(type)) {
@@ -729,19 +744,18 @@ CanvasFrameAnonymousContentHelper.protot
     let eventProxy = new Proxy(event, {
       get: (obj, name) => {
         if (name === "originalTarget") {
           return null;
         } else if (name === "stopPropagation") {
           return () => {
             isPropagationStopped = true;
           };
-        } else {
-          return obj[name];
         }
+        return obj[name];
       }
     });
 
     // Start at originalTarget, bubble through ancestors and call handlers when
     // needed.
     let node = event.originalTarget;
     while (node) {
       let handler = listeners.get(node.id);
@@ -811,18 +825,18 @@ CanvasFrameAnonymousContentHelper.protot
    * @param {String} id The ID of the root element inserted with this API.
    */
   scaleRootElement: function(node, id) {
     let zoom = LayoutHelpers.getCurrentZoom(node);
     let value = "position:absolute;width:100%;height:100%;";
 
     if (zoom !== 1) {
       value = "position:absolute;";
-      value += "transform-origin:top left;transform:scale(" + (1/zoom) + ");";
-      value += "width:" + (100*zoom) + "%;height:" + (100*zoom) + "%;";
+      value += "transform-origin:top left;transform:scale(" + (1 / zoom) + ");";
+      value += "width:" + (100 * zoom) + "%;height:" + (100 * zoom) + "%;";
     }
 
     this.setAttributeForElement(id, "style", value);
   }
 };
 
 /**
  * Base class for auto-refresh-on-change highlighters. Sub classes will have a
@@ -948,17 +962,17 @@ AutoRefreshHighlighter.prototype = {
     this._updateAdjustedQuads();
     let newQuads = JSON.stringify(this.currentQuads);
     return oldQuads !== newQuads;
   },
 
   /**
    * Update the highlighter if the node has moved since the last update.
    */
-  update: function(e) {
+  update: function() {
     if (!isNodeValid(this.currentNode) || !this._hasMoved()) {
       return;
     }
 
     this._update();
     this.emit("updated");
   },
 
@@ -1247,17 +1261,17 @@ BoxModelHighlighter.prototype = Heritage
   getElement: function(id) {
     return this.markup.getElement(this.ID_CLASS_PREFIX + id);
   },
 
   /**
    * Show the highlighter on a given node
    */
   _show: function() {
-    if (BOX_MODEL_REGIONS.indexOf(this.options.region) == -1)  {
+    if (BOX_MODEL_REGIONS.indexOf(this.options.region) == -1) {
       this.options.region = "content";
     }
 
     let shown = this._update();
     this._trackMutations();
     this.emit("ready");
     return shown;
   },
@@ -1585,36 +1599,40 @@ BoxModelHighlighter.prototype = Heritage
   /**
    * Update node information (tagName#id.class)
    */
   _updateInfobar: function() {
     if (!this.currentNode) {
       return;
     }
 
-    let {bindingElement:node, pseudo} =
-      CssLogic.getBindingElementAndPseudo(this.currentNode);
+    let {bindingElement: node, pseudo} =
+        CssLogic.getBindingElementAndPseudo(this.currentNode);
 
     // Update the tag, id, classes, pseudo-classes and dimensions
     let tagName = node.tagName;
 
     let id = node.id ? "#" + node.id : "";
 
-    let classList = (node.classList || []).length ? "." + [...node.classList].join(".") : "";
+    let classList = (node.classList || []).length
+                    ? "." + [...node.classList].join(".")
+                    : "";
 
     let pseudos = PSEUDO_CLASSES.filter(pseudo => {
       return DOMUtils.hasPseudoClassLock(node, pseudo);
     }, this).join("");
     if (pseudo) {
       // Display :after as ::after
       pseudos += ":" + pseudo;
     }
 
     let rect = this._getOuterQuad("border").bounds;
-    let dim = parseFloat(rect.width.toPrecision(6)) + " \u00D7 " + parseFloat(rect.height.toPrecision(6));
+    let dim = parseFloat(rect.width.toPrecision(6)) +
+              " \u00D7 " +
+              parseFloat(rect.height.toPrecision(6));
 
     this.getElement("nodeinfobar-tagname").setTextContent(tagName);
     this.getElement("nodeinfobar-id").setTextContent(id);
     this.getElement("nodeinfobar-classes").setTextContent(classList);
     this.getElement("nodeinfobar-pseudo-classes").setTextContent(pseudos);
     this.getElement("nodeinfobar-dimensions").setTextContent(dim);
 
     this._moveInfobar();
@@ -1689,18 +1707,16 @@ function CssTransformHighlighter(highlig
 let MARKER_COUNTER = 1;
 
 CssTransformHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype, {
   typeName: "CssTransformHighlighter",
 
   ID_CLASS_PREFIX: "css-transform-",
 
   _buildMarkup: function() {
-    let doc = this.win.document;
-
     let container = createNode(this.win, {
       attributes: {
         "class": "highlighter-container"
       }
     });
 
     // The root wrapper is used to unzoom the highlighter when needed.
     let rootWrapper = createNode(this.win, {
@@ -1721,17 +1737,17 @@ CssTransformHighlighter.prototype = Heri
         "width": "100%",
         "height": "100%"
       },
       prefix: this.ID_CLASS_PREFIX
     });
 
     // Add a marker tag to the svg root for the arrow tip
     this.markerId = "arrow-marker-" + MARKER_COUNTER;
-    MARKER_COUNTER ++;
+    MARKER_COUNTER++;
     let marker = createSVGNode(this.win, {
       nodeType: "marker",
       parent: svg,
       attributes: {
         "id": this.markerId,
         "markerWidth": "10",
         "markerHeight": "5",
         "orient": "auto",
@@ -1822,17 +1838,17 @@ CssTransformHighlighter.prototype = Heri
    */
   _isTransformed: function(node) {
     let style = CssLogic.getComputedStyle(node);
     return style && (style.transform !== "none" && style.display !== "inline");
   },
 
   _setPolygonPoints: function(quad, id) {
     let points = [];
-    for (let point of ["p1","p2", "p3", "p4"]) {
+    for (let point of ["p1", "p2", "p3", "p4"]) {
       points.push(quad[point].x + "," + quad[point].y);
     }
     this.getElement(id).setAttribute("points", points.join(" "));
   },
 
   _setLinePoints: function(p1, p2, id) {
     let line = this.getElement(id);
     line.setAttribute("x1", p1.x);
@@ -1931,33 +1947,36 @@ SelectorHighlighter.prototype = {
 
     if (!isNodeValid(node) || !options.selector) {
       return false;
     }
 
     let nodes = [];
     try {
       nodes = [...node.ownerDocument.querySelectorAll(options.selector)];
-    } catch (e) {}
+    } catch (e) {
+      // It's fine if the provided selector is invalid, nodes will be an empty
+      // array.
+    }
 
     delete options.selector;
 
     let i = 0;
     for (let matchingNode of nodes) {
       if (i >= MAX_HIGHLIGHTED_ELEMENTS) {
         break;
       }
 
       let highlighter = new BoxModelHighlighter(this.highlighterEnv);
       if (options.fill) {
         highlighter.regionFill[options.region || "border"] = options.fill;
       }
       highlighter.show(matchingNode, options);
       this._highlighters.push(highlighter);
-      i ++;
+      i++;
     }
 
     return true;
   },
 
   hide: function() {
     for (let highlighter of this._highlighters) {
       highlighter.destroy();
@@ -1990,18 +2009,18 @@ function RectHighlighter(highlighterEnv)
 RectHighlighter.prototype = {
   typeName: "RectHighlighter",
 
   _buildMarkup: function() {
     let doc = this.win.document;
 
     let container = doc.createElement("div");
     container.className = "highlighter-container";
-    container.innerHTML = '<div id="highlighted-rect" ' +
-                          'class="highlighted-rect" hidden="true">';
+    container.innerHTML = "<div id=\"highlighted-rect\" " +
+                          "class=\"highlighted-rect\" hidden=\"true\">";
 
     return container;
   },
 
   destroy: function() {
     this.win = null;
     this.layoutHelpers = null;
     this.markup.destroy();
@@ -2021,17 +2040,18 @@ RectHighlighter.prototype = {
   },
 
   /**
    * @param {DOMNode} node The highlighter rect is relatively positioned to the
    * viewport this node is in. Using the provided node, the highligther will get
    * the parent documentElement and use it as context to position the
    * highlighter correctly.
    * @param {Object} options Accepts the following options:
-   * - rect: mandatory object that should have the x, y, width, height properties
+   * - rect: mandatory object that should have the x, y, width, height
+   *   properties
    * - fill: optional fill color for the rect
    */
   show: function(node, options) {
     if (!this._hasValidOptions(options) || !node || !node.ownerDocument) {
       this.hide();
       return false;
     }
 
@@ -2346,18 +2366,18 @@ GeometryEditorHighlighter.prototype = He
 
     // Get the list of css rules applying to the current node.
     let cssRules = DOMUtils.getCSSStyleRules(this.currentNode);
     for (let i = 0; i < cssRules.Count(); i++) {
       let rule = cssRules.GetElementAt(i);
       for (let name of GeoProp.allProps()) {
         let value = rule.style.getPropertyValue(name);
         if (value && value !== "auto") {
-          // getCSSStyleRules returns rules ordered from least-specific to
-          // most-specific, so just override any previous properties we have set.
+          // getCSSStyleRules returns rules ordered from least to most specific
+          // so just override any previous properties we have set.
           props.set(name, {
             cssRule: rule
           });
         }
       }
     }
 
     // Go through the inline styles last.
@@ -2413,19 +2433,16 @@ GeometryEditorHighlighter.prototype = He
     return true;
   },
 
   _update: function() {
     // At each update, the position or/and size may have changed, so get the
     // list of defined properties, and re-position the arrows and highlighters.
     this.definedProperties = this.getDefinedGeometryProperties();
 
-    let isStatic = this.computedStyle.position === "static";
-    let hasSizes = GeoProp.containsSize([...this.definedProperties.keys()]);
-
     if (!this.definedProperties.size) {
       console.warn("The element does not have editable geometry properties");
       return false;
     }
 
     setIgnoreLayoutChanges(true);
 
     // Update the highlighters and arrows.
@@ -2559,20 +2576,16 @@ GeometryEditorHighlighter.prototype = He
     }
   },
 
   updateArrows: function() {
     this.hideArrows();
 
     // Position arrows always end at the node's margin box.
     let marginBox = this.currentQuads.margin[0].bounds;
-    // But size arrows are displayed in the box that corresponds to the current
-    // box-sizing.
-    let boxSizing = this.computedStyle.boxSizing.split("-")[0];
-    let box = this.currentQuads[boxSizing][0].bounds;
 
     // Position the side arrows which need to be visible.
     // Arrows always start at the offsetParent edge, and end at the middle
     // position of the node's margin edge.
     // Note that for relative positioning, the offsetParent is considered to be
     // the node itself, where it would have been originally.
     // +------------------+----------------+
     // | offsetparent     | top            |
@@ -2589,28 +2602,26 @@ GeometryEditorHighlighter.prototype = He
       if (this.parentQuads && this.parentQuads.length) {
         return this.parentQuads[0].bounds[side];
       }
 
       // In case of relative positioning.
       if (this.computedStyle.position === "relative") {
         if (GeoProp.isInverted(side)) {
           return marginBox[side] + parseFloat(this.computedStyle[side]);
-        } else {
-          return marginBox[side] - parseFloat(this.computedStyle[side]);
         }
+        return marginBox[side] - parseFloat(this.computedStyle[side]);
       }
 
       // In case the element is positioned in the viewport.
       if (GeoProp.isInverted(side)) {
         return this.offsetParent.dimension[GeoProp.mainAxisSize(side)];
-      } else {
-        return -1 * getWindow(this.currentNode)["scroll" +
-                                                GeoProp.axis(side).toUpperCase()];
       }
+      return -1 * getWindow(this.currentNode)["scroll" +
+                                              GeoProp.axis(side).toUpperCase()];
     };
 
     for (let side of GeoProp.SIDES) {
       let sideProp = this.definedProperties.get(side);
       if (!sideProp) {
         continue;
       }
 
@@ -2634,17 +2645,17 @@ GeometryEditorHighlighter.prototype = He
     arrowEl.setAttribute(GeoProp.crossAxis(side) + "1", crossPos);
     arrowEl.setAttribute(GeoProp.axis(side) + "2", mainEnd);
     arrowEl.setAttribute(GeoProp.crossAxis(side) + "2", crossPos);
     arrowEl.removeAttribute("hidden");
 
     // Position the label <text> in the middle of the arrow (making sure it's
     // not hidden below the fold).
     let capitalize = str => str.substring(0, 1).toUpperCase() + str.substring(1);
-    let winMain = this.win["inner" + capitalize(GeoProp.mainAxisSize(side))]
+    let winMain = this.win["inner" + capitalize(GeoProp.mainAxisSize(side))];
     let labelMain = mainStart + (mainEnd - mainStart) / 2;
     if ((mainStart > 0 && mainStart < winMain) ||
         (mainEnd > 0 && mainEnd < winMain)) {
       if (labelMain < GEOMETRY_LABEL_SIZE) {
         labelMain = GEOMETRY_LABEL_SIZE;
       } else if (labelMain > winMain - GEOMETRY_LABEL_SIZE) {
         labelMain = winMain - GEOMETRY_LABEL_SIZE;
       }
@@ -2669,17 +2680,17 @@ function RulersHighlighter(highlighterEn
   this.win = highlighterEnv.window;
   this.markup = new CanvasFrameAnonymousContentHelper(highlighterEnv,
     this._buildMarkup.bind(this));
 
   this.win.addEventListener("scroll", this, true);
   this.win.addEventListener("pagehide", this, true);
 }
 
-RulersHighlighter.prototype =  {
+RulersHighlighter.prototype = {
   typeName: "RulersHighlighter",
 
   ID_CLASS_PREFIX: "rulers-highlighter-",
 
   _buildMarkup: function() {
     let prefix = this.ID_CLASS_PREFIX;
     let window = this.win;
 
@@ -2757,44 +2768,47 @@ RulersHighlighter.prototype =  {
         parent: g,
         prefix
       });
 
       let dGraduations = "";
       let dMarkers = "";
       let graduationLength;
 
-      for (let i = 0; i < size; i+=RULERS_GRADUATION_STEP) {
-        if (i === 0) continue;
+      for (let i = 0; i < size; i += RULERS_GRADUATION_STEP) {
+        if (i === 0) {
+          continue;
+        }
 
         graduationLength = (i % 2 === 0) ? 6 : 4;
 
         if (i % RULERS_TEXT_STEP === 0) {
           graduationLength = 8;
           createSVGNode(window, {
             nodeType: "text",
             parent: gText,
             attributes: {
               x: isHorizontal ? 2 + i : -i - 1,
               y: 5
             }
           }).textContent = i;
         }
 
         if (isHorizontal) {
-          if (i % RULERS_MARKER_STEP === 0)
+          if (i % RULERS_MARKER_STEP === 0) {
             dMarkers += `M${i} 0 L${i} ${graduationLength}`;
-          else
+          } else {
             dGraduations += `M${i} 0 L${i} ${graduationLength} `;
-
+          }
         } else {
-          if (i % 50 === 0)
+          if (i % 50 === 0) {
             dMarkers += `M0 ${i} L${graduationLength} ${i}`;
-          else
+          } else {
             dGraduations += `M0 ${i} L${graduationLength} ${i}`;
+          }
         }
       }
 
       pathGraduations.setAttribute("d", dGraduations);
       pathMarkers.setAttribute("d", dMarkers);
 
       return g;
     }
@@ -2924,17 +2938,17 @@ SimpleOutlineHighlighter.prototype = {
       DOMUtils.removePseudoClassLock(this.currentNode, HIGHLIGHTED_PSEUDO_CLASS);
       this.currentNode = null;
     }
   }
 };
 
 function isNodeValid(node) {
   // Is it null or dead?
-  if(!node || Cu.isDeadWrapper(node)) {
+  if (!node || Cu.isDeadWrapper(node)) {
     return false;
   }
 
   // Is it an element node
   if (node.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
     return false;
   }
 
@@ -2952,17 +2966,17 @@ function isNodeValid(node) {
   }
 
   return true;
 }
 
 /**
  * Inject a helper stylesheet in the window.
  */
-let installedHelperSheets = new WeakMap;
+let installedHelperSheets = new WeakMap();
 function installHelperSheet(win, source, type="agent") {
   if (installedHelperSheets.has(win.document)) {
     return;
   }
   let {Style} = require("sdk/stylesheet/style");
   let {attach} = require("sdk/content/mod");
   let style = Style({source, type});
   attach(style, win);
@@ -3018,17 +3032,17 @@ function createNode(win, options) {
     node = win.document.createElementNS(options.namespace, type);
   } else {
     node = win.document.createElement(type);
   }
 
   for (let name in options.attributes || {}) {
     let value = options.attributes[name];
     if (options.prefix && (name === "class" || name === "id")) {
-      value = options.prefix + value
+      value = options.prefix + value;
     }
     node.setAttribute(name, value);
   }
 
   if (options.parent) {
     options.parent.appendChild(node);
   }
 
@@ -3221,12 +3235,8 @@ HighlighterEnvironment.prototype = {
         // Which may fail in case the window was already destroyed.
       }
     }
 
     this._tabActor = null;
     this._win = null;
   }
 };
-
-XPCOMUtils.defineLazyGetter(this, "DOMUtils", function() {
-  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
-});
--- a/toolkit/devtools/server/actors/styles.js
+++ b/toolkit/devtools/server/actors/styles.js
@@ -1,30 +1,33 @@
 /* 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/. */
+/* globals CssLogic, DOMUtils, CSS */
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
-const Services = require("Services");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const protocol = require("devtools/server/protocol");
 const {Arg, Option, method, RetVal, types} = protocol;
 const events = require("sdk/event/core");
-const object = require("sdk/util/object");
 const {Class} = require("sdk/core/heritage");
 const {LongStringActor} = require("devtools/server/actors/string");
 const {PSEUDO_ELEMENT_SET} = require("devtools/styleinspector/css-logic");
 
 // This will add the "stylesheet" actor type for protocol.js to recognize
 require("devtools/server/actors/stylesheets");
 
-loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic);
-loader.lazyGetter(this, "DOMUtils", () => Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils));
+loader.lazyGetter(this, "CssLogic", () => {
+  return require("devtools/styleinspector/css-logic").CssLogic;
+});
+loader.lazyGetter(this, "DOMUtils", () => {
+  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
 
 // The PageStyle actor flattens the DOM CSS objects a little bit, merging
 // Rules and their Styles into one actor.  For elements (which have a style
 // but no associated rule) we fake a rule with the following style id.
 const ELEMENT_STYLE = 100;
 exports.ELEMENT_STYLE = ELEMENT_STYLE;
 
 // Not included since these are uneditable by the user.
@@ -48,16 +51,18 @@ const PSEUDO_ELEMENTS_TO_READ = PSEUDO_E
 });
 
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 const FONT_PREVIEW_TEXT = "Abc";
 const FONT_PREVIEW_FONT_SIZE = 40;
 const FONT_PREVIEW_FILLSTYLE = "black";
 const NORMAL_FONT_WEIGHT = 400;
 const BOLD_FONT_WEIGHT = 700;
+// Offset (in px) to avoid cutting off text edges of italic fonts.
+const FONT_PREVIEW_OFFSET = 4;
 
 // Predeclare the domnode actor type for use in requests.
 types.addActorType("domnode");
 
 // Predeclare the domstylerule actor type
 types.addActorType("domstylerule");
 
 /**
@@ -115,17 +120,17 @@ types.addDictType("fontface", {
   localName: "string",
   metadata: "string"
 });
 
 /**
  * The PageStyle actor lets the client look at the styles on a page, as
  * they are applied to a given node.
  */
-var PageStyleActor = protocol.ActorClass({
+let PageStyleActor = protocol.ActorClass({
   typeName: "pagestyle",
 
   /**
    * Create a PageStyleActor.
    *
    * @param inspector
    *    The InspectorActor that owns this PageStyleActor.
    *
@@ -134,20 +139,20 @@ var PageStyleActor = protocol.ActorClass
   initialize: function(inspector) {
     protocol.Actor.prototype.initialize.call(this, null);
     this.inspector = inspector;
     if (!this.inspector.walker) {
       throw Error("The inspector's WalkerActor must be created before " +
                    "creating a PageStyleActor.");
     }
     this.walker = inspector.walker;
-    this.cssLogic = new CssLogic;
+    this.cssLogic = new CssLogic();
 
     // Stores the association of DOM objects -> actors
-    this.refMap = new Map;
+    this.refMap = new Map();
 
     this.onFrameUnload = this.onFrameUnload.bind(this);
     events.on(this.inspector.tabActor, "will-navigate", this.onFrameUnload);
   },
 
   get conn() {
     return this.inspector.conn;
   },
@@ -224,28 +229,27 @@ var PageStyleActor = protocol.ActorClass
   getComputed: method(function(node, options) {
     let ret = Object.create(null);
 
     this.cssLogic.sourceFilter = options.filter || CssLogic.FILTER.UA;
     this.cssLogic.highlight(node.rawNode);
     let computed = this.cssLogic.computedStyle || [];
 
     Array.prototype.forEach.call(computed, name => {
-      let matched = undefined;
       ret[name] = {
         value: computed.getPropertyValue(name),
         priority: computed.getPropertyPriority(name) || undefined
       };
     });
 
     if (options.markMatched || options.onlyMatched) {
       let matched = this.cssLogic.hasMatchedSelectors(Object.keys(ret));
       for (let key in ret) {
         if (matched[key]) {
-          ret[key].matched = options.markMatched ? true : undefined
+          ret[key].matched = options.markMatched ? true : undefined;
         } else if (options.onlyMatched) {
           delete ret[key];
         }
       }
     }
 
     return ret;
   }, {
@@ -269,23 +273,22 @@ var PageStyleActor = protocol.ActorClass
    *   `previewFontSize`: The font size of the text in the previews.
    *
    * @returns object
    *   object with 'fontFaces', a list of fonts that apply to this node.
    */
   getAllUsedFontFaces: method(function(options) {
     let windows = this.inspector.tabActor.windows;
     let fontsList = [];
-    for(let win of windows){
+    for (let win of windows) {
       fontsList = [...fontsList,
                    ...this.getUsedFontFaces(win.document.body, options)];
     }
     return fontsList;
-  },
-  {
+  }, {
     request: {
       includePreviews: Option(0, "boolean"),
       previewText: Option(0, "string"),
       previewFontSize: Option(0, "string"),
       previewFillStyle: Option(0, "string")
     },
     response: {
       fontFaces: RetVal("array:fontface")
@@ -320,17 +323,17 @@ var PageStyleActor = protocol.ActorClass
       let fontFace = {
         name: font.name,
         CSSFamilyName: font.CSSFamilyName,
         srcIndex: font.srcIndex,
         URI: font.URI,
         format: font.format,
         localName: font.localName,
         metadata: font.metadata
-      }
+      };
 
       // If this font comes from a @font-face rule
       if (font.rule) {
         fontFace.rule = StyleRuleActor(this, font.rule);
         fontFace.ruleText = font.rule.cssText;
       }
 
       // Get the weight and style of this font for the preview and sort order
@@ -349,17 +352,17 @@ var PageStyleActor = protocol.ActorClass
       fontFace.style = style;
 
       if (options.includePreviews) {
         let opts = {
           previewText: options.previewText,
           previewFontSize: options.previewFontSize,
           fontStyle: weight + " " + style,
           fillStyle: options.previewFillStyle
-        }
+        };
         let { dataURL, size } = getFontPreviewData(font.CSSFamilyName,
                                                    contentDocument, opts);
         fontFace.preview = {
           data: LongStringActor(this.conn, dataURL),
           size: size
         };
       }
       fontsArray.push(fontFace);
@@ -431,20 +434,18 @@ var PageStyleActor = protocol.ActorClass
    *     // The full form of any sheets referenced.
    *     sheets: [ <domsheet>, ... ]
    *  }
    */
   getMatchedSelectors: method(function(node, property, options) {
     this.cssLogic.sourceFilter = options.filter || CssLogic.FILTER.UA;
     this.cssLogic.highlight(node.rawNode);
 
-    let walker = node.parent();
-
-    let rules = new Set;
-    let sheets = new Set;
+    let rules = new Set();
+    let sheets = new Set();
 
     let matched = [];
     let propInfo = this.cssLogic.getPropertyInfo(property);
     for (let selectorInfo of propInfo.matchedSelectors) {
       let cssRule = selectorInfo.selector.cssRule;
       let domRule = cssRule.sourceElement || cssRule.domRule;
 
       let rule = this._styleRef(domRule);
@@ -460,17 +461,17 @@ var PageStyleActor = protocol.ActorClass
       });
     }
 
     this.expandSets(rules, sheets);
 
     return {
       matched: matched,
       rules: [...rules],
-      sheets: [...sheets],
+      sheets: [...sheets]
     };
   }, {
     request: {
       node: Arg(0, "domnode"),
       property: Arg(1, "string"),
       filter: Option(2, "string")
     },
     response: RetVal(types.addDictType("matchedselectorresponse", {
@@ -486,17 +487,17 @@ var PageStyleActor = protocol.ActorClass
     let result = selectorInfo.selector.text;
     if (selectorInfo.elementStyle) {
       let source = selectorInfo.sourceElement;
       if (source === relativeTo) {
         result = "this";
       } else {
         result = CssLogic.getShortName(source);
       }
-      result += ".style"
+      result += ".style";
     }
     return result;
   },
 
   /**
    * Get the set of styles that apply to a given node.
    * @param NodeActor node
    * @param object options
@@ -552,17 +553,18 @@ var PageStyleActor = protocol.ActorClass
     let rules = [];
 
     if (!bindingElement || !bindingElement.style) {
       return rules;
     }
 
     let elementStyle = this._styleRef(bindingElement);
     let showElementStyles = !inherited && !pseudo;
-    let showInheritedStyles = inherited && this._hasInheritedProps(bindingElement.style);
+    let showInheritedStyles = inherited &&
+                              this._hasInheritedProps(bindingElement.style);
 
     let rule = {
       rule: elementStyle,
       pseudoElement: null,
       isSystem: false,
       inherited: false
     };
 
@@ -575,29 +577,29 @@ var PageStyleActor = protocol.ActorClass
     if (showInheritedStyles) {
       rule.inherited = inherited;
       rules.push(rule);
     }
 
     // Add normal rules.  Typically this is passing in the node passed into the
     // function, unless if that node was ::before/::after.  In which case,
     // it will pass in the parentNode along with "::before"/"::after".
-    this._getElementRules(bindingElement, pseudo, inherited, options).forEach((rule) => {
+    this._getElementRules(bindingElement, pseudo, inherited, options).forEach(rule => {
       // The only case when there would be a pseudo here is ::before/::after,
       // and in this case we want to tell the view that it belongs to the
       // element (which is a _moz_generated_content native anonymous element).
       rule.pseudoElement = null;
       rules.push(rule);
     });
 
     // Now any pseudos (except for ::before / ::after, which was handled as
     // a 'normal rule' above.
     if (showElementStyles) {
       for (let pseudo of PSEUDO_ELEMENTS_TO_READ) {
-        this._getElementRules(bindingElement, pseudo, inherited, options).forEach((rule) => {
+        this._getElementRules(bindingElement, pseudo, inherited, options).forEach(rule => {
           rules.push(rule);
         });
       }
     }
 
     return rules;
   },
 
@@ -606,17 +608,17 @@ var PageStyleActor = protocol.ActorClass
    * element. See getApplied for documentation on parameters.
    * @param DOMNode node
    * @param string pseudo
    * @param DOMNode inherited
    * @param object options
    *
    * @returns Array
    */
-  _getElementRules: function (node, pseudo, inherited, options) {
+  _getElementRules: function(node, pseudo, inherited, options) {
     let domRules = DOMUtils.getCSSStyleRules(node, pseudo);
     if (!domRules) {
       return [];
     }
 
     let rules = [];
 
     // getCSSStyleRules returns ordered from least-specific to
@@ -687,17 +689,17 @@ var PageStyleActor = protocol.ActorClass
         if (entry.rule.type === ELEMENT_STYLE) {
           continue;
         }
 
         let domRule = entry.rule.rawRule;
         let selectors = CssLogic.getSelectors(domRule);
         let element = entry.inherited ? entry.inherited.rawNode : node.rawNode;
 
-        let {bindingElement,pseudo} = CssLogic.getBindingElementAndPseudo(element);
+        let {bindingElement, pseudo} = CssLogic.getBindingElementAndPseudo(element);
         entry.matchedSelectors = [];
         for (let i = 0; i < selectors.length; i++) {
           if (DOMUtils.selectorMatchesElement(bindingElement, domRule, i, pseudo)) {
             entry.matchedSelectors.push(selectors[i]);
           }
         }
       }
     }
@@ -719,26 +721,26 @@ var PageStyleActor = protocol.ActorClass
                 keyframes: this._styleRef(keyframesRule)
               });
             }
           }
         }
       }
     }
 
-    let rules = new Set;
-    let sheets = new Set;
+    let rules = new Set();
+    let sheets = new Set();
     entries.forEach(entry => rules.add(entry.rule));
     this.expandSets(rules, sheets);
 
     return {
       entries: entries,
       rules: [...rules],
       sheets: [...sheets]
-    }
+    };
   },
 
   /**
    * Expand Sets of rules and sheets to include all parent rules and sheets.
    */
   expandSets: function(ruleSet, sheetSet) {
     // Sets include new items in their iteration
     for (let rule of ruleSet) {
@@ -802,19 +804,18 @@ var PageStyleActor = protocol.ActorClass
       layout.autoMargins = this.processMargins(this.cssLogic);
     }
 
     for (let i in this.map) {
       let property = this.map[i].property;
       this.map[i].value = parseFloat(style.getPropertyValue(property));
     }
 
-
     if (options.margins) {
-      layout.margins = this.processMargins(cssLogic);
+      layout.margins = this.processMargins(this.cssLogic);
     }
 
     return layout;
   }, {
     request: {
       node: Arg(0, "domnode"),
       autoMargins: Option(1, "boolean")
     },
@@ -847,17 +848,17 @@ var PageStyleActor = protocol.ActorClass
 
   /**
    * Helper function to addNewRule to construct a new style tag in the document.
    * @returns DOMElement of the style tag
    */
   get styleElement() {
     if (!this._styleElement) {
       let document = this.inspector.window.document;
-      let style = document.createElementNS("http://www.w3.org/1999/xhtml", "style");
+      let style = document.createElementNS(XHTML_NS, "style");
       style.setAttribute("type", "text/css");
       document.documentElement.appendChild(style);
       this._styleElement = style;
     }
 
     return this._styleElement;
   },
 
@@ -886,18 +887,17 @@ var PageStyleActor = protocol.ActorClass
     let sheet = style.sheet;
     let cssRules = sheet.cssRules;
     let rawNode = node.rawNode;
 
     let selector;
     if (rawNode.id) {
       selector = "#" + CSS.escape(rawNode.id);
     } else if (rawNode.className) {
-      selector = "." +
-        rawNode.className.split(" ").map(c => CSS.escape(c)).join(".");
+      selector = "." + [...rawNode.classList].map(c => CSS.escape(c)).join(".");
     } else {
       selector = rawNode.tagName.toLowerCase();
     }
 
     if (pseudoClasses && pseudoClasses.length > 0) {
       selector += pseudoClasses.join("");
     }
 
@@ -911,17 +911,17 @@ var PageStyleActor = protocol.ActorClass
     response: RetVal("appliedStylesReturn")
   }),
 });
 exports.PageStyleActor = PageStyleActor;
 
 /**
  * Front object for the PageStyleActor
  */
-var PageStyleFront = protocol.FrontClass(PageStyleActor, {
+let PageStyleFront = protocol.FrontClass(PageStyleActor, {
   initialize: function(conn, form, ctx, detail) {
     protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail);
     this.inspector = this.parent();
   },
 
   form: function(form, detail) {
     if (detail === "actorid") {
       this.actorID = form;
@@ -972,17 +972,17 @@ var PageStyleFront = protocol.FrontClass
 /**
  * An actor that represents a CSS style object on the protocol.
  *
  * We slightly flatten the CSSOM for this actor, it represents
  * both the CSSRule and CSSStyle objects in one actor.  For nodes
  * (which have a CSSStyle but no CSSRule) we create a StyleRuleActor
  * with a special rule type (100).
  */
-var StyleRuleActor = protocol.ActorClass({
+let StyleRuleActor = protocol.ActorClass({
   typeName: "domstylerule",
   initialize: function(pageStyle, item) {
     protocol.Actor.prototype.initialize.call(this, null);
     this.pageStyle = pageStyle;
     this.rawStyle = item.style;
 
     if (item instanceof (Ci.nsIDOMCSSRule)) {
       this.type = item.type;
@@ -994,18 +994,20 @@ var StyleRuleActor = protocol.ActorClass
         this.column = DOMUtils.getRuleColumn(this.rawRule);
       }
     } else {
       // Fake a rule
       this.type = ELEMENT_STYLE;
       this.rawNode = item;
       this.rawRule = {
         style: item.style,
-        toString: function() { return "[element rule " + this.style + "]"; }
-      }
+        toString: function() {
+          return "[element rule " + this.style + "]";
+        }
+      };
     }
   },
 
   get conn() {
     return this.pageStyle.conn;
   },
 
   // Objects returned by this actor are owned by the PageStyleActor
@@ -1021,17 +1023,19 @@ var StyleRuleActor = protocol.ActorClass
       document = sheet.ownerNode;
     } else {
       document = sheet.ownerNode.ownerDocument;
     }
 
     return document;
   },
 
-  toString: function() { return "[StyleRuleActor for " + this.rawRule + "]" },
+  toString: function() {
+    return "[StyleRuleActor for " + this.rawRule + "]"
+  },
 
   form: function(detail) {
     if (detail === "actorid") {
       return this.actorID;
     }
 
     let form = {
       actor: this.actorID,
@@ -1107,20 +1111,19 @@ var StyleRuleActor = protocol.ActorClass
    * {
    *   type: "remove",
    *   name: <string>,
    * }
    *
    * @returns the rule with updated properties
    */
   modifyProperties: method(function(modifications) {
-    let validProps = new Map();
-
-    // Use a fresh element for each call to this function to prevent side effects
-    // that pop up based on property values that were already set on the element.
+    // Use a fresh element for each call to this function to prevent side
+    // effects that pop up based on property values that were already set on the
+    // element.
 
     let document;
     if (this.rawNode) {
       document = this.rawNode.ownerDocument;
     } else {
       let parentStyleSheet = this.rawRule.parentStyleSheet;
       while (parentStyleSheet.ownerRule &&
           parentStyleSheet.ownerRule instanceof Ci.nsIDOMCSSImportRule) {
@@ -1171,17 +1174,20 @@ var StyleRuleActor = protocol.ActorClass
       if (rule === cssRules.item(i)) {
         try {
           // Inserts the new style rule into the current style sheet and
           // delete the current rule
           let ruleText = cssText.slice(selectorText.length).trim();
           parentStyleSheet.insertRule(value + " " + ruleText, i);
           parentStyleSheet.deleteRule(i + 1);
           return cssRules.item(i);
-        } catch(e) {}
+        } catch(e) {
+          // The selector could be invalid, or the rule could fail to insert.
+          // If that happens, the method returns null.
+        }
 
         break;
       }
     }
 
     return null;
   },
 
@@ -1214,19 +1220,18 @@ var StyleRuleActor = protocol.ActorClass
       return false;
     }
 
     // Check if the selector is valid and not the same as the original
     // selector
     if (selectorElement && this.rawRule.selectorText !== value) {
       this._addNewSelector(value);
       return true;
-    } else {
-      return false;
     }
+    return false;
   }, {
     request: { selector: Arg(0, "string") },
     response: { isModified: RetVal("boolean") },
   }),
 
   /**
    * Modify the current rule's selector by inserting a new rule with the new
    * selector value and removing the current rule.
@@ -1258,32 +1263,34 @@ var StyleRuleActor = protocol.ActorClass
     let newCssRule = this._addNewSelector(value);
     if (newCssRule) {
       ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule);
     }
 
     // Determine if the new selector value matches the current selected element
     try {
       isMatching = node.rawNode.matches(value);
-    } catch(e) {}
+    } catch(e) {
+      // This fails when value is an invalid selector.
+    }
 
     return { ruleProps, isMatching };
   }, {
     request: {
       node: Arg(0, "domnode"),
       value: Arg(1, "string")
     },
     response: RetVal("modifiedStylesReturn")
   })
 });
 
 /**
  * Front for the StyleRule actor.
  */
-var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
+let StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
   initialize: function(client, form, ctx, detail) {
     protocol.Front.prototype.initialize.call(this, client, form, ctx, detail);
   },
 
   destroy: function() {
     protocol.Front.prototype.destroy.call(this);
   },
 
@@ -1365,28 +1372,26 @@ var StyleRuleFront = protocol.FrontClass
     let sheet = this.parentStyleSheet;
     return sheet ? sheet.nodeHref : "";
   },
 
   get supportsModifySelectorUnmatched() {
     return this._form.traits && this._form.traits.modifySelectorUnmatched;
   },
 
-  get location()
-  {
+  get location() {
     return {
       source: this.parentStyleSheet,
       href: this.href,
       line: this.line,
       column: this.column
     };
   },
 
-  getOriginalLocation: function()
-  {
+  getOriginalLocation: function() {
     if (this._originalLocation) {
       return promise.resolve(this._originalLocation);
     }
     let parentSheet = this.parentStyleSheet;
     if (!parentSheet) {
       // This rule doesn't belong to a stylesheet so it is an inline style.
       // Inline styles do not have any mediaText so we can return early.
       return promise.resolve(this.location);
@@ -1427,17 +1432,17 @@ var StyleRuleFront = protocol.FrontClass
     impl: "_modifySelector"
   })
 });
 
 /**
  * Convenience API for building a list of attribute modifications
  * for the `modifyAttributes` request.
  */
-var RuleModificationList = Class({
+let RuleModificationList = Class({
   initialize: function(rule) {
     this.rule = rule;
     this.modifications = [];
   },
 
   apply: function() {
     return this.rule.modifyProperties(this.modifications);
   },
@@ -1480,30 +1485,32 @@ function getFontPreviewData(font, doc, o
   let canvas = doc.createElementNS(XHTML_NS, "canvas");
   let ctx = canvas.getContext("2d");
   let fontValue = fontStyle + " " + previewFontSize + "px " + font + ", serif";
 
   // Get the correct preview text measurements and set the canvas dimensions
   ctx.font = fontValue;
   ctx.fillStyle = fillStyle;
   let textWidth = ctx.measureText(previewText).width;
-  let offset = 4; // offset to avoid cutting off text edge of italics
-  canvas.width = textWidth * 2 + offset * 2;
+
+  canvas.width = textWidth * 2 + FONT_PREVIEW_OFFSET * 2;
   canvas.height = previewFontSize * 3;
 
   // we have to reset these after changing the canvas size
   ctx.font = fontValue;
   ctx.fillStyle = fillStyle;
 
   // Oversample the canvas for better text quality
   ctx.textBaseline = "top";
   ctx.scale(2, 2);
-  ctx.fillText(previewText, offset, Math.round(previewFontSize / 3));
+  ctx.fillText(previewText,
+               FONT_PREVIEW_OFFSET,
+               Math.round(previewFontSize / 3));
 
   let dataURL = canvas.toDataURL("image/png");
 
   return {
     dataURL: dataURL,
-    size: textWidth + offset * 2
+    size: textWidth + FONT_PREVIEW_OFFSET * 2
   };
 }
 
 exports.getFontPreviewData = getFontPreviewData;
--- a/toolkit/devtools/shared/timeline.js
+++ b/toolkit/devtools/shared/timeline.js
@@ -177,19 +177,16 @@ let Timeline = exports.Timeline = Class(
    *         will be created regardless (to hook into GC events), but this determines
    *         whether or not a `memory` event gets fired.
    * @option {boolean} withTicks
    *         Boolean indicating whether a `ticks` event is fired and a FramerateActor
    *         is created.
    */
   start: Task.async(function *({ withMemory, withTicks }) {
     let startTime = this._startTime = this.docShells[0].now();
-    // Store the start time from unix epoch so we can normalize
-    // markers from the memory actor
-    this._unixStartTime = Date.now();
 
     if (this._isRecording) {
       return startTime;
     }
 
     this._isRecording = true;
     this._stackFrames = new StackFrameCache();
     this._stackFrames.initFrames();
@@ -261,25 +258,22 @@ let Timeline = exports.Timeline = Class(
    * why there was a GC, and may contain a `nonincrementalReason` when SpiderMonkey could
    * not incrementally collect garbage.
    */
   _onGarbageCollection: function ({ collections, reason, nonincrementalReason }) {
     if (!this._isRecording || !this.docShells.length) {
       return;
     }
 
-    // Normalize the start time to docshell start time, and convert it
-    // to microseconds.
-    let startTime = (this._unixStartTime - this._startTime) * 1000;
     let endTime = this.docShells[0].now();
 
     events.emit(this, "markers", collections.map(({ startTimestamp: start, endTimestamp: end }) => {
       return {
         name: "GarbageCollection",
         causeName: reason,
         nonincrementalReason: nonincrementalReason,
         // Both timestamps are in microseconds -- convert to milliseconds to match other markers
-        start: (start - startTime) / 1000,
-        end: (end - startTime) / 1000
+        start: start,
+        end: end
       };
     }), endTime);
   },
 });
--- a/toolkit/mozapps/preferences/changemp.xul
+++ b/toolkit/mozapps/preferences/changemp.xul
@@ -9,54 +9,54 @@
 <!DOCTYPE dialog [
 <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
 <!ENTITY % changempDTD SYSTEM "chrome://mozapps/locale/preferences/changemp.dtd" >
 %brandDTD;
 %changempDTD;
 ]>
 
 <dialog id="changemp" title="&setPassword.title;"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" 
-        style="width: 35em;" 
-        ondialogaccept="setPassword();" 
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        style="width: 40em;" 
+        ondialogaccept="setPassword();"
         onload="init()">
 
   <script type="application/javascript" src="chrome://mozapps/content/preferences/changemp.js"/>
 
   <stringbundle id="bundlePreferences" src="chrome://mozapps/locale/preferences/preferences.properties"/>
 
   <description control="pw1">&masterPasswordDescription.label;</description>
 
   <groupbox>
     <grid>
       <columns>
+        <column flex="1"/>
         <column/>
-        <column/> 
       </columns>
       <rows>
         <row>
-          <label control="oldpw" value="&setPassword.oldPassword.label;"/> 
+          <label control="oldpw">&setPassword.oldPassword.label;</label>
           <textbox id="oldpw" type="password"/>
-          <!-- This textbox is inserted as a workaround to the fact that making the 'type' 
-                & 'disabled' property of the 'oldpw' textbox toggle between ['password' & 
-                'false'] and ['text' & 'true'] - as would be necessary if the menu has more 
-                than one tokens, some initialized and some not - does not work properly. So, 
-                either the textbox 'oldpw' or the textbox 'message' would be displayed, 
-                depending on the state of the token selected 
+          <!-- This textbox is inserted as a workaround to the fact that making the 'type'
+                & 'disabled' property of the 'oldpw' textbox toggle between ['password' &
+                'false'] and ['text' & 'true'] - as would be necessary if the menu has more
+                than one tokens, some initialized and some not - does not work properly. So,
+                either the textbox 'oldpw' or the textbox 'message' would be displayed,
+                depending on the state of the token selected
           -->
           <textbox id="message" disabled="true" />
         </row>
         <row>
-          <label control="pw1" value="&setPassword.newPassword.label;"/> 
-          <textbox id="pw1" type="password" 
-                   oninput="setPasswordStrength(); checkPasswords();"/> 
+          <label control="pw1">&setPassword.newPassword.label;</label>
+          <textbox id="pw1" type="password"
+                   oninput="setPasswordStrength(); checkPasswords();"/>
         </row>
         <row>
-          <label control="pw2" value="&setPassword.reenterPassword.label;"/> 
-          <textbox id="pw2" type="password" oninput="checkPasswords();"/>  
+          <label control="pw2">&setPassword.reenterPassword.label;</label>
+          <textbox id="pw2" type="password" oninput="checkPasswords();"/>
         </row>
       </rows>
     </grid>
   </groupbox>
 
   <groupbox>
     <caption label="&setPassword.meter.label;"/>
     <progressmeter id="pwmeter" mode="determined" value="0"/>