Merge fx-team to central, a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 21 Jul 2015 16:08:37 -0700
changeset 253975 8e5c888d0d896f06b90f5a6a68e84652aea85eb6
parent 253947 1875a5584e5fd29c5e32b179318c5591d1ae3304 (current diff)
parent 253974 8ef2ef7d849035fe7ec04935c9c1e37f022ea535 (diff)
child 253978 7262f2d6582e783c596df34b4c1743a3978015d1
child 253993 abe958c2ca538723ac9ab4782023871258dcd789
child 254034 b84cf2b696876ca920908e4c0d366c396c68db8f
push id29084
push userkwierso@gmail.com
push dateTue, 21 Jul 2015 23:08:41 +0000
treeherdermozilla-central@8e5c888d0d89 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone42.0a1
first release with
nightly linux32
8e5c888d0d89 / 42.0a1 / 20150722030205 / files
nightly linux64
8e5c888d0d89 / 42.0a1 / 20150722030205 / files
nightly mac
8e5c888d0d89 / 42.0a1 / 20150722030205 / files
nightly win32
8e5c888d0d89 / 42.0a1 / 20150722030205 / files
nightly win64
8e5c888d0d89 / 42.0a1 / 20150722030205 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to central, a=merge
browser/app/profile/firefox.js
browser/components/loop/content/shared/js/feedbackApiClient.js
browser/components/loop/content/shared/js/feedbackStore.js
browser/components/loop/content/shared/js/feedbackViews.js
browser/components/loop/content/shared/js/feedbackViews.jsx
browser/components/loop/test/shared/feedbackApiClient_test.js
browser/components/loop/test/shared/feedbackStore_test.js
browser/components/loop/test/shared/feedbackViews_test.js
browser/themes/windows/browser.css
layout/inspector/tests/test_bug1046140.html
mobile/android/base/resources/layout-large-v11/home_pager.xml
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1753,16 +1753,19 @@ pref("loop.ping.interval", 1800000);
 pref("loop.ping.timeout", 10000);
 pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
 pref("loop.feedback.product", "Loop");
 pref("loop.debug.loglevel", "Error");
 pref("loop.debug.dispatcher", false);
 pref("loop.debug.websocket", false);
 pref("loop.debug.sdk", false);
 pref("loop.debug.twoWayMediaTelemetry", false);
+pref("loop.feedback.dateLastSeenSec", 0);
+pref("loop.feedback.periodSec", 15770000); // 6 months.
+pref("loop.feedback.formURL", "http://www.surveygizmo.com/s3/2227372/Firefox-Hello-Product-Survey");
 #ifdef DEBUG
 pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
 #else
 pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
 #endif
 pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
 pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
 pref("loop.fxa_oauth.tokendata", "");
--- a/browser/components/loop/.eslintignore
+++ b/browser/components/loop/.eslintignore
@@ -12,15 +12,15 @@ standalone/node_modules
 # Libs we don't need to check
 test/shared/vendor
 # These are generated react files that we don't need to check
 content/js/contacts.js
 content/js/conversation.js
 content/js/conversationViews.js
 content/js/panel.js
 content/js/roomViews.js
-content/shared/js/feedbackViews.js
+content/js/feedbackViews.js
 content/shared/js/textChatView.js
 content/shared/js/views.js
 standalone/content/js/fxOSMarketplace.js
 standalone/content/js/standaloneRoomViews.js
 standalone/content/js/webapp.js
 ui/ui-showcase.js
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -22,29 +22,27 @@
     <script type="text/javascript" src="loop/libs/sdk.js"></script>
     <script type="text/javascript" src="loop/shared/libs/react-0.12.2.js"></script>
     <script type="text/javascript" src="loop/shared/libs/jquery-2.1.4.js"></script>
     <script type="text/javascript" src="loop/shared/libs/lodash-3.9.3.js"></script>
     <script type="text/javascript" src="loop/shared/libs/backbone-1.2.1.js"></script>
 
     <script type="text/javascript" src="loop/shared/js/utils.js"></script>
     <script type="text/javascript" src="loop/shared/js/mixins.js"></script>
-    <script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
     <script type="text/javascript" src="loop/shared/js/actions.js"></script>
     <script type="text/javascript" src="loop/shared/js/validate.js"></script>
     <script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
     <script type="text/javascript" src="loop/shared/js/otSdkDriver.js"></script>
     <script type="text/javascript" src="loop/shared/js/store.js"></script>
     <script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/roomStates.js"></script>
     <script type="text/javascript" src="loop/shared/js/fxOSActiveRoomStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
-    <script type="text/javascript" src="loop/shared/js/feedbackStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
-    <script type="text/javascript" src="loop/shared/js/feedbackViews.js"></script>
+    <script type="text/javascript" src="loop/js/feedbackViews.js"></script>
     <script type="text/javascript" src="loop/shared/js/textChatStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/textChatView.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
     <script type="text/javascript" src="loop/shared/js/websocket.js"></script>
     <script type="text/javascript" src="loop/js/conversationAppStore.js"></script>
     <script type="text/javascript" src="loop/js/client.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
     <script type="text/javascript" src="loop/js/roomStore.js"></script>
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -1,60 +1,91 @@
 /* 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.conversation = (function(mozL10n) {
   "use strict";
 
-  var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
-  var sharedModels = loop.shared.models;
   var sharedActions = loop.shared.actions;
 
   var CallControllerView = loop.conversationViews.CallControllerView;
-  var CallIdentifierView = loop.conversationViews.CallIdentifierView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
+  var FeedbackView = loop.feedbackViews.FeedbackView;
   var GenericFailureView = loop.conversationViews.GenericFailureView;
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * in progress, and hence, which view to display.
    */
   var AppControllerView = React.createClass({displayName: "AppControllerView",
     mixins: [
       Backbone.Events,
       loop.store.StoreMixin("conversationAppStore"),
+      sharedMixins.DocumentTitleMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       mozLoop: React.PropTypes.object.isRequired,
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
 
+    _renderFeedbackForm: function() {
+      this.setTitle(mozL10n.get("conversation_has_ended"));
+
+      return (React.createElement(FeedbackView, {
+        mozLoop: this.props.mozLoop, 
+        onAfterFeedbackReceived: this.closeWindow}));
+    },
+
+    /**
+     * We only show the feedback for once every 6 months, otherwise close
+     * the window.
+     */
+    handleCallTerminated: function() {
+      var delta = new Date() - new Date(this.state.feedbackTimestamp);
+
+      // Show timestamp if feedback period (6 months) passed.
+      // 0 is default value for pref. Always show feedback form on first use.
+      if (this.state.feedbackTimestamp === 0 ||
+          delta >= this.state.feedbackPeriod) {
+        this.props.dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
+        return;
+      }
+
+      this.closeWindow();
+    },
+
     render: function() {
+      if (this.state.showFeedbackForm) {
+        return this._renderFeedbackForm();
+      }
+
       switch(this.state.windowType) {
         // CallControllerView is used for both.
         case "incoming":
         case "outgoing": {
           return (React.createElement(CallControllerView, {
             dispatcher: this.props.dispatcher, 
-            mozLoop: this.props.mozLoop}));
+            mozLoop: this.props.mozLoop, 
+            onCallTerminated: this.handleCallTerminated}));
         }
         case "room": {
           return (React.createElement(DesktopRoomConversationView, {
             dispatcher: this.props.dispatcher, 
             mozLoop: this.props.mozLoop, 
+            onCallTerminated: this.handleCallTerminated, 
             roomStore: this.props.roomStore}));
         }
         case "failed": {
           return React.createElement(GenericFailureView, {cancelCall: this.closeWindow});
         }
         default: {
           // If we don't have a windowType, we don't know what we are yet,
           // so don't display anything.
@@ -97,25 +128,16 @@ loop.conversation = (function(mozL10n) {
       dispatcher: dispatcher,
       sdk: OT,
       mozLoop: navigator.mozLoop
     });
 
     // expose for functional tests
     loop.conversation._sdkDriver = sdkDriver;
 
-    var appVersionInfo = navigator.mozLoop.appVersionInfo;
-    var feedbackClient = new loop.FeedbackAPIClient(
-      navigator.mozLoop.getLoopPref("feedback.baseUrl"), {
-      product: navigator.mozLoop.getLoopPref("feedback.product"),
-      platform: appVersionInfo.OS,
-      channel: appVersionInfo.channel,
-      version: appVersionInfo.version
-    });
-
     // Create the stores.
     var conversationAppStore = new loop.store.ConversationAppStore({
       dispatcher: dispatcher,
       mozLoop: navigator.mozLoop
     });
     var conversationStore = new loop.store.ConversationStore(dispatcher, {
       client: client,
       isDesktop: true,
@@ -126,27 +148,23 @@ loop.conversation = (function(mozL10n) {
       isDesktop: true,
       mozLoop: navigator.mozLoop,
       sdkDriver: sdkDriver
     });
     var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
-    var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: feedbackClient
-    });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: sdkDriver
     });
 
     loop.store.StoreMixin.register({
       conversationAppStore: conversationAppStore,
       conversationStore: conversationStore,
-      feedbackStore: feedbackStore,
       textChatStore: textChatStore
     });
 
     // Obtain the windowId and pass it through
     var locationHash = loop.shared.utils.locationData().hash;
     var windowId;
 
     var hash = locationHash.match(/#(.*)/);
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -1,60 +1,91 @@
 /* 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.conversation = (function(mozL10n) {
   "use strict";
 
-  var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
-  var sharedModels = loop.shared.models;
   var sharedActions = loop.shared.actions;
 
   var CallControllerView = loop.conversationViews.CallControllerView;
-  var CallIdentifierView = loop.conversationViews.CallIdentifierView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
+  var FeedbackView = loop.feedbackViews.FeedbackView;
   var GenericFailureView = loop.conversationViews.GenericFailureView;
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * in progress, and hence, which view to display.
    */
   var AppControllerView = React.createClass({
     mixins: [
       Backbone.Events,
       loop.store.StoreMixin("conversationAppStore"),
+      sharedMixins.DocumentTitleMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       mozLoop: React.PropTypes.object.isRequired,
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
 
+    _renderFeedbackForm: function() {
+      this.setTitle(mozL10n.get("conversation_has_ended"));
+
+      return (<FeedbackView
+        mozLoop={this.props.mozLoop}
+        onAfterFeedbackReceived={this.closeWindow} />);
+    },
+
+    /**
+     * We only show the feedback for once every 6 months, otherwise close
+     * the window.
+     */
+    handleCallTerminated: function() {
+      var delta = new Date() - new Date(this.state.feedbackTimestamp);
+
+      // Show timestamp if feedback period (6 months) passed.
+      // 0 is default value for pref. Always show feedback form on first use.
+      if (this.state.feedbackTimestamp === 0 ||
+          delta >= this.state.feedbackPeriod) {
+        this.props.dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
+        return;
+      }
+
+      this.closeWindow();
+    },
+
     render: function() {
+      if (this.state.showFeedbackForm) {
+        return this._renderFeedbackForm();
+      }
+
       switch(this.state.windowType) {
         // CallControllerView is used for both.
         case "incoming":
         case "outgoing": {
           return (<CallControllerView
             dispatcher={this.props.dispatcher}
-            mozLoop={this.props.mozLoop} />);
+            mozLoop={this.props.mozLoop}
+            onCallTerminated={this.handleCallTerminated} />);
         }
         case "room": {
           return (<DesktopRoomConversationView
             dispatcher={this.props.dispatcher}
             mozLoop={this.props.mozLoop}
+            onCallTerminated={this.handleCallTerminated}
             roomStore={this.props.roomStore} />);
         }
         case "failed": {
           return <GenericFailureView cancelCall={this.closeWindow} />;
         }
         default: {
           // If we don't have a windowType, we don't know what we are yet,
           // so don't display anything.
@@ -97,25 +128,16 @@ loop.conversation = (function(mozL10n) {
       dispatcher: dispatcher,
       sdk: OT,
       mozLoop: navigator.mozLoop
     });
 
     // expose for functional tests
     loop.conversation._sdkDriver = sdkDriver;
 
-    var appVersionInfo = navigator.mozLoop.appVersionInfo;
-    var feedbackClient = new loop.FeedbackAPIClient(
-      navigator.mozLoop.getLoopPref("feedback.baseUrl"), {
-      product: navigator.mozLoop.getLoopPref("feedback.product"),
-      platform: appVersionInfo.OS,
-      channel: appVersionInfo.channel,
-      version: appVersionInfo.version
-    });
-
     // Create the stores.
     var conversationAppStore = new loop.store.ConversationAppStore({
       dispatcher: dispatcher,
       mozLoop: navigator.mozLoop
     });
     var conversationStore = new loop.store.ConversationStore(dispatcher, {
       client: client,
       isDesktop: true,
@@ -126,27 +148,23 @@ loop.conversation = (function(mozL10n) {
       isDesktop: true,
       mozLoop: navigator.mozLoop,
       sdkDriver: sdkDriver
     });
     var roomStore = new loop.store.RoomStore(dispatcher, {
       mozLoop: navigator.mozLoop,
       activeRoomStore: activeRoomStore
     });
-    var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: feedbackClient
-    });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: sdkDriver
     });
 
     loop.store.StoreMixin.register({
       conversationAppStore: conversationAppStore,
       conversationStore: conversationStore,
-      feedbackStore: feedbackStore,
       textChatStore: textChatStore
     });
 
     // Obtain the windowId and pass it through
     var locationHash = loop.shared.utils.locationData().hash;
     var windowId;
 
     var hash = locationHash.match(/#(.*)/);
--- a/browser/components/loop/content/js/conversationAppStore.js
+++ b/browser/components/loop/content/js/conversationAppStore.js
@@ -22,24 +22,36 @@ loop.store.ConversationAppStore = (funct
       throw new Error("Missing option dispatcher");
     }
     if (!options.mozLoop) {
       throw new Error("Missing option mozLoop");
     }
 
     this._dispatcher = options.dispatcher;
     this._mozLoop = options.mozLoop;
-    this._storeState = {};
+    this._storeState = this.getInitialStoreState();
 
     this._dispatcher.register(this, [
-      "getWindowData"
+      "getWindowData",
+      "showFeedbackForm"
     ]);
   };
 
   ConversationAppStore.prototype = _.extend({
+    getInitialStoreState: function() {
+      return {
+        // How often to display the form. Convert seconds to ms.
+        feedbackPeriod: this._mozLoop.getLoopPref("feedback.periodSec") * 1000,
+        // Date when the feedback form was last presented. Convert to ms.
+        feedbackTimestamp: this._mozLoop
+                               .getLoopPref("feedback.dateLastSeenSec") * 1000,
+        showFeedbackForm: false
+      };
+    },
+
     /**
      * Retrieves current store state.
      *
      * @return {Object}
      */
     getStoreState: function() {
       return this._storeState;
     },
@@ -50,16 +62,30 @@ loop.store.ConversationAppStore = (funct
      * @param {Object} state The new store state.
      */
     setStoreState: function(state) {
       this._storeState = state;
       this.trigger("change");
     },
 
     /**
+     * Sets store state which will result in the feedback form rendered.
+     * Saves a timestamp of when the feedback was last rendered.
+     */
+    showFeedbackForm: function() {
+      var timestamp = Math.floor(new Date().getTime() / 1000);
+
+      this._mozLoop.setLoopPref("feedback.dateLastSeenSec", timestamp);
+
+      this.setStoreState({
+        showFeedbackForm: true
+      });
+    },
+
+    /**
      * Handles the get window data action - obtains the window data,
      * updates the store and notifies interested components.
      *
      * @param {sharedActions.GetWindowData} actionData The action data
      */
     getWindowData: function(actionData) {
       var windowData = this._mozLoop.getConversationWindowData(actionData.windowId);
 
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -10,17 +10,16 @@ loop.conversationViews = (function(mozL1
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
   var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
   var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
-  var sharedModels = loop.shared.models;
 
   // This duplicates a similar function in contacts.jsx that isn't used in the
   // conversation window. If we get too many of these, we might want to consider
   // finding a logical place for them to be shared.
 
   // XXXdmose this code is already out of sync with the code in contacts.jsx
   // which, unlike this code, now has unit tests.  We should totally do the
   // above.
@@ -696,17 +695,18 @@ loop.conversationViews = (function(mozL1
       sharedMixins.AudioMixin,
       sharedMixins.DocumentTitleMixin,
       loop.store.StoreMixin("conversationStore"),
       Backbone.Events
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      mozLoop: React.PropTypes.object.isRequired
+      mozLoop: React.PropTypes.object.isRequired,
+      onCallTerminated: React.PropTypes.func.isRequired
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
 
     _closeWindow: function() {
       window.close();
@@ -715,29 +715,16 @@ loop.conversationViews = (function(mozL1
     /**
      * Returns true if the call is in a cancellable state, during call setup.
      */
     _isCancellable: function() {
       return this.state.callState !== CALL_STATES.INIT &&
              this.state.callState !== CALL_STATES.GATHER;
     },
 
-    /**
-     * Used to setup and render the feedback view.
-     */
-    _renderFeedbackView: function() {
-      this.setTitle(mozL10n.get("conversation_has_ended"));
-
-      return (
-        React.createElement(sharedViews.FeedbackView, {
-          onAfterFeedbackReceived: this._closeWindow.bind(this)}
-        )
-      );
-    },
-
     _renderViewFromCallType: function() {
       // For outgoing calls we can display the pending conversation view
       // for any state that render() doesn't manage.
       if (this.state.outgoing) {
         return (React.createElement(PendingConversationView, {
           callState: this.state.callState, 
           contact: this.state.contact, 
           dispatcher: this.props.dispatcher, 
@@ -755,16 +742,24 @@ loop.conversationViews = (function(mozL1
         ));
       }
 
       // Otherwise we're still gathering or connecting, so
       // don't display anything.
       return null;
     },
 
+    componentDidUpdate: function(prevProps, prevState) {
+      // Handle timestamp and window closing only when the call has terminated.
+      if (prevState.callState === CALL_STATES.ONGOING &&
+          this.state.callState === CALL_STATES.FINISHED) {
+        this.props.onCallTerminated();
+      }
+    },
+
     render: function() {
       // Set the default title to the contact name or the callerId, note
       // that views may override this, e.g. the feedback view.
       if (this.state.contact) {
         this.setTitle(_getContactDisplayName(this.state.contact));
       } else {
         this.setTitle(this.state.callerId || "");
       }
@@ -787,17 +782,20 @@ loop.conversationViews = (function(mozL1
             mediaConnected: this.state.mediaConnected, 
             remoteSrcVideoObject: this.state.remoteSrcVideoObject, 
             remoteVideoEnabled: this.state.remoteVideoEnabled, 
             video: {enabled: !this.state.videoMuted}})
           );
         }
         case CALL_STATES.FINISHED: {
           this.play("terminated");
-          return this._renderFeedbackView();
+
+          // When conversation ended we either display a feedback form or
+          // close the window. This is decided in the AppControllerView.
+          return null;
         }
         case CALL_STATES.INIT: {
           // We know what we are, but we haven't got the data yet.
           return null;
         }
         default: {
           return this._renderViewFromCallType();
         }
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -10,17 +10,16 @@ loop.conversationViews = (function(mozL1
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
   var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
   var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
-  var sharedModels = loop.shared.models;
 
   // This duplicates a similar function in contacts.jsx that isn't used in the
   // conversation window. If we get too many of these, we might want to consider
   // finding a logical place for them to be shared.
 
   // XXXdmose this code is already out of sync with the code in contacts.jsx
   // which, unlike this code, now has unit tests.  We should totally do the
   // above.
@@ -696,17 +695,18 @@ loop.conversationViews = (function(mozL1
       sharedMixins.AudioMixin,
       sharedMixins.DocumentTitleMixin,
       loop.store.StoreMixin("conversationStore"),
       Backbone.Events
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      mozLoop: React.PropTypes.object.isRequired
+      mozLoop: React.PropTypes.object.isRequired,
+      onCallTerminated: React.PropTypes.func.isRequired
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
 
     _closeWindow: function() {
       window.close();
@@ -715,29 +715,16 @@ loop.conversationViews = (function(mozL1
     /**
      * Returns true if the call is in a cancellable state, during call setup.
      */
     _isCancellable: function() {
       return this.state.callState !== CALL_STATES.INIT &&
              this.state.callState !== CALL_STATES.GATHER;
     },
 
-    /**
-     * Used to setup and render the feedback view.
-     */
-    _renderFeedbackView: function() {
-      this.setTitle(mozL10n.get("conversation_has_ended"));
-
-      return (
-        <sharedViews.FeedbackView
-          onAfterFeedbackReceived={this._closeWindow.bind(this)}
-        />
-      );
-    },
-
     _renderViewFromCallType: function() {
       // For outgoing calls we can display the pending conversation view
       // for any state that render() doesn't manage.
       if (this.state.outgoing) {
         return (<PendingConversationView
           callState={this.state.callState}
           contact={this.state.contact}
           dispatcher={this.props.dispatcher}
@@ -755,16 +742,24 @@ loop.conversationViews = (function(mozL1
         />);
       }
 
       // Otherwise we're still gathering or connecting, so
       // don't display anything.
       return null;
     },
 
+    componentDidUpdate: function(prevProps, prevState) {
+      // Handle timestamp and window closing only when the call has terminated.
+      if (prevState.callState === CALL_STATES.ONGOING &&
+          this.state.callState === CALL_STATES.FINISHED) {
+        this.props.onCallTerminated();
+      }
+    },
+
     render: function() {
       // Set the default title to the contact name or the callerId, note
       // that views may override this, e.g. the feedback view.
       if (this.state.contact) {
         this.setTitle(_getContactDisplayName(this.state.contact));
       } else {
         this.setTitle(this.state.callerId || "");
       }
@@ -787,17 +782,20 @@ loop.conversationViews = (function(mozL1
             mediaConnected={this.state.mediaConnected}
             remoteSrcVideoObject={this.state.remoteSrcVideoObject}
             remoteVideoEnabled={this.state.remoteVideoEnabled}
             video={{enabled: !this.state.videoMuted}} />
           );
         }
         case CALL_STATES.FINISHED: {
           this.play("terminated");
-          return this._renderFeedbackView();
+
+          // When conversation ended we either display a feedback form or
+          // close the window. This is decided in the AppControllerView.
+          return null;
         }
         case CALL_STATES.INIT: {
           // We know what we are, but we haven't got the data yet.
           return null;
         }
         default: {
           return this._renderViewFromCallType();
         }
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/js/feedbackViews.js
@@ -0,0 +1,47 @@
+var loop = loop || {};
+loop.feedbackViews = (function(_, mozL10n) {
+  "use strict";
+
+  /**
+   * Feedback view is displayed once every 6 months (loop.feedback.periodSec)
+   * after a conversation has ended.
+   */
+  var FeedbackView = React.createClass({displayName: "FeedbackView",
+    propTypes: {
+      mozLoop: React.PropTypes.object.isRequired,
+      onAfterFeedbackReceived: React.PropTypes.func.isRequired
+    },
+
+    /**
+     * Pressing the button to leave feedback will open the form in a new page
+     * and close the conversation window.
+     */
+    onFeedbackButtonClick: function() {
+      var url = this.props.mozLoop.getLoopPref("feedback.formURL");
+      this.props.mozLoop.openURL(url);
+
+      this.props.onAfterFeedbackReceived();
+    },
+
+    render: function() {
+      return (
+        React.createElement("div", {className: "feedback-view-container"}, 
+          React.createElement("h2", {className: "feedback-heading"}, 
+            mozL10n.get("feedback_window_heading")
+          ), 
+          React.createElement("div", {className: "feedback-hello-logo"}), 
+          React.createElement("div", {className: "feedback-button-container"}, 
+            React.createElement("button", {onClick: this.onFeedbackButtonClick, 
+              ref: "feedbackFormBtn"}, 
+              mozL10n.get("feedback_request_button")
+            )
+          )
+        )
+      );
+    }
+  });
+
+  return {
+    FeedbackView: FeedbackView
+  };
+})(_, navigator.mozL10n || document.mozL10n);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/js/feedbackViews.jsx
@@ -0,0 +1,47 @@
+var loop = loop || {};
+loop.feedbackViews = (function(_, mozL10n) {
+  "use strict";
+
+  /**
+   * Feedback view is displayed once every 6 months (loop.feedback.periodSec)
+   * after a conversation has ended.
+   */
+  var FeedbackView = React.createClass({
+    propTypes: {
+      mozLoop: React.PropTypes.object.isRequired,
+      onAfterFeedbackReceived: React.PropTypes.func.isRequired
+    },
+
+    /**
+     * Pressing the button to leave feedback will open the form in a new page
+     * and close the conversation window.
+     */
+    onFeedbackButtonClick: function() {
+      var url = this.props.mozLoop.getLoopPref("feedback.formURL");
+      this.props.mozLoop.openURL(url);
+
+      this.props.onAfterFeedbackReceived();
+    },
+
+    render: function() {
+      return (
+        <div className="feedback-view-container">
+          <h2 className="feedback-heading">
+            {mozL10n.get("feedback_window_heading")}
+          </h2>
+          <div className="feedback-hello-logo" />
+          <div className="feedback-button-container">
+            <button onClick={this.onFeedbackButtonClick}
+              ref="feedbackFormBtn">
+              {mozL10n.get("feedback_request_button")}
+            </button>
+          </div>
+        </div>
+      );
+    }
+  });
+
+  return {
+    FeedbackView: FeedbackView
+  };
+})(_, navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -553,16 +553,17 @@ loop.roomViews = (function(mozL10n) {
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       mozLoop: React.PropTypes.object.isRequired,
+      onCallTerminated: React.PropTypes.func.isRequired,
       remotePosterUrl: React.PropTypes.string,
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
 
     getInitialState: function() {
       return {
         contextEnabled: this.props.mozLoop.getLoopPref("contextInConversations.enabled"),
         showEditContext: false
@@ -688,16 +689,24 @@ loop.roomViews = (function(mozL10n) {
     handleEditContextClick: function() {
       this.setState({ showEditContext: !this.state.showEditContext });
     },
 
     handleEditContextClose: function() {
       this.setState({ showEditContext: false });
     },
 
+    componentDidUpdate: function(prevProps, prevState) {
+      // Handle timestamp and window closing only when the call has terminated.
+      if (prevState.roomState === ROOM_STATES.ENDED &&
+          this.state.roomState === ROOM_STATES.ENDED) {
+        this.props.onCallTerminated();
+      }
+    },
+
     render: function() {
       if (this.state.roomName) {
         this.setTitle(this.state.roomName);
       }
 
       var localStreamClasses = React.addons.classSet({
         local: true,
         "local-stream": true,
@@ -721,20 +730,19 @@ loop.roomViews = (function(mozL10n) {
           //       FULL case should never happen on desktop.
           return (
             React.createElement(loop.conversationViews.GenericFailureView, {
               cancelCall: this.closeWindow, 
               failureReason: this.state.failureReason})
           );
         }
         case ROOM_STATES.ENDED: {
-          return (
-            React.createElement(sharedViews.FeedbackView, {
-              onAfterFeedbackReceived: this.closeWindow})
-          );
+          // When conversation ended we either display a feedback form or
+          // close the window. This is decided in the AppControllerView.
+          return null;
         }
         default: {
 
           return (
             React.createElement("div", {className: "room-conversation-wrapper"}, 
               React.createElement("div", {className: "video-layout-wrapper"}, 
                 React.createElement("div", {className: "conversation room-conversation"}, 
                   React.createElement("div", {className: "media nested"}, 
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -553,16 +553,17 @@ loop.roomViews = (function(mozL10n) {
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // The poster URLs are for UI-showcase testing and development.
       localPosterUrl: React.PropTypes.string,
       mozLoop: React.PropTypes.object.isRequired,
+      onCallTerminated: React.PropTypes.func.isRequired,
       remotePosterUrl: React.PropTypes.string,
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
 
     getInitialState: function() {
       return {
         contextEnabled: this.props.mozLoop.getLoopPref("contextInConversations.enabled"),
         showEditContext: false
@@ -688,16 +689,24 @@ loop.roomViews = (function(mozL10n) {
     handleEditContextClick: function() {
       this.setState({ showEditContext: !this.state.showEditContext });
     },
 
     handleEditContextClose: function() {
       this.setState({ showEditContext: false });
     },
 
+    componentDidUpdate: function(prevProps, prevState) {
+      // Handle timestamp and window closing only when the call has terminated.
+      if (prevState.roomState === ROOM_STATES.ENDED &&
+          this.state.roomState === ROOM_STATES.ENDED) {
+        this.props.onCallTerminated();
+      }
+    },
+
     render: function() {
       if (this.state.roomName) {
         this.setTitle(this.state.roomName);
       }
 
       var localStreamClasses = React.addons.classSet({
         local: true,
         "local-stream": true,
@@ -721,20 +730,19 @@ loop.roomViews = (function(mozL10n) {
           //       FULL case should never happen on desktop.
           return (
             <loop.conversationViews.GenericFailureView
               cancelCall={this.closeWindow}
               failureReason={this.state.failureReason} />
           );
         }
         case ROOM_STATES.ENDED: {
-          return (
-            <sharedViews.FeedbackView
-              onAfterFeedbackReceived={this.closeWindow} />
-          );
+          // When conversation ended we either display a feedback form or
+          // close the window. This is decided in the AppControllerView.
+          return null;
         }
         default: {
 
           return (
             <div className="room-conversation-wrapper">
               <div className="video-layout-wrapper">
                 <div className="conversation room-conversation">
                   <div className="media nested">
--- a/browser/components/loop/content/shared/css/common.css
+++ b/browser/components/loop/content/shared/css/common.css
@@ -531,17 +531,17 @@ html[dir="rtl"] .context-content {
 }
 
 .context-wrapper > .context-preview {
   float: left;
   /* 16px is standard height/width for a favicon */
   width: 16px;
   max-height: 16px;
   margin-right: .8em;
-  flex: 0 1 auto;
+  flex: 0 0 auto;
 }
 
 html[dir="rtl"] .context-wrapper > .context-preview {
   float: left;
   margin-left: .8em;
   margin-right: 0;
 }
 
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -418,66 +418,57 @@
   margin: 2em 0;
 }
 
 .promote-firefox h3 {
   font-weight: 300;
 }
 
 /* Feedback form */
-
-.feedback {
-  padding: 14px;
-}
-
-.feedback p {
-  margin: 0px;
+.feedback-view-container {
+  display: flex;
+  flex-direction: column;
+  flex-wrap: nowrap;
+  justify-content: center;
+  align-content: center;
+  align-items: flex-start;
+  height: 100%;
 }
 
-.feedback h3 {
-  color: #666;
-  font-size: 12px;
-  font-weight: 700;
+.feedback-heading {
+  margin: 1em 0;
+  width: 100%;
   text-align: center;
-  margin: 0 0 1em 0;
-}
-
-.feedback .faces {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: center;
-  padding: 20px 0;
+  font-weight: bold;
+  font-size: 1.2em;
 }
 
-.feedback .face {
-  border: 1px solid transparent;
-  box-shadow: 0 1px 2px #CCC;
-  cursor: pointer;
-  border-radius: 4px;
-  margin: 0 10px;
-  width: 80px;
-  height: 80px;
-  background-color: #fbfbfb;
-  background-size: 60px auto;
-  background-position: center center;
+.feedback-hello-logo {
+  background-image: url("../img/helloicon.svg");
+  background-position: center;
+  background-size: contain;
   background-repeat: no-repeat;
+  flex: 2 1 auto;
+  width: 100%;
+  margin: 30px 0;
 }
 
-.feedback .face:hover {
-  border: 1px solid #DDD;
-  background-color: #FEFEFE;
+.feedback-button-container {
+  flex: 0 1 auto;
+  margin: 30px;
+  align-self: center;
 }
 
-.feedback .face.face-happy {
-  background-image: url("../img/happy.png");
-}
-
-.feedback .face.face-sad {
-  background-image: url("../img/sad.png");
+.feedback-button-container button {
+  margin: 0 30px;
+  padding: .5em 2em;
+  border: none;
+  background: #4E92DF;
+  color: #fff;
+  cursor: pointer;
 }
 
 .fx-embedded-btn-back {
   margin-bottom: 1rem;
   padding: .2rem .8rem;
   border: 1px solid #aaa;
   border-radius: 2px;
   background: transparent;
@@ -1542,34 +1533,29 @@ html[dir="rtl"] .text-chat-entry.receive
 }
 
 /* Text chat entry timestamp */
 .text-chat-entry-timestamp {
   margin: 0 .5em;
   color: #aaa;
   font-style: italic;
   font-size: .8em;
-  order: 0;
   flex: 0 1 auto;
   align-self: center;
 }
 
 /* Sent text chat entries should be on the right */
 .text-chat-entry.sent {
   justify-content: flex-end;
 }
 
 .received > .text-chat-entry-timestamp {
   order: 2;
 }
 
-.sent > .text-chat-entry-timestamp {
-  order: 0;
-}
-
 /* Pseudo element used to cover part between chat bubble and chat arrow. */
 .text-chat-entry > p:after {
   position: absolute;
   background: #fff;
   content: "";
 }
 
 .text-chat-entry.sent > p:after {
@@ -1626,17 +1612,16 @@ html[dir="rtl"] .text-chat-entry.receive
   align-self: flex-end;
 }
 
 .text-chat-entry.received .text-chat-arrow {
   margin-left: 0;
   margin-right: -9px;
   height: 10px;
   background-image: url("../img/chatbubble-arrow-left.svg");
-  order: 0;
   align-self: auto;
 }
 
 html[dir="rtl"] .text-chat-arrow {
   transform: scaleX(-1);
 }
 
 html[dir="rtl"] .text-chat-entry.sent .text-chat-arrow {
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/helloicon.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill-rule="evenodd" clip-rule="evenodd" fill="#4E92DF" d="M32 0C14.3 0 0 12.6 0 28.1c0 7.7 3.6 14.7 9.3 19.8-1 3.5-3 8.3-6.9 12.9.7 1.2 11.7-3 19.4-6.1 3.2.9 6.6 1.5 10.2 1.5 17.7 0 32-12.6 32-28.1S49.7 0 32 0zm9.6 16.9c2.3 0 4.2 1.9 4.2 4.2 0 2.3-1.9 4.2-4.2 4.2-2.3 0-4.2-1.9-4.2-4.2-.1-2.3 1.8-4.2 4.2-4.2zm-19.3 0c2.3 0 4.2 1.9 4.2 4.2 0 2.3-1.9 4.2-4.2 4.2-2.3 0-4.2-1.9-4.2-4.2-.1-2.3 1.8-4.2 4.2-4.2zM32 47.7h-.1-.1c-8.6 0-18.1-5.5-20.3-14.9 5.8 2.7 13.8 3.8 20.4 3.8 6.6 0 14.7-1.2 20.4-3.8-2.2 9.3-11.7 14.9-20.3 14.9z"/></svg>
\ No newline at end of file
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -516,62 +516,39 @@ loop.shared.actions = (function() {
     JoinedRoom: Action.define("joinedRoom", {
       apiKey: String,
       sessionToken: String,
       sessionId: String,
       expires: Number
     }),
 
     /**
-     * Used to indicate that the feedback cycle is completed and the countdown
-     * finished.
+     * Used to indicate the user wishes to leave the room.
      */
-    FeedbackComplete: Action.define("feedbackComplete", {
+    LeaveRoom: Action.define("leaveRoom", {
     }),
 
     /**
-     * Used to indicate the user wishes to leave the room.
+     * Signals that the feedback view should be rendered.
      */
-    LeaveRoom: Action.define("leaveRoom", {
+    ShowFeedbackForm: Action.define("showFeedbackForm", {
     }),
 
     /**
      * Used to record a link click for metrics purposes.
      */
     RecordClick: Action.define("recordClick", {
       // Note: for ToS and Privacy links, this should be the link, for
       // other links this should be a generic description so that we don't
       // record what users are clicking, just the information about the fact
       // they clicked the link in that spot (e.g. "Shared URL").
       linkInfo: String
     }),
 
     /**
-     * Requires detailed information on sad feedback.
-     */
-    RequireFeedbackDetails: Action.define("requireFeedbackDetails", {
-    }),
-
-    /**
-     * Send feedback data.
-     */
-    SendFeedback: Action.define("sendFeedback", {
-      happy: Boolean,
-      category: String,
-      description: String
-    }),
-
-    /**
-     * Reacts on feedback submission error.
-     */
-    SendFeedbackError: Action.define("sendFeedbackError", {
-      error: Error
-    }),
-
-    /**
      * Used to inform of the current session, publisher and connection
      * status.
      */
     ConnectionStatus: Action.define("connectionStatus", {
       event: String,
       state: String,
       connections: Number,
       sendStreams: Number,
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/feedbackApiClient.js
+++ /dev/null
@@ -1,115 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var loop = loop || {};
-loop.FeedbackAPIClient = (function($, _) {
-  "use strict";
-
-  /**
-   * Feedback API client. Sends feedback data to an input.mozilla.com compatible
-   * API.
-   *
-   * @param {String} baseUrl  Base API url (required)
-   * @param {Object} defaults Defaults field values for that client.
-   *
-   * Required defaults:
-   * - {String} product Product name (required)
-   *
-   * Optional defaults:
-   * - {String} platform   Platform name, eg. "Windows 8", "Android", "Linux"
-   * - {String} version    Product version, eg. "22b2", "1.1"
-   * - {String} channel    Product channel, eg. "stable", "beta"
-   * - {String} user_agent eg. Mozilla/5.0 (Mobile; rv:18.0) Gecko/18.0 Firefox/18.0
-   *
-   * @link  http://fjord.readthedocs.org/en/latest/api.html
-   */
-  function FeedbackAPIClient(baseUrl, defaults) {
-    this.baseUrl = baseUrl;
-    if (!this.baseUrl) {
-      throw new Error("Missing required 'baseUrl' argument.");
-    }
-
-    this.defaults = defaults || {};
-    // required defaults checks
-    if (!this.defaults.hasOwnProperty("product")) {
-      throw new Error("Missing required 'product' default.");
-    }
-  }
-
-  FeedbackAPIClient.prototype = {
-    /**
-     * Supported field names by the feedback API.
-     * @type {Array}
-     */
-    _supportedFields: ["happy",
-                       "category",
-                       "description",
-                       "product",
-                       "platform",
-                       "version",
-                       "channel",
-                       "user_agent",
-                       "url"],
-
-    /**
-     * Creates a formatted payload object compliant with the Feedback API spec
-     * against validated field data.
-     *
-     * @param  {Object} fields Feedback initial values.
-     * @return {Object}        Formatted payload object.
-     * @throws {Error}         If provided values are invalid
-     */
-    _createPayload: function(fields) {
-      if (typeof fields !== "object") {
-        throw new Error("Invalid feedback data provided.");
-      }
-
-      Object.keys(fields).forEach(function(name) {
-        if (this._supportedFields.indexOf(name) === -1) {
-          throw new Error("Unsupported field " + name);
-        }
-      }, this);
-
-      // Payload is basically defaults + fields merged in
-      var payload = _.extend({}, this.defaults, fields);
-
-      // Default description field value
-      if (!fields.description) {
-        payload.description = (fields.happy ? "Happy" : "Sad") + " User";
-      }
-
-      return payload;
-    },
-
-    /**
-     * Sends feedback data.
-     *
-     * @param  {Object}   fields Feedback form data.
-     * @param  {Function} cb     Callback(err, result)
-     */
-    send: function(fields, cb) {
-      var req = $.ajax({
-        url: this.baseUrl,
-        method: "POST",
-        contentType: "application/json",
-        dataType: "json",
-        data: JSON.stringify(this._createPayload(fields))
-      });
-
-      req.done(function(result) {
-        console.info("User feedback data have been submitted", result);
-        cb(null, result);
-      });
-
-      req.fail(function(jqXHR, textStatus, errorThrown) {
-        var message = "Error posting user feedback data";
-        var httpError = jqXHR.status + " " + errorThrown;
-        cb(new Error(message + ": " + httpError + "; " +
-                     (jqXHR.responseJSON && jqXHR.responseJSON.detail || "")));
-      });
-    }
-  };
-
-  return FeedbackAPIClient;
-})(jQuery, _);
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/feedbackStore.js
+++ /dev/null
@@ -1,105 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var loop = loop || {};
-loop.store = loop.store || {};
-
-loop.store.FeedbackStore = (function() {
-  "use strict";
-
-  var sharedActions = loop.shared.actions;
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES = {
-    // Initial state (mood selection)
-    INIT: "feedback-init",
-    // User detailed feedback form step
-    DETAILS: "feedback-details",
-    // Pending feedback data submission
-    PENDING: "feedback-pending",
-    // Feedback has been sent
-    SENT: "feedback-sent",
-    // There was an issue with the feedback API
-    FAILED: "feedback-failed"
-  };
-
-  /**
-   * Feedback store.
-   *
-   * @param {loop.Dispatcher} dispatcher  The dispatcher for dispatching actions
-   *                                      and registering to consume actions.
-   * @param {Object} options Options object:
-   * - {mozLoop}        mozLoop                 The MozLoop API object.
-   * - {feedbackClient} loop.FeedbackAPIClient  The feedback API client.
-   */
-  var FeedbackStore = loop.store.createStore({
-    actions: [
-      "requireFeedbackDetails",
-      "sendFeedback",
-      "sendFeedbackError",
-      "feedbackComplete"
-    ],
-
-    initialize: function(options) {
-      if (!options.feedbackClient) {
-        throw new Error("Missing option feedbackClient");
-      }
-      this._feedbackClient = options.feedbackClient;
-    },
-
-    /**
-     * Returns initial state data for this active room.
-     */
-    getInitialStoreState: function() {
-      return {feedbackState: FEEDBACK_STATES.INIT};
-    },
-
-    /**
-     * Requires user detailed feedback.
-     */
-    requireFeedbackDetails: function() {
-      this.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
-    },
-
-    /**
-     * Sends feedback data to the feedback server.
-     *
-     * @param {sharedActions.SendFeedback} actionData The action data.
-     */
-    sendFeedback: function(actionData) {
-      delete actionData.name;
-      this._feedbackClient.send(actionData, function(err) {
-        if (err) {
-          this.dispatchAction(new sharedActions.SendFeedbackError({
-            error: err
-          }));
-          return;
-        }
-        this.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
-      }.bind(this));
-
-      this.setStoreState({feedbackState: FEEDBACK_STATES.PENDING});
-    },
-
-    /**
-     * Notifies a store from any error encountered while sending feedback data.
-     *
-     * @param {sharedActions.SendFeedback} actionData The action data.
-     */
-    sendFeedbackError: function(actionData) {
-      this.setStoreState({
-        feedbackState: FEEDBACK_STATES.FAILED,
-        error: actionData.error
-      });
-    },
-
-    /**
-     * Resets the store to its initial state as feedback has been completed,
-     * i.e. ready for the next round of feedback.
-     */
-    feedbackComplete: function() {
-      this.resetStoreState();
-    }
-  });
-
-  return FeedbackStore;
-})();
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/feedbackViews.js
+++ /dev/null
@@ -1,323 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var loop = loop || {};
-loop.shared = loop.shared || {};
-loop.shared.views = loop.shared.views || {};
-loop.shared.views.FeedbackView = (function(l10n) {
-  "use strict";
-
-  var sharedActions = loop.shared.actions;
-  var sharedMixins = loop.shared.mixins;
-
-  var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS =
-      loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
-
-  /**
-   * Feedback outer layout.
-   *
-   * Props:
-   * -
-   */
-  var FeedbackLayout = React.createClass({displayName: "FeedbackLayout",
-    propTypes: {
-      children: React.PropTypes.element,
-      reset: React.PropTypes.func, // if not specified, no Back btn is shown
-      title: React.PropTypes.string.isRequired
-    },
-
-    render: function() {
-      var backButton = React.createElement("div", null);
-      if (this.props.reset) {
-        backButton = (
-          React.createElement("button", {className: "fx-embedded-btn-back", 
-            onClick: this.props.reset, 
-            type: "button"}, 
-            "« ", l10n.get("feedback_back_button")
-          )
-        );
-      }
-      return (
-        React.createElement("div", {className: "feedback"}, 
-          backButton, 
-          React.createElement("h3", null, this.props.title), 
-          this.props.children
-        )
-      );
-    }
-  });
-
-  /**
-   * Detailed feedback form.
-   */
-  var FeedbackForm = React.createClass({displayName: "FeedbackForm",
-    propTypes: {
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
-      pending: React.PropTypes.bool,
-      reset: React.PropTypes.func
-    },
-
-    getInitialState: function() {
-      return {category: "", description: ""};
-    },
-
-    getDefaultProps: function() {
-      return {pending: false};
-    },
-
-    _getCategories: function() {
-      return {
-        audio_quality: l10n.get("feedback_category_audio_quality"),
-        video_quality: l10n.get("feedback_category_video_quality"),
-        disconnected: l10n.get("feedback_category_was_disconnected"),
-        confusing: l10n.get("feedback_category_confusing2"),
-        other: l10n.get("feedback_category_other2")
-      };
-    },
-
-    _getCategoryFields: function() {
-      var categories = this._getCategories();
-      return Object.keys(categories).map(function(category, key) {
-        return (
-          React.createElement("label", {className: "feedback-category-label", key: key}, 
-            React.createElement("input", {
-              checked: this.state.category === category, 
-              className: "feedback-category-radio", 
-              name: "category", 
-              onChange: this.handleCategoryChange, 
-              ref: "category", 
-              type: "radio", 
-              value: category}), 
-            categories[category]
-          )
-        );
-      }, this);
-    },
-
-    /**
-     * Checks if the form is ready for submission:
-     *
-     * - no feedback submission should be pending.
-     * - a category (reason) must be chosen;
-     * - if the "other" category is chosen, a custom description must have been
-     *   entered by the end user;
-     *
-     * @return {Boolean}
-     */
-    _isFormReady: function() {
-      if (this.props.pending || !this.state.category) {
-        return false;
-      }
-      if (this.state.category === "other" && !this.state.description) {
-        return false;
-      }
-      return true;
-    },
-
-    handleCategoryChange: function(event) {
-      var category = event.target.value;
-      this.setState({
-        category: category
-      });
-      if (category == "other") {
-        this.refs.description.getDOMNode().focus();
-      }
-    },
-
-    handleDescriptionFieldChange: function(event) {
-      this.setState({description: event.target.value});
-    },
-
-    handleFormSubmit: function(event) {
-      event.preventDefault();
-      // XXX this feels ugly, we really want a feedbackActions object here.
-      this.props.feedbackStore.dispatchAction(new sharedActions.SendFeedback({
-        happy: false,
-        category: this.state.category,
-        description: this.state.description
-      }));
-    },
-
-    render: function() {
-      return (
-        React.createElement(FeedbackLayout, {
-          reset: this.props.reset, 
-          title: l10n.get("feedback_category_list_heading")}, 
-          React.createElement("form", {onSubmit: this.handleFormSubmit}, 
-            this._getCategoryFields(), 
-            React.createElement("p", null, 
-              React.createElement("input", {className: "feedback-description", 
-                name: "description", 
-                onChange: this.handleDescriptionFieldChange, 
-                placeholder: 
-                  l10n.get("feedback_custom_category_text_placeholder"), 
-                ref: "description", 
-                type: "text", 
-                value: this.state.description})
-            ), 
-            React.createElement("button", {className: "btn btn-success", 
-              disabled: !this._isFormReady(), 
-              type: "submit"}, 
-              l10n.get("feedback_submit_button")
-            )
-          )
-        )
-      );
-    }
-  });
-
-  /**
-   * Feedback received view.
-   *
-   * Props:
-   * - {Function} onAfterFeedbackReceived Function to execute after the
-   *   WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed
-   */
-  var FeedbackReceived = React.createClass({displayName: "FeedbackReceived",
-    propTypes: {
-      noCloseText: React.PropTypes.bool,
-      onAfterFeedbackReceived: React.PropTypes.func
-    },
-
-    getInitialState: function() {
-      return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
-    },
-
-    componentDidMount: function() {
-      this._timer = setInterval(function() {
-      if (this.state.countdown == 1) {
-        clearInterval(this._timer);
-        if (this.props.onAfterFeedbackReceived) {
-          this.props.onAfterFeedbackReceived();
-        }
-        return;
-      }
-        this.setState({countdown: this.state.countdown - 1});
-      }.bind(this), 1000);
-    },
-
-    componentWillUnmount: function() {
-      if (this._timer) {
-        clearInterval(this._timer);
-      }
-    },
-
-    _renderCloseText: function() {
-      if (this.props.noCloseText) {
-        return null;
-      }
-
-      return (
-        React.createElement("p", {className: "info thank-you"}, 
-          l10n.get("feedback_window_will_close_in2", {
-            countdown: this.state.countdown,
-            num: this.state.countdown
-          }))
-      );
-    },
-
-    render: function() {
-      return (
-        React.createElement(FeedbackLayout, {title: l10n.get("feedback_thank_you_heading")}, 
-          this._renderCloseText()
-        )
-      );
-    }
-  });
-
-  /**
-   * Feedback view.
-   */
-  var FeedbackView = React.createClass({displayName: "FeedbackView",
-    mixins: [
-      Backbone.Events,
-      loop.store.StoreMixin("feedbackStore")
-    ],
-
-    propTypes: {
-      // Used by the UI showcase.
-      feedbackState: React.PropTypes.string,
-      noCloseText: React.PropTypes.bool,
-      onAfterFeedbackReceived: React.PropTypes.func
-    },
-
-    getInitialState: function() {
-      var storeState = this.getStoreState();
-      return _.extend({}, storeState, {
-        feedbackState: this.props.feedbackState || storeState.feedbackState
-      });
-    },
-
-    reset: function() {
-      this.setState(this.getStore().getInitialStoreState());
-    },
-
-    handleHappyClick: function() {
-      // XXX: If the user is happy, we directly send this information to the
-      //      feedback API; this is a behavior we might want to revisit later.
-      this.getStore().dispatchAction(new sharedActions.SendFeedback({
-        happy: true,
-        category: "",
-        description: ""
-      }));
-    },
-
-    handleSadClick: function() {
-      this.getStore().dispatchAction(
-        new sharedActions.RequireFeedbackDetails());
-    },
-
-    _onFeedbackSent: function(err) {
-      if (err) {
-        // XXX better end user error reporting, see bug 1046738
-        console.error("Unable to send user feedback", err);
-      }
-      this.setState({pending: false, step: "finished"});
-    },
-
-    render: function() {
-      switch(this.state.feedbackState) {
-        default:
-        case FEEDBACK_STATES.INIT: {
-          return (
-            React.createElement(FeedbackLayout, {title: 
-              l10n.get("feedback_call_experience_heading2")}, 
-              React.createElement("div", {className: "faces"}, 
-                React.createElement("button", {className: "face face-happy", 
-                        onClick: this.handleHappyClick}), 
-                React.createElement("button", {className: "face face-sad", 
-                        onClick: this.handleSadClick})
-              )
-            )
-          );
-        }
-        case FEEDBACK_STATES.DETAILS: {
-          return (
-            React.createElement(FeedbackForm, {
-              feedbackStore: this.getStore(), 
-              pending: this.state.feedbackState === FEEDBACK_STATES.PENDING, 
-              reset: this.reset})
-            );
-        }
-        case FEEDBACK_STATES.PENDING:
-        case FEEDBACK_STATES.SENT:
-        case FEEDBACK_STATES.FAILED: {
-          if (this.state.error) {
-            // XXX better end user error reporting, see bug 1046738
-            console.error("Error encountered while submitting feedback",
-                          this.state.error);
-          }
-          return (
-            React.createElement(FeedbackReceived, {
-              noCloseText: this.props.noCloseText, 
-              onAfterFeedbackReceived: this.props.onAfterFeedbackReceived})
-          );
-        }
-      }
-    }
-  });
-
-  return FeedbackView;
-})(navigator.mozL10n || document.mozL10n);
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/feedbackViews.jsx
+++ /dev/null
@@ -1,323 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var loop = loop || {};
-loop.shared = loop.shared || {};
-loop.shared.views = loop.shared.views || {};
-loop.shared.views.FeedbackView = (function(l10n) {
-  "use strict";
-
-  var sharedActions = loop.shared.actions;
-  var sharedMixins = loop.shared.mixins;
-
-  var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS =
-      loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
-
-  /**
-   * Feedback outer layout.
-   *
-   * Props:
-   * -
-   */
-  var FeedbackLayout = React.createClass({
-    propTypes: {
-      children: React.PropTypes.element,
-      reset: React.PropTypes.func, // if not specified, no Back btn is shown
-      title: React.PropTypes.string.isRequired
-    },
-
-    render: function() {
-      var backButton = <div />;
-      if (this.props.reset) {
-        backButton = (
-          <button className="fx-embedded-btn-back"
-            onClick={this.props.reset}
-            type="button" >
-            &laquo;&nbsp;{l10n.get("feedback_back_button")}
-          </button>
-        );
-      }
-      return (
-        <div className="feedback">
-          {backButton}
-          <h3>{this.props.title}</h3>
-          {this.props.children}
-        </div>
-      );
-    }
-  });
-
-  /**
-   * Detailed feedback form.
-   */
-  var FeedbackForm = React.createClass({
-    propTypes: {
-      feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
-      pending: React.PropTypes.bool,
-      reset: React.PropTypes.func
-    },
-
-    getInitialState: function() {
-      return {category: "", description: ""};
-    },
-
-    getDefaultProps: function() {
-      return {pending: false};
-    },
-
-    _getCategories: function() {
-      return {
-        audio_quality: l10n.get("feedback_category_audio_quality"),
-        video_quality: l10n.get("feedback_category_video_quality"),
-        disconnected: l10n.get("feedback_category_was_disconnected"),
-        confusing: l10n.get("feedback_category_confusing2"),
-        other: l10n.get("feedback_category_other2")
-      };
-    },
-
-    _getCategoryFields: function() {
-      var categories = this._getCategories();
-      return Object.keys(categories).map(function(category, key) {
-        return (
-          <label className="feedback-category-label" key={key}>
-            <input
-              checked={this.state.category === category}
-              className="feedback-category-radio"
-              name="category"
-              onChange={this.handleCategoryChange}
-              ref="category"
-              type="radio"
-              value={category} />
-            {categories[category]}
-          </label>
-        );
-      }, this);
-    },
-
-    /**
-     * Checks if the form is ready for submission:
-     *
-     * - no feedback submission should be pending.
-     * - a category (reason) must be chosen;
-     * - if the "other" category is chosen, a custom description must have been
-     *   entered by the end user;
-     *
-     * @return {Boolean}
-     */
-    _isFormReady: function() {
-      if (this.props.pending || !this.state.category) {
-        return false;
-      }
-      if (this.state.category === "other" && !this.state.description) {
-        return false;
-      }
-      return true;
-    },
-
-    handleCategoryChange: function(event) {
-      var category = event.target.value;
-      this.setState({
-        category: category
-      });
-      if (category == "other") {
-        this.refs.description.getDOMNode().focus();
-      }
-    },
-
-    handleDescriptionFieldChange: function(event) {
-      this.setState({description: event.target.value});
-    },
-
-    handleFormSubmit: function(event) {
-      event.preventDefault();
-      // XXX this feels ugly, we really want a feedbackActions object here.
-      this.props.feedbackStore.dispatchAction(new sharedActions.SendFeedback({
-        happy: false,
-        category: this.state.category,
-        description: this.state.description
-      }));
-    },
-
-    render: function() {
-      return (
-        <FeedbackLayout
-          reset={this.props.reset}
-          title={l10n.get("feedback_category_list_heading")}>
-          <form onSubmit={this.handleFormSubmit}>
-            {this._getCategoryFields()}
-            <p>
-              <input className="feedback-description"
-                name="description"
-                onChange={this.handleDescriptionFieldChange}
-                placeholder={
-                  l10n.get("feedback_custom_category_text_placeholder")}
-                ref="description"
-                type="text"
-                value={this.state.description} />
-            </p>
-            <button className="btn btn-success"
-              disabled={!this._isFormReady()}
-              type="submit">
-              {l10n.get("feedback_submit_button")}
-            </button>
-          </form>
-        </FeedbackLayout>
-      );
-    }
-  });
-
-  /**
-   * Feedback received view.
-   *
-   * Props:
-   * - {Function} onAfterFeedbackReceived Function to execute after the
-   *   WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed
-   */
-  var FeedbackReceived = React.createClass({
-    propTypes: {
-      noCloseText: React.PropTypes.bool,
-      onAfterFeedbackReceived: React.PropTypes.func
-    },
-
-    getInitialState: function() {
-      return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
-    },
-
-    componentDidMount: function() {
-      this._timer = setInterval(function() {
-      if (this.state.countdown == 1) {
-        clearInterval(this._timer);
-        if (this.props.onAfterFeedbackReceived) {
-          this.props.onAfterFeedbackReceived();
-        }
-        return;
-      }
-        this.setState({countdown: this.state.countdown - 1});
-      }.bind(this), 1000);
-    },
-
-    componentWillUnmount: function() {
-      if (this._timer) {
-        clearInterval(this._timer);
-      }
-    },
-
-    _renderCloseText: function() {
-      if (this.props.noCloseText) {
-        return null;
-      }
-
-      return (
-        <p className="info thank-you">{
-          l10n.get("feedback_window_will_close_in2", {
-            countdown: this.state.countdown,
-            num: this.state.countdown
-          })}</p>
-      );
-    },
-
-    render: function() {
-      return (
-        <FeedbackLayout title={l10n.get("feedback_thank_you_heading")}>
-          {this._renderCloseText()}
-        </FeedbackLayout>
-      );
-    }
-  });
-
-  /**
-   * Feedback view.
-   */
-  var FeedbackView = React.createClass({
-    mixins: [
-      Backbone.Events,
-      loop.store.StoreMixin("feedbackStore")
-    ],
-
-    propTypes: {
-      // Used by the UI showcase.
-      feedbackState: React.PropTypes.string,
-      noCloseText: React.PropTypes.bool,
-      onAfterFeedbackReceived: React.PropTypes.func
-    },
-
-    getInitialState: function() {
-      var storeState = this.getStoreState();
-      return _.extend({}, storeState, {
-        feedbackState: this.props.feedbackState || storeState.feedbackState
-      });
-    },
-
-    reset: function() {
-      this.setState(this.getStore().getInitialStoreState());
-    },
-
-    handleHappyClick: function() {
-      // XXX: If the user is happy, we directly send this information to the
-      //      feedback API; this is a behavior we might want to revisit later.
-      this.getStore().dispatchAction(new sharedActions.SendFeedback({
-        happy: true,
-        category: "",
-        description: ""
-      }));
-    },
-
-    handleSadClick: function() {
-      this.getStore().dispatchAction(
-        new sharedActions.RequireFeedbackDetails());
-    },
-
-    _onFeedbackSent: function(err) {
-      if (err) {
-        // XXX better end user error reporting, see bug 1046738
-        console.error("Unable to send user feedback", err);
-      }
-      this.setState({pending: false, step: "finished"});
-    },
-
-    render: function() {
-      switch(this.state.feedbackState) {
-        default:
-        case FEEDBACK_STATES.INIT: {
-          return (
-            <FeedbackLayout title={
-              l10n.get("feedback_call_experience_heading2")}>
-              <div className="faces">
-                <button className="face face-happy"
-                        onClick={this.handleHappyClick}></button>
-                <button className="face face-sad"
-                        onClick={this.handleSadClick}></button>
-              </div>
-            </FeedbackLayout>
-          );
-        }
-        case FEEDBACK_STATES.DETAILS: {
-          return (
-            <FeedbackForm
-              feedbackStore={this.getStore()}
-              pending={this.state.feedbackState === FEEDBACK_STATES.PENDING}
-              reset={this.reset} />
-            );
-        }
-        case FEEDBACK_STATES.PENDING:
-        case FEEDBACK_STATES.SENT:
-        case FEEDBACK_STATES.FAILED: {
-          if (this.state.error) {
-            // XXX better end user error reporting, see bug 1046738
-            console.error("Error encountered while submitting feedback",
-                          this.state.error);
-          }
-          return (
-            <FeedbackReceived
-              noCloseText={this.props.noCloseText}
-              onAfterFeedbackReceived={this.props.onAfterFeedbackReceived} />
-          );
-        }
-      }
-    }
-  });
-
-  return FeedbackView;
-})(navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -17,17 +17,16 @@ loop.shared.mixins = (function() {
    * Sets a new root object.  This is useful for testing native DOM events so we
    * can fake them. In beforeEach(), loop.shared.mixins.setRootObject is used to
    * substitute a fake window, and in afterEach(), the real window object is
    * replaced.
    *
    * @param {Object}
    */
   function setRootObject(obj) {
-    // console.log("loop.shared.mixins: rootObject set to " + obj);
     rootObject = obj;
   }
 
   /**
    * window.location mixin. Handles changes in the call url.
    * Forces a reload of the page to ensure proper state of the webapp
    *
    * @type {Object}
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -15,29 +15,29 @@ browser.jar:
   content/browser/loop/js/conversation.js           (content/js/conversation.js)
   content/browser/loop/js/conversationAppStore.js   (content/js/conversationAppStore.js)
   content/browser/loop/js/otconfig.js               (content/js/otconfig.js)
   content/browser/loop/js/panel.js                  (content/js/panel.js)
   content/browser/loop/js/contacts.js               (content/js/contacts.js)
   content/browser/loop/js/conversationViews.js      (content/js/conversationViews.js)
   content/browser/loop/js/roomStore.js              (content/js/roomStore.js)
   content/browser/loop/js/roomViews.js              (content/js/roomViews.js)
+  content/browser/loop/js/feedbackViews.js          (content/js/feedbackViews.js)
 
   # Desktop styles
   content/browser/loop/css/contacts.css             (content/css/contacts.css)
   content/browser/loop/css/panel.css                (content/css/panel.css)
 
   # Shared styles
   content/browser/loop/shared/css/reset.css         (content/shared/css/reset.css)
   content/browser/loop/shared/css/common.css        (content/shared/css/common.css)
   content/browser/loop/shared/css/conversation.css  (content/shared/css/conversation.css)
 
   # Shared images
-  content/browser/loop/shared/img/happy.png                     (content/shared/img/happy.png)
-  content/browser/loop/shared/img/sad.png                       (content/shared/img/sad.png)
+  content/browser/loop/shared/img/helloicon.svg                 (content/shared/img/helloicon.svg)
   content/browser/loop/shared/img/icon_32.png                   (content/shared/img/icon_32.png)
   content/browser/loop/shared/img/icon_64.png                   (content/shared/img/icon_64.png)
   content/browser/loop/shared/img/spinner.svg                   (content/shared/img/spinner.svg)
   # XXX could get rid of the png spinner usages and replace them with the svg
   # one?
   content/browser/loop/shared/img/spinner.png                   (content/shared/img/spinner.png)
   content/browser/loop/shared/img/spinner@2x.png                (content/shared/img/spinner@2x.png)
   content/browser/loop/shared/img/chatbubble-arrow-left.svg     (content/shared/img/chatbubble-arrow-left.svg)
@@ -76,24 +76,21 @@ browser.jar:
 
   # Shared scripts
   content/browser/loop/shared/js/actions.js             (content/shared/js/actions.js)
   content/browser/loop/shared/js/conversationStore.js   (content/shared/js/conversationStore.js)
   content/browser/loop/shared/js/store.js               (content/shared/js/store.js)
   content/browser/loop/shared/js/roomStates.js          (content/shared/js/roomStates.js)
   content/browser/loop/shared/js/fxOSActiveRoomStore.js (content/shared/js/fxOSActiveRoomStore.js)
   content/browser/loop/shared/js/activeRoomStore.js     (content/shared/js/activeRoomStore.js)
-  content/browser/loop/shared/js/feedbackStore.js       (content/shared/js/feedbackStore.js)
   content/browser/loop/shared/js/dispatcher.js          (content/shared/js/dispatcher.js)
-  content/browser/loop/shared/js/feedbackApiClient.js   (content/shared/js/feedbackApiClient.js)
   content/browser/loop/shared/js/models.js              (content/shared/js/models.js)
   content/browser/loop/shared/js/mixins.js              (content/shared/js/mixins.js)
   content/browser/loop/shared/js/otSdkDriver.js         (content/shared/js/otSdkDriver.js)
   content/browser/loop/shared/js/views.js               (content/shared/js/views.js)
-  content/browser/loop/shared/js/feedbackViews.js       (content/shared/js/feedbackViews.js)
   content/browser/loop/shared/js/textChatStore.js       (content/shared/js/textChatStore.js)
   content/browser/loop/shared/js/textChatView.js        (content/shared/js/textChatView.js)
   content/browser/loop/shared/js/utils.js               (content/shared/js/utils.js)
   content/browser/loop/shared/js/validate.js            (content/shared/js/validate.js)
   content/browser/loop/shared/js/websocket.js           (content/shared/js/websocket.js)
 
   # Shared libs
 #ifdef DEBUG
--- a/browser/components/loop/standalone/content/css/webapp.css
+++ b/browser/components/loop/standalone/content/css/webapp.css
@@ -332,38 +332,16 @@ p.standalone-btn-label {
 .standalone .ended-conversation {
   position: relative;
   height: 100%;
   background-color: #444;
   text-align: left; /* as backup */
   text-align: start;
 }
 
-.standalone .ended-conversation .feedback {
-  position: absolute;
-  width: 50%;
-  max-width: 400px;
-  margin: 10px auto;
-  top: 20px;
-  left: 10%;
-  right: 10%;
-  background: #FFF;
-  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;
 }
 
 @media screen and (max-width:640px) {
   .standalone .ended-conversation .feedback {
     width: 92%;
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -129,27 +129,25 @@
     <script type="text/javascript" src="shared/libs/backbone-1.2.1.js"></script>
 
     <!-- app scripts -->
     <script type="text/javascript" src="config.js"></script>
     <script type="text/javascript" src="shared/js/utils.js"></script>
     <script type="text/javascript" src="shared/js/crypto.js"></script>
     <script type="text/javascript" src="shared/js/models.js"></script>
     <script type="text/javascript" src="shared/js/mixins.js"></script>
-    <script type="text/javascript" src="shared/js/feedbackApiClient.js"></script>
     <script type="text/javascript" src="shared/js/actions.js"></script>
     <script type="text/javascript" src="shared/js/validate.js"></script>
     <script type="text/javascript" src="shared/js/dispatcher.js"></script>
     <script type="text/javascript" src="shared/js/websocket.js"></script>
     <script type="text/javascript" src="shared/js/otSdkDriver.js"></script>
     <script type="text/javascript" src="shared/js/store.js"></script>
     <script type="text/javascript" src="shared/js/roomStates.js"></script>
     <script type="text/javascript" src="shared/js/fxOSActiveRoomStore.js"></script>
     <script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
-    <script type="text/javascript" src="shared/js/feedbackStore.js"></script>
     <script type="text/javascript" src="shared/js/views.js"></script>
     <script type="text/javascript" src="shared/js/feedbackViews.js"></script>
     <script type="text/javascript" src="shared/js/textChatStore.js"></script>
     <script type="text/javascript" src="shared/js/textChatView.js"></script>
     <script type="text/javascript" src="js/standaloneAppStore.js"></script>
     <script type="text/javascript" src="js/standaloneClient.js"></script>
     <script type="text/javascript" src="js/standaloneMozLoop.js"></script>
     <script type="text/javascript" src="js/fxOSMarketplace.js"></script>
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -85,23 +85,16 @@ loop.standaloneRoomViews = (function(moz
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       failureReason: React.PropTypes.string,
       isFirefox: React.PropTypes.bool.isRequired,
       joinRoom: React.PropTypes.func.isRequired,
       roomState: React.PropTypes.string.isRequired,
       roomUsed: React.PropTypes.bool.isRequired
     },
 
-    onFeedbackSent: function() {
-      // We pass a tick to prevent React warnings regarding nested updates.
-      setTimeout(function() {
-        this.props.activeRoomStore.dispatchAction(new sharedActions.FeedbackComplete());
-      }.bind(this));
-    },
-
     _renderCallToActionLink: function() {
       if (this.props.isFirefox) {
         return (
           React.createElement("a", {className: "btn btn-info", href: loop.config.learnMoreUrl}, 
             mozL10n.get("rooms_room_full_call_to_action_label", {
               clientShortname: mozL10n.get("clientShortname2")
             })
           )
@@ -113,18 +106,18 @@ loop.standaloneRoomViews = (function(moz
             brandShortname: mozL10n.get("brandShortname")
           })
         )
       );
     },
 
     render: function() {
       switch(this.props.roomState) {
+        case ROOM_STATES.ENDED:
         case ROOM_STATES.READY: {
-          // XXX: In ENDED state, we should rather display the feedback form.
           return (
             React.createElement("div", {className: "room-inner-info-area"}, 
               React.createElement("button", {className: "btn btn-join btn-info", 
                       onClick: this.props.joinRoom}, 
                 mozL10n.get("rooms_room_join_label")
               )
             )
           );
@@ -167,32 +160,16 @@ loop.standaloneRoomViews = (function(moz
             React.createElement("div", {className: "room-inner-info-area"}, 
               React.createElement("p", {className: "full-room-message"}, 
                 mozL10n.get("rooms_room_full_label")
               ), 
               React.createElement("p", null, this._renderCallToActionLink())
             )
           );
         }
-        case ROOM_STATES.ENDED: {
-          if (this.props.roomUsed) {
-            return (
-              React.createElement("div", {className: "ended-conversation"}, 
-                React.createElement(sharedViews.FeedbackView, {
-                  noCloseText: true, 
-                  onAfterFeedbackReceived: this.onFeedbackSent})
-              )
-            );
-          }
-
-          // In case the room was not used (no one was here), we
-          // bypass the feedback form.
-          this.onFeedbackSent();
-          return null;
-        }
         case ROOM_STATES.FAILED: {
           return (
             React.createElement(StandaloneRoomFailureView, {
               dispatcher: this.props.dispatcher, 
               failureReason: this.props.failureReason})
           );
         }
         case ROOM_STATES.INIT:
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -85,23 +85,16 @@ loop.standaloneRoomViews = (function(moz
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       failureReason: React.PropTypes.string,
       isFirefox: React.PropTypes.bool.isRequired,
       joinRoom: React.PropTypes.func.isRequired,
       roomState: React.PropTypes.string.isRequired,
       roomUsed: React.PropTypes.bool.isRequired
     },
 
-    onFeedbackSent: function() {
-      // We pass a tick to prevent React warnings regarding nested updates.
-      setTimeout(function() {
-        this.props.activeRoomStore.dispatchAction(new sharedActions.FeedbackComplete());
-      }.bind(this));
-    },
-
     _renderCallToActionLink: function() {
       if (this.props.isFirefox) {
         return (
           <a className="btn btn-info" href={loop.config.learnMoreUrl}>
             {mozL10n.get("rooms_room_full_call_to_action_label", {
               clientShortname: mozL10n.get("clientShortname2")
             })}
           </a>
@@ -113,18 +106,18 @@ loop.standaloneRoomViews = (function(moz
             brandShortname: mozL10n.get("brandShortname")
           })}
         </a>
       );
     },
 
     render: function() {
       switch(this.props.roomState) {
+        case ROOM_STATES.ENDED:
         case ROOM_STATES.READY: {
-          // XXX: In ENDED state, we should rather display the feedback form.
           return (
             <div className="room-inner-info-area">
               <button className="btn btn-join btn-info"
                       onClick={this.props.joinRoom}>
                 {mozL10n.get("rooms_room_join_label")}
               </button>
             </div>
           );
@@ -167,32 +160,16 @@ loop.standaloneRoomViews = (function(moz
             <div className="room-inner-info-area">
               <p className="full-room-message">
                 {mozL10n.get("rooms_room_full_label")}
               </p>
               <p>{this._renderCallToActionLink()}</p>
             </div>
           );
         }
-        case ROOM_STATES.ENDED: {
-          if (this.props.roomUsed) {
-            return (
-              <div className="ended-conversation">
-                <sharedViews.FeedbackView
-                  noCloseText={true}
-                  onAfterFeedbackReceived={this.onFeedbackSent} />
-              </div>
-            );
-          }
-
-          // In case the room was not used (no one was here), we
-          // bypass the feedback form.
-          this.onFeedbackSent();
-          return null;
-        }
         case ROOM_STATES.FAILED: {
           return (
             <StandaloneRoomFailureView
               dispatcher={this.props.dispatcher}
               failureReason={this.props.failureReason} />
           );
         }
         case ROOM_STATES.INIT:
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -586,18 +586,16 @@ loop.webapp = (function($, _, OT, mozL10
     },
 
     render: function() {
       document.title = mozL10n.get("standalone_title_with_status",
                                    {clientShortname: mozL10n.get("clientShortname2"),
                                     currentStatus: mozL10n.get("status_conversation_ended")});
       return (
         React.createElement("div", {className: "ended-conversation"}, 
-          React.createElement(sharedViews.FeedbackView, {
-            onAfterFeedbackReceived: this.props.onAfterFeedbackReceived}), 
           React.createElement(sharedViews.ConversationView, {
             audio: {enabled: false, visible: false}, 
             dispatcher: this.props.dispatcher, 
             initiate: false, 
             model: this.props.conversation, 
             sdk: this.props.sdk, 
             video: {enabled: false, visible: false}})
         )
@@ -679,17 +677,16 @@ loop.webapp = (function($, _, OT, mozL10
     },
 
     shouldComponentUpdate: function(nextProps, nextState) {
       // Only rerender if current state has actually changed
       return nextState.callStatus !== this.state.callStatus;
     },
 
     resetCallStatus: function() {
-      this.props.dispatcher.dispatch(new sharedActions.FeedbackComplete());
       return function() {
         this.setState({callStatus: "start"});
       }.bind(this);
     },
 
     /**
      * Renders the conversation views.
      */
@@ -1019,23 +1016,16 @@ loop.webapp = (function($, _, OT, mozL10
   function init() {
     var standaloneMozLoop = new loop.StandaloneMozLoop({
       baseServerUrl: loop.config.serverUrl
     });
 
     // Older non-flux based items.
     var notifications = new sharedModels.NotificationCollection();
 
-    var feedbackApiClient = new loop.FeedbackAPIClient(
-      loop.config.feedbackApiUrl, {
-        product: loop.config.feedbackProductName,
-        user_agent: navigator.userAgent,
-        url: document.location.origin
-      });
-
     // New flux items.
     var dispatcher = new loop.Dispatcher();
     var client = new loop.StandaloneClient({
       baseServerUrl: loop.config.serverUrl
     });
     var sdkDriver = new loop.OTSdkDriver({
       // For the standalone, always request data channels. If they aren't
       // implemented on the client, there won't be a similar message to us, and
@@ -1062,42 +1052,31 @@ loop.webapp = (function($, _, OT, mozL10
         sdk: OT
     });
     activeRoomStore = activeRoomStore ||
       new loop.store.ActiveRoomStore(dispatcher, {
         mozLoop: standaloneMozLoop,
         sdkDriver: sdkDriver
     });
 
-    var feedbackClient = new loop.FeedbackAPIClient(
-      loop.config.feedbackApiUrl, {
-      product: loop.config.feedbackProductName,
-      user_agent: navigator.userAgent,
-      url: document.location.origin
-    });
-
     // Stores
     var standaloneAppStore = new loop.store.StandaloneAppStore({
       conversation: conversation,
       dispatcher: dispatcher,
       sdk: OT
     });
-    var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: feedbackClient
-    });
     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() {
       dispatcher.dispatch(new sharedActions.WindowUnload());
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -586,18 +586,16 @@ loop.webapp = (function($, _, OT, mozL10
     },
 
     render: function() {
       document.title = mozL10n.get("standalone_title_with_status",
                                    {clientShortname: mozL10n.get("clientShortname2"),
                                     currentStatus: mozL10n.get("status_conversation_ended")});
       return (
         <div className="ended-conversation">
-          <sharedViews.FeedbackView
-            onAfterFeedbackReceived={this.props.onAfterFeedbackReceived} />
           <sharedViews.ConversationView
             audio={{enabled: false, visible: false}}
             dispatcher={this.props.dispatcher}
             initiate={false}
             model={this.props.conversation}
             sdk={this.props.sdk}
             video={{enabled: false, visible: false}} />
         </div>
@@ -679,17 +677,16 @@ loop.webapp = (function($, _, OT, mozL10
     },
 
     shouldComponentUpdate: function(nextProps, nextState) {
       // Only rerender if current state has actually changed
       return nextState.callStatus !== this.state.callStatus;
     },
 
     resetCallStatus: function() {
-      this.props.dispatcher.dispatch(new sharedActions.FeedbackComplete());
       return function() {
         this.setState({callStatus: "start"});
       }.bind(this);
     },
 
     /**
      * Renders the conversation views.
      */
@@ -1019,23 +1016,16 @@ loop.webapp = (function($, _, OT, mozL10
   function init() {
     var standaloneMozLoop = new loop.StandaloneMozLoop({
       baseServerUrl: loop.config.serverUrl
     });
 
     // Older non-flux based items.
     var notifications = new sharedModels.NotificationCollection();
 
-    var feedbackApiClient = new loop.FeedbackAPIClient(
-      loop.config.feedbackApiUrl, {
-        product: loop.config.feedbackProductName,
-        user_agent: navigator.userAgent,
-        url: document.location.origin
-      });
-
     // New flux items.
     var dispatcher = new loop.Dispatcher();
     var client = new loop.StandaloneClient({
       baseServerUrl: loop.config.serverUrl
     });
     var sdkDriver = new loop.OTSdkDriver({
       // For the standalone, always request data channels. If they aren't
       // implemented on the client, there won't be a similar message to us, and
@@ -1062,42 +1052,31 @@ loop.webapp = (function($, _, OT, mozL10
         sdk: OT
     });
     activeRoomStore = activeRoomStore ||
       new loop.store.ActiveRoomStore(dispatcher, {
         mozLoop: standaloneMozLoop,
         sdkDriver: sdkDriver
     });
 
-    var feedbackClient = new loop.FeedbackAPIClient(
-      loop.config.feedbackApiUrl, {
-      product: loop.config.feedbackProductName,
-      user_agent: navigator.userAgent,
-      url: document.location.origin
-    });
-
     // Stores
     var standaloneAppStore = new loop.store.StandaloneAppStore({
       conversation: conversation,
       dispatcher: dispatcher,
       sdk: OT
     });
-    var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: feedbackClient
-    });
     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() {
       dispatcher.dispatch(new sharedActions.WindowUnload());
--- a/browser/components/loop/standalone/content/l10n/en-US/loop.properties
+++ b/browser/components/loop/standalone/content/l10n/en-US/loop.properties
@@ -68,37 +68,16 @@ vendor_alttext={{vendorShortname}} logo
 ## LOCALIZATION NOTE (call_url_creation_date_label): Example output: (from May 26, 2014)
 call_url_creation_date_label=(from {{call_url_creation_date}})
 call_progress_getting_media_description={{clientShortname}} requires access to your camera and microphone.
 call_progress_getting_media_title=Waiting for media…
 call_progress_connecting_description=Connecting…
 call_progress_ringing_description=Ringing…
 fxos_app_needed=Please install the {{fxosAppName}} app from the Firefox Marketplace.
 
-feedback_call_experience_heading2=How was your conversation?
-feedback_thank_you_heading=Thank you for your feedback!
-feedback_category_list_heading=What made you sad?
-feedback_category_audio_quality=Audio quality
-feedback_category_video_quality=Video quality
-feedback_category_was_disconnected=Was disconnected
-feedback_category_confusing2=Confusing controls
-feedback_category_other2=Other
-feedback_custom_category_text_placeholder=What went wrong?
-feedback_submit_button=Submit
-feedback_back_button=Back
-## LOCALIZATION NOTE (feedback_window_will_close_in2):
-## Gaia l10n format; see https://github.com/mozilla-b2g/gaia/blob/f108c706fae43cd61628babdd9463e7695b2496e/apps/email/locales/email.en-US.properties#L387
-## In this item, don't translate the part between {{..}}
-feedback_window_will_close_in2={[ plural(countdown) ]}
-feedback_window_will_close_in2[one] = This window will close in {{countdown}} second
-feedback_window_will_close_in2[two] = This window will close in {{countdown}} seconds
-feedback_window_will_close_in2[few] = This window will close in {{countdown}} seconds
-feedback_window_will_close_in2[many] = This window will close in {{countdown}} seconds
-feedback_window_will_close_in2[other] = This window will close in {{countdown}} seconds
-
 ## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
 ## a signed-in to signed-in user call.
 ## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
 feedback_rejoin_button=Rejoin
 ## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
 ## an abusive user.
 feedback_report_user_button=Report User
 
--- a/browser/components/loop/test/desktop-local/conversationAppStore_test.js
+++ b/browser/components/loop/test/desktop-local/conversationAppStore_test.js
@@ -27,58 +27,113 @@ describe("loop.store.ConversationAppStor
     it("should throw an error if mozLoop is missing", function() {
       expect(function() {
         new loop.store.ConversationAppStore({dispatcher: dispatcher});
       }).to.Throw(/mozLoop/);
     });
   });
 
   describe("#getWindowData", function() {
-    var fakeWindowData, fakeGetWindowData, fakeMozLoop, store;
+    var fakeWindowData, fakeGetWindowData, fakeMozLoop, store, getLoopPrefStub;
+    var setLoopPrefStub;
 
     beforeEach(function() {
       fakeWindowData = {
         type: "incoming",
         callId: "123456"
       };
 
       fakeGetWindowData = {
         windowId: "42"
       };
 
+      getLoopPrefStub = sandbox.stub();
+      setLoopPrefStub = sandbox.stub();
+
       fakeMozLoop = {
         getConversationWindowData: function(windowId) {
           if (windowId === "42") {
             return fakeWindowData;
           }
           return null;
-        }
+        },
+        getLoopPref: getLoopPrefStub,
+        setLoopPref: setLoopPrefStub
       };
 
       store = new loop.store.ConversationAppStore({
         dispatcher: dispatcher,
         mozLoop: fakeMozLoop
       });
     });
 
+    afterEach(function() {
+      sandbox.restore();
+    });
+
     it("should fetch the window type from the mozLoop API", function() {
       dispatcher.dispatch(new sharedActions.GetWindowData(fakeGetWindowData));
 
       expect(store.getStoreState()).eql({
         windowType: "incoming"
       });
     });
 
+    it("should have the feedback period in initial state", function() {
+      getLoopPrefStub.returns(42);
+
+      // Expect ms.
+      expect(store.getInitialStoreState().feedbackPeriod).to.eql(42 * 1000);
+    });
+
+    it("should have the dateLastSeen in initial state", function() {
+      getLoopPrefStub.returns(42);
+
+      // Expect ms.
+      expect(store.getInitialStoreState().feedbackTimestamp).to.eql(42 * 1000);
+    });
+
+    it("should fetch the correct pref for feedback period", function() {
+      store.getInitialStoreState();
+
+      sinon.assert.calledWithExactly(getLoopPrefStub, "feedback.periodSec");
+    });
+
+    it("should fetch the correct pref for feedback period", function() {
+      store.getInitialStoreState();
+
+      sinon.assert.calledWithExactly(getLoopPrefStub,
+                                     "feedback.dateLastSeenSec");
+    });
+
+    it("should set showFeedbackForm to true when action is triggered", function() {
+      var showFeedbackFormStub = sandbox.stub(store, "showFeedbackForm");
+
+      dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
+
+      sinon.assert.calledOnce(showFeedbackFormStub);
+    });
+
+    it("should set feedback timestamp on ShowFeedbackForm action", function() {
+      var clock = sandbox.useFakeTimers();
+      // Make sure we round down the value.
+      clock.tick(1001);
+      dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
+
+      sinon.assert.calledOnce(setLoopPrefStub);
+      sinon.assert.calledWithExactly(setLoopPrefStub,
+                                     "feedback.dateLastSeenSec", 1);
+    });
+
     it("should dispatch a SetupWindowData action with the data from the mozLoop API",
       function() {
         sandbox.stub(dispatcher, "dispatch");
 
         store.getWindowData(new sharedActions.GetWindowData(fakeGetWindowData));
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.SetupWindowData(_.extend({
             windowId: fakeGetWindowData.windowId
           }, fakeWindowData)));
       });
   });
-
 });
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -5,27 +5,27 @@ describe("loop.conversationViews", funct
   "use strict";
 
   var expect = chai.expect;
   var TestUtils = React.addons.TestUtils;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sandbox, view, dispatcher, contact, fakeAudioXHR, conversationStore;
-  var fakeMozLoop, fakeWindow;
+  var fakeMozLoop, fakeWindow, fakeClock;
 
   var CALL_STATES = loop.store.CALL_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
   var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
   var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
-    sandbox.useFakeTimers();
+    fakeClock = sandbox.useFakeTimers();
 
     sandbox.stub(document.mozL10n, "get", function(x) {
       return x;
     });
 
     dispatcher = new loop.Dispatcher();
     sandbox.stub(dispatcher, "dispatch");
 
@@ -53,16 +53,17 @@ describe("loop.conversationViews", funct
 
     fakeMozLoop = navigator.mozLoop = {
       SHARING_ROOM_URL: {
         EMAIL_FROM_CALLFAILED: 2,
         EMAIL_FROM_CONVERSATION: 3
       },
       // Dummy function, stubbed below.
       getLoopPref: function() {},
+      setLoopPref: sandbox.stub(),
       calls: {
         clearCallInProgress: sinon.stub()
       },
       composeEmail: sinon.spy(),
       get appVersionInfo() {
         return {
           version: "42",
           channel: "test",
@@ -90,28 +91,24 @@ describe("loop.conversationViews", funct
       navigator: { mozLoop: fakeMozLoop },
       close: sinon.stub(),
       document: {},
       addEventListener: function() {},
       removeEventListener: function() {}
     };
     loop.shared.mixins.setRootObject(fakeWindow);
 
-    var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: {}
-    });
     conversationStore = new loop.store.ConversationStore(dispatcher, {
       client: {},
       mozLoop: fakeMozLoop,
       sdkDriver: {}
     });
 
     loop.store.StoreMixin.register({
-      conversationStore: conversationStore,
-      feedbackStore: feedbackStore
+      conversationStore: conversationStore
     });
   });
 
   afterEach(function() {
     loop.shared.mixins.setRootObject(window);
     view = undefined;
     delete navigator.mozLoop;
     sandbox.restore();
@@ -603,30 +600,33 @@ describe("loop.conversationViews", funct
 
       var muteBtn = view.getDOMNode().querySelector(".btn-mute-audio");
 
       expect(muteBtn.classList.contains("muted")).eql(true);
     });
   });
 
   describe("CallControllerView", function() {
-    var feedbackStore;
+    var onCallTerminatedStub;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.CallControllerView, {
           dispatcher: dispatcher,
-          mozLoop: fakeMozLoop
+          mozLoop: fakeMozLoop,
+          onCallTerminated: onCallTerminatedStub
         }));
     }
 
     beforeEach(function() {
-      feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-        feedbackClient: {}
-      });
+      onCallTerminatedStub = sandbox.stub();
+    });
+
+    afterEach(function() {
+      sandbox.restore();
     });
 
     it("should set the document title to the callerId", function() {
       conversationStore.setStoreState({
         contact: contact
       });
 
       mountTestComponent();
@@ -699,34 +699,16 @@ describe("loop.conversationViews", funct
         conversationStore.setStoreState({callState: CALL_STATES.ONGOING});
 
         view = mountTestComponent();
 
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.OngoingConversationView);
     });
 
-    it("should render the FeedbackView when the call state is 'finished'",
-      function() {
-        conversationStore.setStoreState({callState: CALL_STATES.FINISHED});
-
-        view = mountTestComponent();
-
-        TestUtils.findRenderedComponentWithType(view,
-          loop.shared.views.FeedbackView);
-    });
-
-    it("should set the document title to conversation_has_ended when displaying the feedback view", function() {
-      conversationStore.setStoreState({callState: CALL_STATES.FINISHED});
-
-      mountTestComponent();
-
-      expect(fakeWindow.document.title).eql("conversation_has_ended");
-    });
-
     it("should play the terminated sound when the call state is 'finished'",
       function() {
         var fakeAudio = {
           play: sinon.spy(),
           pause: sinon.spy(),
           removeAttribute: sinon.spy()
         };
         sandbox.stub(window, "Audio").returns(fakeAudio);
@@ -751,16 +733,31 @@ describe("loop.conversationViews", funct
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.PendingConversationView);
 
         conversationStore.setStoreState({callState: CALL_STATES.TERMINATED});
 
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.CallFailedView);
     });
+
+    it("should call onCallTerminated when the call is finished", function() {
+      conversationStore.setStoreState({
+        callState: CALL_STATES.ONGOING
+      });
+
+      view = mountTestComponent({
+        callState: CALL_STATES.FINISHED
+      });
+      // Force a state change so that it triggers componentDidUpdate.
+      view.setState({ callState: CALL_STATES.FINISHED });
+
+      sinon.assert.calledOnce(onCallTerminatedStub);
+      sinon.assert.calledWithExactly(onCallTerminatedStub);
+    });
   });
 
   describe("AcceptCallView", function() {
     var callView;
 
     function mountTestComponent(extraProps) {
       var props = _.extend({dispatcher: dispatcher, mozLoop: fakeMozLoop}, extraProps);
       return TestUtils.renderIntoDocument(
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -1,33 +1,35 @@
 /* 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/. */
 
 describe("loop.conversation", function() {
   "use strict";
 
   var expect = chai.expect;
+  var FeedbackView = loop.feedbackViews.FeedbackView;
   var TestUtils = React.addons.TestUtils;
-  var sharedModels = loop.shared.models,
-      fakeWindow,
-      sandbox;
+  var sharedActions = loop.shared.actions;
+  var sharedModels = loop.shared.models;
+  var fakeWindow, sandbox, getLoopPrefStub, setLoopPrefStub, mozL10nGet;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
+    setLoopPrefStub = sandbox.stub();
 
     navigator.mozLoop = {
       doNotDisturb: true,
       getStrings: function() {
         return JSON.stringify({textContent: "fakeText"});
       },
       get locale() {
         return "en-US";
       },
-      setLoopPref: sinon.stub(),
+      setLoopPref: setLoopPrefStub,
       getLoopPref: function(prefName) {
         if (prefName == "debug.sdk") {
           return false;
         }
 
         return "http://fake";
       },
       LOOP_SESSION_TYPE: {
@@ -58,17 +60,17 @@ describe("loop.conversation", function()
       document: {},
       addEventListener: function() {},
       removeEventListener: function() {}
     };
     loop.shared.mixins.setRootObject(fakeWindow);
 
     // XXX These stubs should be hoisted in a common file
     // Bug 1040968
-    sandbox.stub(document.mozL10n, "get", function(x) {
+    mozL10nGet = sandbox.stub(document.mozL10n, "get", function(x) {
       return x;
     });
     document.mozL10n.initialize(navigator.mozLoop);
   });
 
   afterEach(function() {
     loop.shared.mixins.setRootObject(window);
     delete navigator.mozLoop;
@@ -127,17 +129,17 @@ describe("loop.conversation", function()
         new loop.shared.actions.GetWindowData({
           windowId: "42"
         }));
     });
   });
 
   describe("AppControllerView", function() {
     var conversationStore, client, ccView, dispatcher;
-    var conversationAppStore, roomStore;
+    var conversationAppStore, roomStore, feedbackPeriodMs = 15770000000;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversation.AppControllerView, {
           roomStore: roomStore,
           dispatcher: dispatcher,
           mozLoop: navigator.mozLoop
         }));
@@ -210,10 +212,102 @@ describe("loop.conversation", function()
     it("should display the GenericFailureView for failures", function() {
       conversationAppStore.setStoreState({windowType: "failed"});
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
         loop.conversationViews.GenericFailureView);
     });
+
+    it("should set the correct title when rendering feedback view", function() {
+      conversationAppStore.setStoreState({showFeedbackForm: true});
+
+      ccView = mountTestComponent();
+
+      sinon.assert.calledWithExactly(mozL10nGet, "conversation_has_ended");
+    });
+
+    it("should render FeedbackView if showFeedbackForm state is true",
+       function() {
+         conversationAppStore.setStoreState({showFeedbackForm: true});
+
+         ccView = mountTestComponent();
+
+         TestUtils.findRenderedComponentWithType(ccView, FeedbackView);
+       });
+
+    it("should dispatch a ShowFeedbackForm action if timestamp is 0",
+       function() {
+         conversationAppStore.setStoreState({feedbackTimestamp: 0});
+         sandbox.stub(dispatcher, "dispatch");
+
+         ccView = mountTestComponent();
+
+         ccView.handleCallTerminated();
+
+         sinon.assert.calledOnce(dispatcher.dispatch);
+         sinon.assert.calledWithExactly(dispatcher.dispatch,
+                                        new sharedActions.ShowFeedbackForm());
+       });
+
+    it("should set feedback timestamp if delta is > feedback period",
+       function() {
+         var feedbackTimestamp = new Date() - feedbackPeriodMs;
+         conversationAppStore.setStoreState({
+           feedbackTimestamp: feedbackTimestamp,
+           feedbackPeriod: feedbackPeriodMs
+         });
+
+         ccView = mountTestComponent();
+
+         ccView.handleCallTerminated();
+
+         sinon.assert.calledOnce(setLoopPrefStub);
+       });
+
+    it("should dispatch a ShowFeedbackForm action if delta > feedback period",
+       function() {
+         var feedbackTimestamp = new Date() - feedbackPeriodMs;
+         conversationAppStore.setStoreState({
+           feedbackTimestamp: feedbackTimestamp,
+           feedbackPeriod: feedbackPeriodMs
+         });
+         sandbox.stub(dispatcher, "dispatch");
+
+         ccView = mountTestComponent();
+
+         ccView.handleCallTerminated();
+
+         sinon.assert.calledOnce(dispatcher.dispatch);
+         sinon.assert.calledWithExactly(dispatcher.dispatch,
+                                        new sharedActions.ShowFeedbackForm());
+       });
+
+    it("should close the window if delta < feedback period", function() {
+      var feedbackTimestamp = new Date().getTime();
+      conversationAppStore.setStoreState({
+        feedbackTimestamp: feedbackTimestamp,
+        feedbackPeriod: feedbackPeriodMs
+      });
+
+      ccView = mountTestComponent();
+      var closeWindowStub = sandbox.stub(ccView, "closeWindow");
+      ccView.handleCallTerminated();
+
+      sinon.assert.calledOnce(closeWindowStub);
+    });
+
+    it("should set the correct timestamp for dateLastSeenSec", function() {
+      var feedbackTimestamp = new Date().getTime();
+      conversationAppStore.setStoreState({
+        feedbackTimestamp: feedbackTimestamp,
+        feedbackPeriod: feedbackPeriodMs
+      });
+
+      ccView = mountTestComponent();
+      var closeWindowStub = sandbox.stub(ccView, "closeWindow");
+      ccView.handleCallTerminated();
+
+      sinon.assert.calledOnce(closeWindowStub);
+    });
   });
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/desktop-local/feedbackViews_test.js
@@ -0,0 +1,100 @@
+/* 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/. */
+
+describe("loop.feedbackViews", function() {
+  "use strict";
+
+  var FeedbackView = loop.feedbackViews.FeedbackView;
+  var l10n = navigator.mozL10n || document.mozL10n;
+  var TestUtils = React.addons.TestUtils;
+  var sandbox, mozL10nGet;
+
+  beforeEach(function() {
+    sandbox = sinon.sandbox.create();
+    mozL10nGet = sandbox.stub(l10n, "get", function(x) {
+      return "translated:" + x;
+    });
+  });
+
+  afterEach(function() {
+    sandbox.restore();
+  });
+
+  describe("FeedbackView", function() {
+    var openURLStub, getLoopPrefStub, feedbackReceivedStub;
+    var fakeURL = "fake.form", mozLoop, view;
+
+    function mountTestComponent(props) {
+      props = _.extend({
+        mozLoop: mozLoop,
+        onAfterFeedbackReceived: feedbackReceivedStub
+      }, props);
+
+      return TestUtils.renderIntoDocument(
+        React.createElement(FeedbackView, props));
+    }
+
+    beforeEach(function() {
+      openURLStub = sandbox.stub();
+      getLoopPrefStub = sandbox.stub();
+      feedbackReceivedStub = sandbox.stub();
+      mozLoop = {
+        openURL: openURLStub,
+        getLoopPref: getLoopPrefStub
+      };
+    });
+
+    afterEach(function() {
+      view = null;
+    });
+
+    it("should render a feedback view", function() {
+      view = mountTestComponent();
+
+      TestUtils.findRenderedComponentWithType(view, FeedbackView);
+    });
+
+    it("should render a button with correct text", function() {
+      view = mountTestComponent();
+
+      sinon.assert.calledWithExactly(mozL10nGet, "feedback_request_button");
+    });
+
+    it("should render a header with correct text", function() {
+      view = mountTestComponent();
+
+      sinon.assert.calledWithExactly(mozL10nGet, "feedback_window_heading");
+    });
+
+    it("should open a new page to the feedback form", function() {
+      mozLoop.getLoopPref = sinon.stub().withArgs("feedback.formURL")
+                              .returns(fakeURL);
+      view = mountTestComponent();
+
+      TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
+
+      sinon.assert.calledOnce(openURLStub);
+      sinon.assert.calledWithExactly(openURLStub, fakeURL);
+    });
+
+    it("should fetch the feedback form URL from the prefs", function() {
+      mozLoop.getLoopPref = sinon.stub().withArgs("feedback.formURL")
+                              .returns(fakeURL);
+      view = mountTestComponent();
+
+      TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
+
+      sinon.assert.calledOnce(mozLoop.getLoopPref);
+      sinon.assert.calledWithExactly(mozLoop.getLoopPref, "feedback.formURL");
+    });
+
+    it("should close the window after opening the form", function() {
+      view = mountTestComponent();
+
+      TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
+
+      sinon.assert.calledOnce(feedbackReceivedStub);
+    });
+  });
+});
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -42,47 +42,46 @@
   <script>
     /*global chai,mocha */
     chai.config.includeStack = true;
     mocha.setup({ui: 'bdd', timeout: 10000});
   </script>
 
   <!-- App scripts -->
   <script src="../../content/shared/js/utils.js"></script>
-  <script src="../../content/shared/js/feedbackApiClient.js"></script>
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/mixins.js"></script>
   <script src="../../content/shared/js/websocket.js"></script>
   <script src="../../content/shared/js/actions.js"></script>
   <script src="../../content/shared/js/validate.js"></script>
   <script src="../../content/shared/js/dispatcher.js"></script>
   <script src="../../content/shared/js/otSdkDriver.js"></script>
   <script src="../../content/shared/js/store.js"></script>
   <script src="../../content/shared/js/conversationStore.js"></script>
   <script src="../../content/shared/js/roomStates.js"></script>
   <script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
   <script src="../../content/shared/js/activeRoomStore.js"></script>
-  <script src="../../content/shared/js/feedbackStore.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/textChatStore.js"></script>
   <script src="../../content/shared/js/textChatView.js"></script>
-  <script src="../../content/shared/js/feedbackViews.js"></script>
   <script src="../../content/js/client.js"></script>
   <script src="../../content/js/conversationAppStore.js"></script>
   <script src="../../content/js/roomStore.js"></script>
   <script src="../../content/js/roomViews.js"></script>
   <script src="../../content/js/conversationViews.js"></script>
+  <script src="../../content/js/feedbackViews.js"></script>
   <script src="../../content/js/conversation.js"></script>
   <script type="text/javascript;version=1.8" src="../../content/js/contacts.js"></script>
   <script src="../../content/js/panel.js"></script>
 
   <!-- Test scripts -->
   <script src="conversationAppStore_test.js"></script>
   <script src="client_test.js"></script>
   <script src="conversation_test.js"></script>
+  <script src="feedbackViews_test.js"></script>
   <script src="panel_test.js"></script>
   <script src="roomViews_test.js"></script>
   <script src="conversationViews_test.js"></script>
   <script src="contacts_test.js"></script>
   <script src="l10n_test.js"></script>
   <script src="roomStore_test.js"></script>
   <script>
     // Stop the default init functions running to avoid conflicts in tests
@@ -92,17 +91,17 @@
     describe("Uncaught Error Check", function() {
       it("should load the tests without errors", function() {
         chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
       });
     });
 
     describe("Unexpected Warnings Check", function() {
       it("should long only the warnings we expect", function() {
-        chai.expect(caughtWarnings.length).to.eql(30);
+        chai.expect(caughtWarnings.length).to.eql(27);
       });
     });
 
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
     });
   </script>
 </body>
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -36,17 +36,18 @@ describe("loop.roomViews", function () {
           roomName: "fakeName",
           decryptedContext: {
             roomName: "fakeName",
             urls: []
           }
         }),
         update: sinon.stub().callsArgWith(2, null)
       },
-      telemetryAddValue: sinon.stub()
+      telemetryAddValue: sinon.stub(),
+      setLoopPref: sandbox.stub()
     };
 
     fakeWindow = {
       close: sinon.stub(),
       document: {},
       navigator: {
         mozLoop: fakeMozLoop
       },
@@ -196,18 +197,17 @@ describe("loop.roomViews", function () {
 
     describe("Copy Button", function() {
       beforeEach(function() {
         view = mountTestComponent({
           roomData: { roomUrl: "http://invalid" }
         });
       });
 
-      it("should dispatch a CopyRoomUrl action when the copy button is " +
-        "pressed", function() {
+      it("should dispatch a CopyRoomUrl action when the copy button is pressed", function() {
           var copyBtn = view.getDOMNode().querySelector(".btn-copy");
 
           React.addons.TestUtils.Simulate.click(copyBtn);
 
           sinon.assert.calledOnce(dispatcher.dispatch);
           sinon.assert.calledWith(dispatcher.dispatch, new sharedActions.CopyRoomUrl({
             roomUrl: "http://invalid",
             from: "conversation"
@@ -285,40 +285,38 @@ describe("loop.roomViews", function () {
         });
 
         expect(view.getDOMNode().querySelector(".room-context")).to.not.eql(null);
       });
     });
   });
 
   describe("DesktopRoomConversationView", function() {
-    var view;
+    var view, onCallTerminatedStub;
 
     beforeEach(function() {
-      loop.store.StoreMixin.register({
-        feedbackStore: new loop.store.FeedbackStore(dispatcher, {
-          feedbackClient: {}
-        })
-      });
       sandbox.stub(dispatcher, "dispatch");
       fakeMozLoop.getLoopPref = function(prefName) {
         if (prefName == "contextInConversations.enabled") {
           return true;
         }
         return "test";
       };
+      onCallTerminatedStub = sandbox.stub();
     });
 
-    function mountTestComponent() {
+    function mountTestComponent(props) {
+      props = _.extend({
+        dispatcher: dispatcher,
+        roomStore: roomStore,
+        mozLoop: fakeMozLoop,
+        onCallTerminated: onCallTerminatedStub
+      }, props);
       return TestUtils.renderIntoDocument(
-        React.createElement(loop.roomViews.DesktopRoomConversationView, {
-          dispatcher: dispatcher,
-          roomStore: roomStore,
-          mozLoop: fakeMozLoop
-        }));
+        React.createElement(loop.roomViews.DesktopRoomConversationView, props));
     }
 
     it("should dispatch a setMute action when the audio mute button is pressed",
       function() {
         view = mountTestComponent();
 
         view.setState({audioMuted: true});
 
@@ -367,18 +365,17 @@ describe("loop.roomViews", function () {
 
       view.setState({audioMuted: true});
 
       var muteBtn = view.getDOMNode().querySelector(".btn-mute-audio");
 
       expect(muteBtn.classList.contains("muted")).eql(true);
     });
 
-    it("should dispatch a `StartScreenShare` action when sharing is not active " +
-       "and the screen share button is pressed", function() {
+    it("should dispatch a `StartScreenShare` action when sharing is not active and the screen share button is pressed", function() {
       view = mountTestComponent();
 
       view.setState({screenSharingState: SCREEN_SHARE_STATES.INACTIVE});
 
       var muteBtn = view.getDOMNode().querySelector(".btn-mute-video");
 
       React.addons.TestUtils.Simulate.click(muteBtn);
 
@@ -414,28 +411,26 @@ describe("loop.roomViews", function () {
 
     describe("#componentWillUpdate", function() {
       function expectActionDispatched(component) {
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           sinon.match.instanceOf(sharedActions.SetupStreamElements));
       }
 
-      it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state " +
-        "is entered", function() {
+      it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state is entered", function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
           var component = mountTestComponent();
 
           activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
 
           expectActionDispatched(component);
         });
 
-      it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is " +
-        "re-entered", function() {
+      it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is re-entered", function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
           var component = mountTestComponent();
 
           activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
 
           expectActionDispatched(component);
         });
     });
@@ -484,28 +479,28 @@ describe("loop.roomViews", function () {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
 
           view = mountTestComponent();
 
           TestUtils.findRenderedComponentWithType(view,
             loop.roomViews.DesktopRoomConversationView);
         });
 
-      it("should render the FeedbackView if roomState is `ENDED`",
-        function() {
-          activeRoomStore.setStoreState({
-            roomState: ROOM_STATES.ENDED,
-            used: true
-          });
+      it("should call onCallTerminated when the call ended", function() {
+        activeRoomStore.setStoreState({
+          roomState: ROOM_STATES.ENDED,
+          used: true
+        });
 
-          view = mountTestComponent();
+        view = mountTestComponent();
+        // Force a state change so that it triggers componentDidUpdate
+        view.setState({ foo: "bar" });
 
-          TestUtils.findRenderedComponentWithType(view,
-            loop.shared.views.FeedbackView);
-        });
+        sinon.assert.calledOnce(onCallTerminatedStub);
+      });
 
       it("should display loading spinner when localSrcVideoObject is null",
          function() {
            activeRoomStore.setStoreState({
              roomState: ROOM_STATES.MEDIA_WAIT,
              localSrcVideoObject: null
            });
 
--- a/browser/components/loop/test/functional/test_1_browser_call.py
+++ b/browser/components/loop/test/functional/test_1_browser_call.py
@@ -147,26 +147,22 @@ class Test1BrowserCall(MarionetteTestCas
         button = self.marionette.find_element(By.CLASS_NAME, "btn-screen-share")
 
         button.click()
 
     def standalone_check_remote_screenshare(self):
         self.switch_to_standalone()
         self.check_video(".screen-share-video")
 
-    def remote_leave_room_and_verify_feedback(self):
+    def remote_leave_room(self):
         self.switch_to_standalone()
         button = self.marionette.find_element(By.CLASS_NAME, "btn-hangup")
 
         button.click()
 
-        # check that the feedback form is displayed
-        feedback_form = self.wait_for_element_displayed(By.CLASS_NAME, "faces")
-        self.assertEqual(feedback_form.tag_name, "div", "expect feedback form")
-
         self.switch_to_chatbox()
         # check that the local view reverts to the preview mode
         self.wait_for_element_displayed(By.CLASS_NAME, "room-invitation-content")
 
     def local_get_chatbox_window_expr(self, expr):
         """
         :expr: a sub-expression which must begin with a property of the
         global content window (e.g. "location.path")
@@ -255,15 +251,15 @@ class Test1BrowserCall(MarionetteTestCas
         # self.local_enable_screenshare()
         # self.standalone_check_remote_screenshare()
 
         # We hangup on the remote (standalone) side, because this also leaves
         # the local chatbox with the local publishing media still connected,
         # which means that the local_check_connection_length below
         # verifies that the connection is noted at the time the remote media
         # drops, rather than waiting until the window closes.
-        self.remote_leave_room_and_verify_feedback()
+        self.remote_leave_room()
 
         self.local_check_connection_length_noted()
 
     def tearDown(self):
         self.loop_test_servers.shutdown()
         MarionetteTestCase.tearDown(self)
--- a/browser/components/loop/test/mochitest/.eslintrc
+++ b/browser/components/loop/test/mochitest/.eslintrc
@@ -1,13 +1,14 @@
 {
   "extends": "../../.eslintrc-gecko",
   "globals": {
     // General test items.
     "add_task": false,
+    "BrowserTestUtils": true,
     "Cc": true,
     "Ci": true,
     "Cr": true,
     "Cu": true,
     "is": false,
     "info": false,
     "ok": false,
     "registerCleanupFunction": false,
@@ -19,17 +20,16 @@
     "getLoopString": false,
     "gMozLoopAPI": true,
     "mockDb": true,
     "mockPushHandler": true,
     "promiseDeletedOAuthParams": false,
     "promiseOAuthGetRegistration": false,
     "promiseOAuthParamsSetup": false,
     "promiseObserverNotified": false,
-    "promiseTabLoadEvent": false,
     "promiseWaitForCondition": false,
     "resetFxA": true,
     // Loop specific items
     "MozLoopServiceInternal": true,
     "LoopRoomsInternal": true,
     "LoopUI": false,
   }
 }
--- a/browser/components/loop/test/mochitest/browser_mozLoop_context.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_context.js
@@ -18,34 +18,32 @@ add_task(function* test_mozLoop_getSelec
 
   let metadata = yield promiseGetMetadata();
 
   Assert.strictEqual(metadata.url, null, "URL should be empty for about:blank");
   Assert.strictEqual(metadata.favicon, null, "Favicon should be empty for about:blank");
   Assert.strictEqual(metadata.title, "", "Title should be empty for about:blank");
   Assert.deepEqual(metadata.previews, [], "No previews available for about:blank");
 
-  let tab = gBrowser.selectedTab = gBrowser.addTab();
-  yield promiseTabLoadEvent(tab, "about:home");
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home");
   metadata = yield promiseGetMetadata();
 
   Assert.strictEqual(metadata.url, null, "URL should be empty for about:home");
   Assert.strictEqual(metadata.favicon, null, "Favicon should be empty for about:home");
   Assert.ok(metadata.title, "Title should be set for about:home");
   // Filter out null elements in the previews - contentSearchUI adds some img
   // elements with chrome:// srcs, which show up as null in metadata.previews.
   Assert.deepEqual(metadata.previews.filter(e => e), [], "No previews available for about:home");
 
-  gBrowser.removeTab(tab);
+  yield BrowserTestUtils.removeTab(tab);
 });
 
 add_task(function* test_mozLoop_getSelectedTabMetadata_defaultIcon() {
-  let tab = gBrowser.selectedTab = gBrowser.addTab();
-  yield promiseTabLoadEvent(tab, "http://example.com/");
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
   let metadata = yield promiseGetMetadata();
 
   Assert.strictEqual(metadata.url, "http://example.com/", "URL should match");
   Assert.strictEqual(metadata.favicon, null, "Favicon should be empty");
   Assert.ok(metadata.title, "Title should be set");
   Assert.deepEqual(metadata.previews, [], "No previews available");
 
-  gBrowser.removeTab(tab);
+  yield BrowserTestUtils.removeTab(tab);
 });
--- a/browser/components/loop/test/mochitest/browser_mozLoop_pluralStrings.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_pluralStrings.js
@@ -10,14 +10,14 @@
 
 Components.utils.import("resource://gre/modules/Promise.jsm", this);
 
 add_task(loadLoopPanel);
 
 add_task(function* test_mozLoop_pluralStrings() {
   Assert.ok(gMozLoopAPI, "mozLoop should exist");
 
-  var strings = JSON.parse(gMozLoopAPI.getStrings("feedback_window_will_close_in2"));
-  Assert.equal(gMozLoopAPI.getPluralForm(0, strings.textContent),
-               "This window will close in {{countdown}} seconds");
+  var strings = JSON.parse(gMozLoopAPI.getStrings("import_contacts_success_message"));
   Assert.equal(gMozLoopAPI.getPluralForm(1, strings.textContent),
-               "This window will close in {{countdown}} second");
+               "{{total}} contact was successfully imported.");
+  Assert.equal(gMozLoopAPI.getPluralForm(3, strings.textContent),
+               "{{total}} contacts were successfully imported.");
 });
--- a/browser/components/loop/test/mochitest/browser_mozLoop_sharingListeners.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_sharingListeners.js
@@ -41,20 +41,20 @@ function promiseWindowIdReceivedNewTab(h
   let promiseHandlers = [];
 
   handlersParam.forEach(handler => {
     promiseHandlers.push(new Promise(resolve => {
       handler.resolve = resolve;
     }));
   });
 
-  let createdTab = gBrowser.selectedTab = gBrowser.addTab();
+  let createdTab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla");
   createdTabs.push(createdTab);
 
-  promiseHandlers.push(promiseTabLoadEvent(createdTab, "about:mozilla"));
+  promiseHandlers.push(BrowserTestUtils.browserLoaded(createdTab.linkedBrowser));
 
   return Promise.all(promiseHandlers);
 }
 
 function promiseRemoveTab(tab) {
   return new Promise(resolve => {
     gBrowser.tabContainer.addEventListener("TabClose", function onTabClose() {
       gBrowser.tabContainer.removeEventListener("TabClose", onTabClose);
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -202,62 +202,16 @@ function promiseOAuthGetRegistration(bas
     xhr.open("GET", baseURL + "/get_registration", true);
     xhr.responseType = "json";
     xhr.addEventListener("load", () => resolve(xhr));
     xhr.addEventListener("error", reject);
     xhr.send();
   });
 }
 
-/**
- * Waits for a load (or custom) event to finish in a given tab. If provided
- * load an uri into the tab.
- *
- * @param tab
- *        The tab to load into.
- * @param [optional] url
- *        The url to load, or the current url.
- * @param [optional] event
- *        The load event type to wait for.  Defaults to "load".
- * @return {Promise} resolved when the event is handled.
- * @resolves to the received event
- * @rejects if a valid load event is not received within a meaningful interval
- */
-function promiseTabLoadEvent(tab, url, eventType="load") {
-  return new Promise((resolve, reject) => {
-    info("Wait tab event: " + eventType);
-
-    function handle(event) {
-      if (event.originalTarget != tab.linkedBrowser.contentDocument ||
-          event.target.location.href == "about:blank" ||
-          (url && event.target.location.href != url)) {
-        info("Skipping spurious '" + eventType + "'' event" +
-             " for " + event.target.location.href);
-        return;
-      }
-      clearTimeout(timeout);
-      tab.linkedBrowser.removeEventListener(eventType, handle, true);
-      info("Tab event received: " + eventType);
-      resolve(event);
-    }
-
-    let timeout = setTimeout(() => {
-      if (tab.linkedBrowser) {
-        tab.linkedBrowser.removeEventListener(eventType, handle, true);
-      }
-      reject(new Error("Timed out while waiting for a '" + eventType + "'' event"));
-    }, 30000);
-
-    tab.linkedBrowser.addEventListener(eventType, handle, true, true);
-    if (url) {
-      tab.linkedBrowser.loadURI(url);
-    }
-  });
-}
-
 function getLoopString(stringID) {
   return MozLoopServiceInternal.localizedStrings.get(stringID);
 }
 
 /**
  * This is used to fake push registration and notifications for
  * MozLoopService tests. There is only one object created per test instance, as
  * once registration has taken place, the object cannot currently be changed.
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -561,40 +561,16 @@ describe("loop.store.ActiveRoomStore", f
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.UpdateRoomInfo(expectedData));
       });
     });
   });
 
-  describe("#feedbackComplete", function() {
-    it("should set the room state to READY", function() {
-      store.setStoreState({
-        roomState: ROOM_STATES.ENDED,
-        used: true
-      });
-
-      store.feedbackComplete(new sharedActions.FeedbackComplete());
-
-      expect(store.getStoreState().roomState).eql(ROOM_STATES.READY);
-    });
-
-    it("should reset the 'used' state", function() {
-      store.setStoreState({
-        roomState: ROOM_STATES.ENDED,
-        used: true
-      });
-
-      store.feedbackComplete(new sharedActions.FeedbackComplete());
-
-      expect(store.getStoreState().used).eql(false);
-    });
-  });
-
   describe("#videoDimensionsChanged", function() {
     it("should not contain any video dimensions at the very start", function() {
       expect(store.getStoreState()).eql(store.getInitialStoreState());
     });
 
     it("should update the store with new video dimensions", function() {
       var actionData = {
         isLocal: true,
deleted file mode 100644
--- a/browser/components/loop/test/shared/feedbackApiClient_test.js
+++ /dev/null
@@ -1,184 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-describe("loop.FeedbackAPIClient", function() {
-  "use strict";
-
-  var expect = chai.expect;
-  var sandbox,
-      fakeXHR,
-      requests = [];
-
-  beforeEach(function() {
-    sandbox = sinon.sandbox.create();
-    fakeXHR = sandbox.useFakeXMLHttpRequest();
-    requests = [];
-    // https://github.com/cjohansen/Sinon.JS/issues/393
-    fakeXHR.xhr.onCreate = function (xhr) {
-      requests.push(xhr);
-    };
-  });
-
-  afterEach(function() {
-    sandbox.restore();
-  });
-
-  describe("#constructor", function() {
-    it("should require a baseUrl setting", function() {
-      expect(function() {
-        return new loop.FeedbackAPIClient();
-      }).to.Throw(/required 'baseUrl'/);
-    });
-
-    it("should require a product setting", function() {
-      expect(function() {
-        return new loop.FeedbackAPIClient("http://fake", {});
-      }).to.Throw(/required 'product'/);
-    });
-  });
-
-  describe("constructed", function() {
-    var client;
-
-    beforeEach(function() {
-      client = new loop.FeedbackAPIClient("http://fake/feedback", {
-        product: "Hello",
-        version: "42b1"
-      });
-    });
-
-    describe("#send", function() {
-      it("should send happy feedback data", function() {
-        var feedbackData = {
-          happy: true,
-          description: "Happy User"
-        };
-
-        client.send(feedbackData, function(){});
-
-        expect(requests).to.have.length.of(1);
-        expect(requests[0].url).to.be.equal("http://fake/feedback");
-        expect(requests[0].method).to.be.equal("POST");
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.happy).eql(true);
-        expect(parsed.description).eql("Happy User");
-      });
-
-      it("should send sad feedback data", function() {
-        var feedbackData = {
-          happy: false,
-          category: "confusing"
-        };
-
-        client.send(feedbackData, function(){});
-
-        expect(requests).to.have.length.of(1);
-        expect(requests[0].url).to.be.equal("http://fake/feedback");
-        expect(requests[0].method).to.be.equal("POST");
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.happy).eql(false);
-        expect(parsed.product).eql("Hello");
-        expect(parsed.category).eql("confusing");
-        expect(parsed.description).eql("Sad User");
-      });
-
-      it("should send formatted feedback data", function() {
-        client.send({
-          happy: false,
-          category: "other",
-          description: "it's far too awesome!"
-        }, function(){});
-
-        expect(requests).to.have.length.of(1);
-        expect(requests[0].url).eql("http://fake/feedback");
-        expect(requests[0].method).eql("POST");
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.happy).eql(false);
-        expect(parsed.product).eql("Hello");
-        expect(parsed.category).eql("other");
-        expect(parsed.description).eql("it's far too awesome!");
-      });
-
-      it("should send product information", function() {
-        client.send({product: "Hello"}, function(){});
-
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.product).eql("Hello");
-      });
-
-      it("should send platform information when provided", function() {
-        client.send({platform: "Windows 8"}, function(){});
-
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.platform).eql("Windows 8");
-      });
-
-      it("should send channel information when provided", function() {
-        client.send({channel: "beta"}, function(){});
-
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.channel).eql("beta");
-      });
-
-      it("should send version information when provided", function() {
-        client.send({version: "42b1"}, function(){});
-
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.version).eql("42b1");
-      });
-
-      it("should send user_agent information when provided", function() {
-        client.send({user_agent: "MOZAGENT"}, function(){});
-
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.user_agent).eql("MOZAGENT");
-      });
-
-      it("should send url information when provided", function() {
-        client.send({url: "http://fake.invalid"}, function(){});
-
-        var parsed = JSON.parse(requests[0].requestBody);
-        expect(parsed.url).eql("http://fake.invalid");
-      });
-
-      it("should throw on invalid feedback data", function() {
-        expect(function() {
-          client.send("invalid data", function(){});
-        }).to.Throw(/Invalid/);
-      });
-
-      it("should throw on unsupported field name", function() {
-        expect(function() {
-          client.send({bleh: "bah"}, function(){});
-        }).to.Throw(/Unsupported/);
-      });
-
-      it("should call passed callback on success", function() {
-        var cb = sandbox.spy();
-        var fakeResponseData = {description: "confusing"};
-        client.send({category: "confusing"}, cb);
-
-        requests[0].respond(200, {"Content-Type": "application/json"},
-                            JSON.stringify(fakeResponseData));
-
-        sinon.assert.calledOnce(cb);
-        sinon.assert.calledWithExactly(cb, null, fakeResponseData);
-      });
-
-      it("should call passed callback on error", function() {
-        var cb = sandbox.spy();
-        var fakeErrorData = {error: true};
-        client.send({category: "confusing"}, cb);
-
-        requests[0].respond(400, {"Content-Type": "application/json"},
-                            JSON.stringify(fakeErrorData));
-
-        sinon.assert.calledOnce(cb);
-        sinon.assert.calledWithExactly(cb, sinon.match(function(err) {
-          return /Bad Request/.test(err);
-        }));
-      });
-    });
-  });
-});
deleted file mode 100644
--- a/browser/components/loop/test/shared/feedbackStore_test.js
+++ /dev/null
@@ -1,121 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-describe("loop.store.FeedbackStore", function () {
-  "use strict";
-
-  var expect = chai.expect;
-  var sharedActions = loop.shared.actions;
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
-  var sandbox, dispatcher, store, feedbackClient;
-
-  beforeEach(function() {
-    sandbox = sinon.sandbox.create();
-
-    dispatcher = new loop.Dispatcher();
-
-    feedbackClient = new loop.FeedbackAPIClient("http://invalid", {
-      product: "Loop"
-    });
-
-    store = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: feedbackClient
-    });
-  });
-
-  afterEach(function() {
-    sandbox.restore();
-  });
-
-  describe("#constructor", function() {
-    it("should throw an error if feedbackClient is missing", function() {
-      expect(function() {
-        new loop.store.FeedbackStore(dispatcher);
-      }).to.Throw(/feedbackClient/);
-    });
-
-    it("should set the store to the INIT feedback state", function() {
-      var fakeStore = new loop.store.FeedbackStore(dispatcher, {
-        feedbackClient: feedbackClient
-      });
-
-      expect(fakeStore.getStoreState("feedbackState"))
-          .eql(FEEDBACK_STATES.INIT);
-    });
-  });
-
-  describe("#requireFeedbackDetails", function() {
-    it("should transition to DETAILS state", function() {
-      store.requireFeedbackDetails(new sharedActions.RequireFeedbackDetails());
-
-      expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.DETAILS);
-    });
-  });
-
-  describe("#sendFeedback", function() {
-    var sadFeedbackData = {
-      happy: false,
-      category: "fakeCategory",
-      description: "fakeDescription"
-    };
-
-    beforeEach(function() {
-      store.requireFeedbackDetails();
-    });
-
-    it("should send feedback data over the feedback client", function() {
-      sandbox.stub(feedbackClient, "send");
-
-      store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
-
-      sinon.assert.calledOnce(feedbackClient.send);
-      sinon.assert.calledWithMatch(feedbackClient.send, sadFeedbackData);
-    });
-
-    it("should transition to PENDING state", function() {
-      sandbox.stub(feedbackClient, "send");
-
-      store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
-
-      expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.PENDING);
-    });
-
-    it("should transition to SENT state on successful submission", function(done) {
-      sandbox.stub(feedbackClient, "send", function(data, cb) {
-        cb(null);
-      });
-
-      store.once("change:feedbackState", function() {
-        expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.SENT);
-        done();
-      });
-
-      store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
-    });
-
-    it("should transition to FAILED state on failed submission", function(done) {
-      sandbox.stub(feedbackClient, "send", function(data, cb) {
-        cb(new Error("failed"));
-      });
-
-      store.once("change:feedbackState", function() {
-        expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.FAILED);
-        done();
-      });
-
-      store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
-    });
-  });
-
-  describe("feedbackComplete", function() {
-    it("should reset the store state", function() {
-      store.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
-
-      store.feedbackComplete();
-
-      expect(store.getStoreState()).eql({
-        feedbackState: FEEDBACK_STATES.INIT
-      });
-    });
-  });
-});
deleted file mode 100644
--- a/browser/components/loop/test/shared/feedbackViews_test.js
+++ /dev/null
@@ -1,191 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-describe("loop.shared.views.FeedbackView", function() {
-  "use strict";
-
-  var expect = chai.expect;
-  var l10n = navigator.mozL10n || document.mozL10n;
-  var TestUtils = React.addons.TestUtils;
-  var sharedActions = loop.shared.actions;
-  var sharedViews = loop.shared.views;
-
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
-  var sandbox, comp, dispatcher, fakeFeedbackClient, feedbackStore;
-
-  beforeEach(function() {
-    sandbox = sinon.sandbox.create();
-    dispatcher = new loop.Dispatcher();
-    fakeFeedbackClient = {send: sandbox.stub()};
-    feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-      feedbackClient: fakeFeedbackClient
-    });
-    loop.store.StoreMixin.register({feedbackStore: feedbackStore});
-    comp = TestUtils.renderIntoDocument(
-      React.createElement(sharedViews.FeedbackView));
-  });
-
-  afterEach(function() {
-    sandbox.restore();
-  });
-
-  // local test helpers
-  function clickHappyFace(component) {
-    var happyFace = component.getDOMNode().querySelector(".face-happy");
-    TestUtils.Simulate.click(happyFace);
-  }
-
-  function clickSadFace(component) {
-    var sadFace = component.getDOMNode().querySelector(".face-sad");
-    TestUtils.Simulate.click(sadFace);
-  }
-
-  function fillSadFeedbackForm(component, category, text) {
-    TestUtils.Simulate.change(
-      component.getDOMNode().querySelector("[value='" + category + "']"));
-
-    if (text) {
-      TestUtils.Simulate.change(
-        component.getDOMNode().querySelector("[name='description']"), {
-          target: {value: "fake reason"}
-        });
-    }
-  }
-
-  function submitSadFeedbackForm(component, category, text) {
-    TestUtils.Simulate.submit(component.getDOMNode().querySelector("form"));
-  }
-
-  describe("Happy feedback", function() {
-    it("should dispatch a SendFeedback action", function() {
-      var dispatch = sandbox.stub(dispatcher, "dispatch");
-
-      clickHappyFace(comp);
-
-      sinon.assert.calledWithMatch(dispatch, new sharedActions.SendFeedback({
-        happy: true,
-        category: "",
-        description: ""
-      }));
-    });
-
-    it("should thank the user once feedback data is sent", function() {
-      feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
-
-      expect(comp.getDOMNode().querySelectorAll(".thank-you")).not.eql(null);
-      expect(comp.getDOMNode().querySelector("button.fx-embedded-btn-back"))
-        .eql(null);
-    });
-
-    it("should not display the countdown text if noCloseText is true", function() {
-      comp = TestUtils.renderIntoDocument(
-        React.createElement(sharedViews.FeedbackView, {
-          noCloseText: true
-        }));
-
-      expect(comp.getDOMNode().querySelector(".info.thank-you")).eql(null);
-    });
-  });
-
-  describe("Sad feedback", function() {
-    it("should bring the user to feedback form when clicking on the sad face",
-      function() {
-        clickSadFace(comp);
-
-        expect(comp.getDOMNode().querySelectorAll("form")).not.eql(null);
-      });
-
-    it("should render a back button", function() {
-      feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
-
-      expect(comp.getDOMNode().querySelector("button.fx-embedded-btn-back"))
-        .not.eql(null);
-    });
-
-    it("should reset the view when clicking the back button", function() {
-      feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
-
-      TestUtils.Simulate.click(
-        comp.getDOMNode().querySelector("button.fx-embedded-btn-back"));
-
-      expect(comp.getDOMNode().querySelector(".faces")).not.eql(null);
-    });
-
-    it("should disable the form submit button when no category is chosen",
-      function() {
-        clickSadFace(comp);
-
-        expect(comp.getDOMNode().querySelector("form button").disabled).eql(true);
-      });
-
-    it("should disable the form submit button when the 'other' category is " +
-       "chosen but no description has been entered yet",
-      function() {
-        clickSadFace(comp);
-        fillSadFeedbackForm(comp, "other");
-
-        expect(comp.getDOMNode().querySelector("form button").disabled).eql(true);
-      });
-
-    it("should enable the form submit button when the 'other' category is " +
-       "chosen and a description is entered",
-      function() {
-        clickSadFace(comp);
-        fillSadFeedbackForm(comp, "other", "fake");
-
-        expect(comp.getDOMNode().querySelector("form button").disabled).eql(false);
-      });
-
-    it("should enable the form submit button once a predefined category is " +
-       "chosen",
-      function() {
-        clickSadFace(comp);
-
-        fillSadFeedbackForm(comp, "confusing");
-
-        expect(comp.getDOMNode().querySelector("form button").disabled).eql(false);
-      });
-
-    it("should send feedback data when the form is submitted", function() {
-      var dispatch = sandbox.stub(dispatcher, "dispatch");
-      feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
-      fillSadFeedbackForm(comp, "confusing");
-
-      submitSadFeedbackForm(comp);
-
-      sinon.assert.calledOnce(dispatch);
-      sinon.assert.calledWithMatch(dispatch, new sharedActions.SendFeedback({
-        happy: false,
-        category: "confusing",
-        description: ""
-      }));
-    });
-
-    it("should send feedback data when user has entered a custom description",
-      function() {
-        clickSadFace(comp);
-
-        fillSadFeedbackForm(comp, "other", "fake reason");
-        submitSadFeedbackForm(comp);
-
-        sinon.assert.calledOnce(fakeFeedbackClient.send);
-        sinon.assert.calledWith(fakeFeedbackClient.send, {
-          happy: false,
-          category: "other",
-          description: "fake reason"
-        });
-      });
-
-    it("should thank the user when feedback data has been sent", function() {
-      fakeFeedbackClient.send = function(data, cb) {
-        cb();
-      };
-      clickSadFace(comp);
-      fillSadFeedbackForm(comp, "confusing");
-      submitSadFeedbackForm(comp);
-
-      expect(comp.getDOMNode().querySelectorAll(".thank-you")).not.eql(null);
-    });
-  });
-});
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -47,47 +47,41 @@
   </script>
 
   <!-- App scripts -->
   <script src="../../content/shared/js/utils.js"></script>
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/mixins.js"></script>
   <script src="../../content/shared/js/crypto.js"></script>
   <script src="../../content/shared/js/websocket.js"></script>
-  <script src="../../content/shared/js/feedbackApiClient.js"></script>
   <script src="../../content/shared/js/validate.js"></script>
   <script src="../../content/shared/js/actions.js"></script>
   <script src="../../content/shared/js/dispatcher.js"></script>
   <script src="../../content/shared/js/otSdkDriver.js"></script>
   <script src="../../content/shared/js/store.js"></script>
   <script src="../../content/shared/js/roomStates.js"></script>
   <script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
   <script src="../../content/shared/js/activeRoomStore.js"></script>
   <script src="../../content/shared/js/conversationStore.js"></script>
-  <script src="../../content/shared/js/feedbackStore.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/textChatStore.js"></script>
   <script src="../../content/shared/js/textChatView.js"></script>
-  <script src="../../content/shared/js/feedbackViews.js"></script>
 
   <!-- Test scripts -->
   <script src="models_test.js"></script>
   <script src="mixins_test.js"></script>
   <script src="utils_test.js"></script>
   <script src="crypto_test.js"></script>
   <script src="views_test.js"></script>
   <script src="websocket_test.js"></script>
-  <script src="feedbackApiClient_test.js"></script>
-  <script src="feedbackViews_test.js"></script>
   <script src="validate_test.js"></script>
   <script src="dispatcher_test.js"></script>
   <script src="activeRoomStore_test.js"></script>
   <script src="fxOSActiveRoomStore_test.js"></script>
   <script src="conversationStore_test.js"></script>
-  <script src="feedbackStore_test.js"></script>
   <script src="otSdkDriver_test.js"></script>
   <script src="store_test.js"></script>
   <script src="textChatStore_test.js"></script>
   <script src="textChatView_test.js"></script>
   <script>
     describe("Uncaught Error Check", function() {
       it("should load the tests without errors", function() {
         chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -44,29 +44,26 @@
     chai.config.includeStack = true;
     mocha.setup({ui: 'bdd', timeout: 10000});
   </script>
   <!-- App scripts -->
   <script src="../../content/shared/js/utils.js"></script>
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/mixins.js"></script>
   <script src="../../content/shared/js/websocket.js"></script>
-  <script src="../../content/shared/js/feedbackApiClient.js"></script>
   <script src="../../content/shared/js/actions.js"></script>
   <script src="../../content/shared/js/validate.js"></script>
   <script src="../../content/shared/js/dispatcher.js"></script>
   <script src="../../content/shared/js/store.js"></script>
   <script src="../../content/shared/js/roomStates.js"></script>
   <script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
   <script src="../../content/shared/js/activeRoomStore.js"></script>
-  <script src="../../content/shared/js/feedbackStore.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/textChatStore.js"></script>
   <script src="../../content/shared/js/textChatView.js"></script>
-  <script src="../../content/shared/js/feedbackViews.js"></script>
   <script src="../../content/shared/js/otSdkDriver.js"></script>
   <script src="../../standalone/content/js/multiplexGum.js"></script>
   <script src="../../standalone/content/js/standaloneAppStore.js"></script>
   <script src="../../standalone/content/js/standaloneClient.js"></script>
   <script src="../../standalone/content/js/standaloneMozLoop.js"></script>
   <script src="../../standalone/content/js/fxOSMarketplace.js"></script>
   <script src="../../standalone/content/js/standaloneRoomViews.js"></script>
   <script src="../../standalone/content/js/standaloneMetricsStore.js"></script>
@@ -83,17 +80,17 @@
     describe("Uncaught Error Check", function() {
       it("should load the tests without errors", function() {
         chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
       });
     });
 
     describe("Unexpected Warnings Check", function() {
       it("should long only the warnings we expect", function() {
-        chai.expect(caughtWarnings.length).to.eql(15);
+        chai.expect(caughtWarnings.length).to.eql(11);
       });
     });
 
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
     });
 </script>
 </body>
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -10,35 +10,31 @@ describe("loop.standaloneRoomViews", fun
 
   var ROOM_STATES = loop.store.ROOM_STATES;
   var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
   var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
 
-  var sandbox, dispatcher, activeRoomStore, feedbackStore, dispatch;
+  var sandbox, dispatcher, activeRoomStore, dispatch;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     dispatcher = new loop.Dispatcher();
     dispatch = sandbox.stub(dispatcher, "dispatch");
     activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       mozLoop: {},
       sdkDriver: {}
     });
     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();
   });
@@ -496,48 +492,16 @@ describe("loop.standaloneRoomViews", fun
 
           TestUtils.Simulate.click(getLeaveButton(view));
 
           sinon.assert.calledOnce(dispatch);
           sinon.assert.calledWithExactly(dispatch, new sharedActions.LeaveRoom());
         });
       });
 
-      describe("Feedback", function() {
-        beforeEach(function() {
-          activeRoomStore.setStoreState({
-            roomState: ROOM_STATES.ENDED,
-            used: true
-          });
-        });
-
-        it("should display a feedback form when the user leaves the room",
-          function() {
-            expect(view.getDOMNode().querySelector(".faces")).not.eql(null);
-          });
-
-        it("should dispatch a `FeedbackComplete` action after feedback is sent",
-          function() {
-            feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
-
-            sandbox.clock.tick(
-              loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS * 1000 + 1000);
-
-            sinon.assert.calledOnce(dispatch);
-            sinon.assert.calledWithExactly(dispatch, new sharedActions.FeedbackComplete());
-          });
-
-        it("should NOT display a feedback form if the room has not been used",
-          function() {
-            activeRoomStore.setStoreState({used: false});
-            expect(view.getDOMNode().querySelector(".faces")).eql(null);
-          });
-
-      });
-
       describe("Mute", function() {
         it("should render a local avatar if video is muted",
           function() {
             activeRoomStore.setStoreState({
               roomState: ROOM_STATES.SESSION_CONNECTED,
               videoMuted: true
             });
 
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -18,21 +18,16 @@ describe("loop.webapp", function() {
       fakeAudioXHR,
       dispatcher,
       WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     dispatcher = new loop.Dispatcher();
     notifications = new sharedModels.NotificationCollection();
-    loop.store.StoreMixin.register({
-      feedbackStore: new loop.store.FeedbackStore(dispatcher, {
-        feedbackClient: {}
-      })
-    });
 
     stubGetPermsAndCacheMedia = sandbox.stub(
       loop.standaloneMedia._MultiplexGum.prototype, "getPermsAndCacheMedia");
 
     fakeAudioXHR = {
       open: sinon.spy(),
       send: function() {},
       abort: function() {},
@@ -49,17 +44,16 @@ describe("loop.webapp", function() {
 
   afterEach(function() {
     sandbox.restore();
   });
 
   describe("#init", function() {
     beforeEach(function() {
       sandbox.stub(React, "render");
-      loop.config.feedbackApiUrl = "http://fake.invalid";
       sandbox.stub(loop.Dispatcher.prototype, "dispatch");
     });
 
     it("should create the WebappRootView", function() {
       loop.webapp.init();
 
       sinon.assert.calledOnce(React.render);
       sinon.assert.calledWith(React.render,
@@ -1072,20 +1066,16 @@ describe("loop.webapp", function() {
             sdk: {},
             onAfterFeedbackReceived: function(){}
           }));
     });
 
     it("should render a ConversationView", function() {
       TestUtils.findRenderedComponentWithType(view, sharedViews.ConversationView);
     });
-
-    it("should render a FeedbackView", function() {
-      TestUtils.findRenderedComponentWithType(view, sharedViews.FeedbackView);
-    });
   });
 
   describe("PromoteFirefoxView", function() {
     describe("#render", function() {
       it("should not render when using Firefox", function() {
         var comp = TestUtils.renderIntoDocument(
           React.createElement(loop.webapp.PromoteFirefoxView, {
             isFirefox: true
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -37,33 +37,31 @@
       window.OTProperties.configURL = window.OTProperties.assetURL + 'js/dynamic_config.min.js';
     </script>
     <script src="../content/js/multiplexGum.js"></script>
     <script src="../content/shared/libs/sdk.js"></script>
     <script src="../content/shared/libs/react-0.12.2.js"></script>
     <script src="../content/shared/libs/jquery-2.1.4.js"></script>
     <script src="../content/shared/libs/lodash-3.9.3.js"></script>
     <script src="../content/shared/libs/backbone-1.2.1.js"></script>
-    <script src="../content/shared/js/feedbackApiClient.js"></script>
     <script src="../content/shared/js/actions.js"></script>
     <script src="../content/shared/js/utils.js"></script>
     <script src="../content/shared/js/models.js"></script>
     <script src="../content/shared/js/mixins.js"></script>
     <script src="../content/shared/js/websocket.js"></script>
     <script src="../content/shared/js/validate.js"></script>
     <script src="../content/shared/js/dispatcher.js"></script>
     <script src="../content/shared/js/store.js"></script>
     <script src="../content/shared/js/conversationStore.js"></script>
     <script src="../content/shared/js/roomStates.js"></script>
     <script src="../content/shared/js/fxOSActiveRoomStore.js"></script>
     <script src="../content/shared/js/activeRoomStore.js"></script>
-    <script src="../content/shared/js/feedbackStore.js"></script>
     <script src="../content/shared/js/views.js"></script>
-    <script src="../content/shared/js/feedbackViews.js"></script>
     <script src="../content/shared/js/textChatStore.js"></script>
+    <script src="../content/js/feedbackViews.js"></script>
     <script src="../content/shared/js/textChatView.js"></script>
     <script src="../content/js/roomStore.js"></script>
     <script src="../content/js/roomViews.js"></script>
     <script src="../content/js/conversationViews.js"></script>
     <script src="../content/js/client.js"></script>
     <script src="../standalone/content/js/multiplexGum.js"></script>
     <script src="../standalone/content/js/webapp.js"></script>
     <script src="../standalone/content/js/standaloneRoomViews.js"></script>
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -27,23 +27,22 @@
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var StandaloneRoomView      = loop.standaloneRoomViews.StandaloneRoomView;
 
   // 3. Shared components
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
-  var FeedbackView = loop.shared.views.FeedbackView;
+  var FeedbackView = loop.feedbackViews.FeedbackView;
   var Checkbox = loop.shared.views.Checkbox;
   var TextChatView = loop.shared.views.chat.TextChatView;
 
   // Store constants
   var ROOM_STATES = loop.store.ROOM_STATES;
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Local helpers
   function returnTrue() {
     return true;
   }
 
   function returnFalse() {
@@ -71,24 +70,16 @@
     }
     window.removeEventListener(eventName, func);
   };
 
   loop.shared.mixins.setRootObject(rootObject);
 
   var dispatcher = new loop.Dispatcher();
 
-  // Feedback API client configured to send data to the stage input server,
-  // which is available at https://input.allizom.org
-  var stageFeedbackApiClient = new loop.FeedbackAPIClient(
-    "https://input.allizom.org/api/v1/feedback", {
-      product: "Loop"
-    }
-  );
-
   var mockSDK = _.extend({
     sendTextChatMessage: function(message) {
       dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
         message: message.message
       }));
     }
   }, Backbone.Events);
 
@@ -276,19 +267,16 @@
     remoteVideoEnabled: false,
     mediaConnected: true
   });
   var desktopRemoteFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
     activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
   });
 
-  var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-    feedbackClient: stageFeedbackApiClient
-  });
   var conversationStore = new loop.store.ConversationStore(dispatcher, {
     client: {},
     mozLoop: navigator.mozLoop,
     sdkDriver: mockSDK
   });
   var textChatStore = new loop.store.TextChatStore(dispatcher, {
     sdkDriver: mockSDK
   });
@@ -349,17 +337,16 @@
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Cool",
     sentTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
     conversationStore: conversationStore,
-    feedbackStore: feedbackStore,
     textChatStore: textChatStore
   });
 
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
 
   var mockContact = {
@@ -856,34 +843,23 @@
                   remoteVideoEnabled: false, 
                   video: {enabled: true}})
               )
             )
 
           ), 
 
           React.createElement(Section, {name: "FeedbackView"}, 
-            React.createElement("p", {className: "note"}, 
-              React.createElement("strong", null, "Note:"), " For the useable demo, you can access submitted data at ", 
-              React.createElement("a", {href: "https://input.allizom.org/"}, "input.allizom.org"), "."
+            React.createElement("p", {className: "note"}
             ), 
             React.createElement(Example, {dashed: true, 
                      style: {width: "300px", height: "272px"}, 
                      summary: "Default (useable demo)"}, 
-              React.createElement(FeedbackView, {feedbackStore: feedbackStore})
-            ), 
-            React.createElement(Example, {dashed: true, 
-                     style: {width: "300px", height: "272px"}, 
-                     summary: "Detailed form"}, 
-              React.createElement(FeedbackView, {feedbackState: FEEDBACK_STATES.DETAILS, feedbackStore: feedbackStore})
-            ), 
-            React.createElement(Example, {dashed: true, 
-                     style: {width: "300px", height: "272px"}, 
-                     summary: "Thank you!"}, 
-              React.createElement(FeedbackView, {feedbackState: FEEDBACK_STATES.SENT, feedbackStore: feedbackStore})
+              React.createElement(FeedbackView, {mozLoop: {}, 
+                            onAfterFeedbackReceived: function() {}})
             )
           ), 
 
           React.createElement(Section, {name: "AlertMessages"}, 
             React.createElement(Example, {summary: "Various alerts"}, 
               React.createElement("div", {className: "alert alert-warning"}, 
                 React.createElement("button", {className: "close"}), 
                 React.createElement("p", {className: "message"}, 
@@ -921,16 +897,17 @@
               height: 254, 
               summary: "Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)", 
               width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
+                  onCallTerminated: function(){}, 
                   roomState: ROOM_STATES.INIT, 
                   roomStore: invitationRoomStore})
               )
             ), 
 
             React.createElement(FramedExample, {
               dashed: true, 
               height: 394, 
@@ -938,56 +915,60 @@
               width: 298}, 
               /* Hide scrollbars here. Rotating loading div overflows and causes
                scrollbars to appear */
               React.createElement("div", {className: "fx-embedded overflow-hidden"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
+                  onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                   roomStore: desktopRoomStoreLoading})
               )
             ), 
 
             React.createElement(FramedExample, {height: 254, 
                            summary: "Desktop room conversation"}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
+                  onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                   roomStore: roomStore})
               )
             ), 
 
             React.createElement(FramedExample, {dashed: true, 
                            height: 394, 
                            summary: "Desktop room conversation local face-mute", 
                            width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   mozLoop: navigator.mozLoop, 
+                  onCallTerminated: function(){}, 
                   remotePosterUrl: "sample-img/video-screen-remote.png", 
                   roomStore: desktopLocalFaceMuteRoomStore})
               )
             ), 
 
             React.createElement(FramedExample, {dashed: true, height: 394, 
                            summary: "Desktop room conversation remote face-mute", 
                            width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
                   localPosterUrl: "sample-img/video-screen-local.png", 
                   mozLoop: navigator.mozLoop, 
+                  onCallTerminated: function(){}, 
                   roomStore: desktopRemoteFaceMuteRoomStore})
               )
             )
           ), 
 
           React.createElement(Section, {name: "StandaloneRoomView"}, 
             React.createElement(FramedExample, {cssClass: "standalone", 
                            dashed: true, 
@@ -1169,30 +1150,16 @@
                   dispatcher: dispatcher, 
                   isFirefox: false})
               )
             ), 
 
             React.createElement(FramedExample, {cssClass: "standalone", 
                            dashed: true, 
                            height: 483, 
-                           summary: "Standalone room conversation (feedback)", 
-                           width: 644}, 
-              React.createElement("div", {className: "standalone"}, 
-                React.createElement(StandaloneRoomView, {
-                  activeRoomStore: endedRoomStore, 
-                  dispatcher: dispatcher, 
-                  feedbackStore: feedbackStore, 
-                  isFirefox: false})
-              )
-            ), 
-
-            React.createElement(FramedExample, {cssClass: "standalone", 
-                           dashed: true, 
-                           height: 483, 
                            summary: "Standalone room conversation (failed)", 
                            width: 644}, 
               React.createElement("div", {className: "standalone"}, 
                 React.createElement(StandaloneRoomView, {
                   activeRoomStore: failedRoomStore, 
                   dispatcher: dispatcher, 
                   isFirefox: false})
               )
@@ -1310,17 +1277,17 @@
         setTimeout(waitForQueuedFrames, 500);
         return;
       }
       // Put the title back, in case views changed it.
       document.title = "Loop UI Components Showcase";
 
       // This simulates the mocha layout for errors which means we can run
       // this alongside our other unit tests but use the same harness.
-      var expectedWarningsCount = 24;
+      var expectedWarningsCount = 23;
       var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
       if (uncaughtError || warningsMismatch) {
         $("#results").append("<div class='failures'><em>" +
           (!!(uncaughtError && warningsMismatch) ? 2 : 1) + "</em></div>");
         if (warningsMismatch) {
           $("#results").append("<li class='test fail'>" +
             "<h2>Unexpected number of warnings detected in UI-Showcase</h2>" +
             "<pre class='error'>Got: " + caughtWarnings.length + "\n" +
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -27,23 +27,22 @@
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var StandaloneRoomView      = loop.standaloneRoomViews.StandaloneRoomView;
 
   // 3. Shared components
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
-  var FeedbackView = loop.shared.views.FeedbackView;
+  var FeedbackView = loop.feedbackViews.FeedbackView;
   var Checkbox = loop.shared.views.Checkbox;
   var TextChatView = loop.shared.views.chat.TextChatView;
 
   // Store constants
   var ROOM_STATES = loop.store.ROOM_STATES;
-  var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Local helpers
   function returnTrue() {
     return true;
   }
 
   function returnFalse() {
@@ -71,24 +70,16 @@
     }
     window.removeEventListener(eventName, func);
   };
 
   loop.shared.mixins.setRootObject(rootObject);
 
   var dispatcher = new loop.Dispatcher();
 
-  // Feedback API client configured to send data to the stage input server,
-  // which is available at https://input.allizom.org
-  var stageFeedbackApiClient = new loop.FeedbackAPIClient(
-    "https://input.allizom.org/api/v1/feedback", {
-      product: "Loop"
-    }
-  );
-
   var mockSDK = _.extend({
     sendTextChatMessage: function(message) {
       dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
         message: message.message
       }));
     }
   }, Backbone.Events);
 
@@ -276,19 +267,16 @@
     remoteVideoEnabled: false,
     mediaConnected: true
   });
   var desktopRemoteFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop,
     activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
   });
 
-  var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-    feedbackClient: stageFeedbackApiClient
-  });
   var conversationStore = new loop.store.ConversationStore(dispatcher, {
     client: {},
     mozLoop: navigator.mozLoop,
     sdkDriver: mockSDK
   });
   var textChatStore = new loop.store.TextChatStore(dispatcher, {
     sdkDriver: mockSDK
   });
@@ -349,17 +337,16 @@
     contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
     message: "Cool",
     sentTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
     conversationStore: conversationStore,
-    feedbackStore: feedbackStore,
     textChatStore: textChatStore
   });
 
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
 
   var mockContact = {
@@ -857,33 +844,22 @@
                   video={{enabled: true}} />
               </div>
             </FramedExample>
 
           </Section>
 
           <Section name="FeedbackView">
             <p className="note">
-              <strong>Note:</strong> For the useable demo, you can access submitted data at&nbsp;
-              <a href="https://input.allizom.org/">input.allizom.org</a>.
             </p>
             <Example dashed={true}
                      style={{width: "300px", height: "272px"}}
                      summary="Default (useable demo)">
-              <FeedbackView feedbackStore={feedbackStore} />
-            </Example>
-            <Example dashed={true}
-                     style={{width: "300px", height: "272px"}}
-                     summary="Detailed form">
-              <FeedbackView feedbackState={FEEDBACK_STATES.DETAILS} feedbackStore={feedbackStore} />
-            </Example>
-            <Example dashed={true}
-                     style={{width: "300px", height: "272px"}}
-                     summary="Thank you!">
-              <FeedbackView feedbackState={FEEDBACK_STATES.SENT} feedbackStore={feedbackStore}/>
+              <FeedbackView mozLoop={{}}
+                            onAfterFeedbackReceived={function() {}} />
             </Example>
           </Section>
 
           <Section name="AlertMessages">
             <Example summary="Various alerts">
               <div className="alert alert-warning">
                 <button className="close"></button>
                 <p className="message">
@@ -921,16 +897,17 @@
               height={254}
               summary="Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)"
               width={298}>
               <div className="fx-embedded">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
+                  onCallTerminated={function(){}}
                   roomState={ROOM_STATES.INIT}
                   roomStore={invitationRoomStore} />
               </div>
             </FramedExample>
 
             <FramedExample
               dashed={true}
               height={394}
@@ -938,56 +915,60 @@
               width={298}>
               {/* Hide scrollbars here. Rotating loading div overflows and causes
                scrollbars to appear */}
               <div className="fx-embedded overflow-hidden">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
+                  onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomState={ROOM_STATES.HAS_PARTICIPANTS}
                   roomStore={desktopRoomStoreLoading} />
               </div>
             </FramedExample>
 
             <FramedExample height={254}
                            summary="Desktop room conversation">
               <div className="fx-embedded">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
+                  onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomState={ROOM_STATES.HAS_PARTICIPANTS}
                   roomStore={roomStore} />
               </div>
             </FramedExample>
 
             <FramedExample dashed={true}
                            height={394}
                            summary="Desktop room conversation local face-mute"
                            width={298}>
               <div className="fx-embedded">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   mozLoop={navigator.mozLoop}
+                  onCallTerminated={function(){}}
                   remotePosterUrl="sample-img/video-screen-remote.png"
                   roomStore={desktopLocalFaceMuteRoomStore} />
               </div>
             </FramedExample>
 
             <FramedExample dashed={true} height={394}
                            summary="Desktop room conversation remote face-mute"
                            width={298} >
               <div className="fx-embedded">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
                   localPosterUrl="sample-img/video-screen-local.png"
                   mozLoop={navigator.mozLoop}
+                  onCallTerminated={function(){}}
                   roomStore={desktopRemoteFaceMuteRoomStore} />
               </div>
             </FramedExample>
           </Section>
 
           <Section name="StandaloneRoomView">
             <FramedExample cssClass="standalone"
                            dashed={true}
@@ -1169,30 +1150,16 @@
                   dispatcher={dispatcher}
                   isFirefox={false} />
               </div>
             </FramedExample>
 
             <FramedExample cssClass="standalone"
                            dashed={true}
                            height={483}
-                           summary="Standalone room conversation (feedback)"
-                           width={644}>
-              <div className="standalone">
-                <StandaloneRoomView
-                  activeRoomStore={endedRoomStore}
-                  dispatcher={dispatcher}
-                  feedbackStore={feedbackStore}
-                  isFirefox={false} />
-              </div>
-            </FramedExample>
-
-            <FramedExample cssClass="standalone"
-                           dashed={true}
-                           height={483}
                            summary="Standalone room conversation (failed)"
                            width={644} >
               <div className="standalone">
                 <StandaloneRoomView
                   activeRoomStore={failedRoomStore}
                   dispatcher={dispatcher}
                   isFirefox={false} />
               </div>
@@ -1310,17 +1277,17 @@
         setTimeout(waitForQueuedFrames, 500);
         return;
       }
       // Put the title back, in case views changed it.
       document.title = "Loop UI Components Showcase";
 
       // This simulates the mocha layout for errors which means we can run
       // this alongside our other unit tests but use the same harness.
-      var expectedWarningsCount = 24;
+      var expectedWarningsCount = 23;
       var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
       if (uncaughtError || warningsMismatch) {
         $("#results").append("<div class='failures'><em>" +
           (!!(uncaughtError && warningsMismatch) ? 2 : 1) + "</em></div>");
         if (warningsMismatch) {
           $("#results").append("<li class='test fail'>" +
             "<h2>Unexpected number of warnings detected in UI-Showcase</h2>" +
             "<pre class='error'>Got: " + caughtWarnings.length + "\n" +
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -84,43 +84,43 @@
 </hbox>
 
 <!-- Tracking -->
 <groupbox id="trackingGroup" data-category="panePrivacy" hidden="true" align="start">
   <caption><label>&tracking.label;</label></caption>
   <vbox>
     <hbox align="center">
       <checkbox id="privacyDoNotTrackCheckbox"
-                label="&dntTrackingNotOkay.label2;"
-                accesskey="&dntTrackingNotOkay.accesskey;"
+                label="&dntTrackingNotOkay2.label;"
+                accesskey="&dntTrackingNotOkay2.accesskey;"
                 preference="privacy.donottrackheader.enabled"/>
       <label id="doNotTrackInfo"
              class="text-link"
              href="https://www.mozilla.org/dnt">
         &doNotTrackInfo.label;
       </label>
     </hbox>
   </vbox>
   <vbox id="trackingprotectionbox" hidden="true">
     <hbox align="center">
       <checkbox id="trackingProtection"
                 preference="privacy.trackingprotection.enabled"
-                accesskey="&trackingProtection.accesskey;"
-                label="&trackingProtection.label;" />
+                accesskey="&trackingProtection2.accesskey;"
+                label="&trackingProtection2.label;" />
       <label id="trackingProtectionLearnMore"
              class="text-link"
              value="&trackingProtectionLearnMore.label;"/>
     </hbox>
   </vbox>
   <vbox id="trackingprotectionpbmbox">
     <hbox align="center">
       <checkbox id="trackingProtectionPBM"
                 preference="privacy.trackingprotection.pbmode.enabled"
-                accesskey="&trackingProtectionPBM.accesskey;"
-                label="&trackingProtectionPBM.label;" />
+                accesskey="&trackingProtectionPBM2.accesskey;"
+                label="&trackingProtectionPBM2.label;" />
       <label id="trackingProtectionPBMLearnMore"
              class="text-link"
              value="&trackingProtectionPBMLearnMore.label;"/>
     </hbox>
   </vbox>
 </groupbox>
 
 <!-- History -->
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -126,17 +126,16 @@ let gSyncPane = {
 
     this.updateWeavePrefs();
 
     this._initProfileImageUI();
   },
 
   _toggleComputerNameControls: function(editMode) {
     let textbox = document.getElementById("fxaSyncComputerName");
-    textbox.className = editMode ? "" : "plain";
     textbox.disabled = !editMode;
     document.getElementById("fxaChangeDeviceName").hidden = editMode;
     document.getElementById("fxaCancelChangeDeviceName").hidden = !editMode;
     document.getElementById("fxaSaveChangeDeviceName").hidden = !editMode;
   },
 
   _focusComputerNameTextbox: function() {
     let textbox = document.getElementById("fxaSyncComputerName");
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -317,40 +317,37 @@
           <checkbox label="&engine.prefs.label;"
                     accesskey="&engine.prefs.accesskey;"
                     preference="engine.prefs"/>
         </vbox>
         <spacer/>
       </hbox>
     </groupbox>
     <groupbox>
-      <vbox>
-        <caption>
-          <label accesskey="&syncDeviceName.accesskey;"
-                 control="fxaSyncComputerName">
-            &fxaSyncDeviceName.label;
-          </label>
-        </caption>
-        <hbox id="fxaDeviceName">
-          <hbox flex="1">
-            <textbox id="fxaSyncComputerName" class="plain"
-                     disabled="true" flex="1"/>
-          </hbox>
-          <hbox>
-            <button id="fxaChangeDeviceName"
-                    label="&changeSyncDeviceName.label;"/>
-            <button id="fxaCancelChangeDeviceName"
-                    label="&cancelChangeSyncDeviceName.label;"
-                    hidden="true"/>
-            <button id="fxaSaveChangeDeviceName"
-                    label="&saveChangeSyncDeviceName.label;"
-                    hidden="true"/>
-          </hbox>
+      <caption>
+        <label accesskey="&syncDeviceName.accesskey;"
+               control="fxaSyncComputerName">
+          &fxaSyncDeviceName.label;
+        </label>
+      </caption>
+      <hbox id="fxaDeviceName">
+        <hbox flex="1">
+          <textbox id="fxaSyncComputerName" disabled="true" flex="1"/>
         </hbox>
-      </vbox>
+        <hbox>
+          <button id="fxaChangeDeviceName"
+                  label="&changeSyncDeviceName.label;"/>
+          <button id="fxaCancelChangeDeviceName"
+                  label="&cancelChangeSyncDeviceName.label;"
+                  hidden="true"/>
+          <button id="fxaSaveChangeDeviceName"
+                  label="&saveChangeSyncDeviceName.label;"
+                  hidden="true"/>
+        </hbox>
+      </hbox>
     </groupbox>
     <spacer flex="1"/>
     <vbox id="tosPP-small">
       <label id="tosPP-small-ToS" class="text-link">
         &prefs.tosLink.label;
       </label>
       <label id="tosPP-small-PP" class="text-link">
         &fxaPrivacyNotice.link.label;
--- a/browser/components/preferences/privacy.xul
+++ b/browser/components/preferences/privacy.xul
@@ -91,33 +91,33 @@
 
     <!-- Tracking -->
     <groupbox id="trackingGroup" align="start">
       <caption label="&tracking.label;"/>
       <vbox id="trackingprotectionbox" hidden="true">
         <hbox align="center">
           <checkbox id="trackingProtection"
                     preference="privacy.trackingprotection.enabled"
-                    accesskey="&trackingProtection.accesskey;"
-                    label="&trackingProtection.label;" />
+                    accesskey="&trackingProtection2.accesskey;"
+                    label="&trackingProtection2.label;" />
           <image id="trackingProtectionImage"
                  src="chrome://browser/skin/bad-content-blocked-16.png"/>
         </hbox>
         <hbox align="center"
               class="indent">
           <label id="trackingProtectionLearnMore"
                  class="text-link"
                  value="&trackingProtectionLearnMore.label;"/>
         </hbox>
       </vbox>
       <vbox>
         <hbox align="center">
           <checkbox id="privacyDoNotTrackCheckbox"
-                    label="&dntTrackingNotOkay.label2;"
-                    accesskey="&dntTrackingNotOkay.accesskey;"
+                    label="&dntTrackingNotOkay2.label;"
+                    accesskey="&dntTrackingNotOkay2.accesskey;"
                     preference="privacy.donottrackheader.enabled"/>
         </hbox>
         <hbox align="center"
               class="indent">
           <label id="doNotTrackInfo"
                  class="text-link"
                  href="https://www.mozilla.org/dnt">
             &doNotTrackInfo.label;
--- a/browser/devtools/commandline/test/browser_cmd_restart.js
+++ b/browser/devtools/commandline/test/browser_cmd_restart.js
@@ -11,25 +11,51 @@ function test() {
       {
         setup: 'restart',
         check: {
           input:  'restart',
           markup: 'VVVVVVV',
           status: 'VALID',
           args: {
             nocache: { value: false },
+            safemode: { value: false },
           }
         },
       },
       {
         setup: 'restart --nocache',
         check: {
           input:  'restart --nocache',
           markup: 'VVVVVVVVVVVVVVVVV',
           status: 'VALID',
           args: {
             nocache: { value: true },
+            safemode: { value: false },
+          }
+        },
+      },
+      {
+        setup: 'restart --safemode',
+        check: {
+          input:  'restart --safemode',
+          markup: 'VVVVVVVVVVVVVVVVVV',
+          status: 'VALID',
+          args: {
+            nocache: { value: false },
+            safemode: { value: true },
+          }
+        },
+      },
+      {
+        setup: 'restart --safemode --nocache',
+        check: {
+          input:  'restart --safemode --nocache',
+          markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+          status: 'VALID',
+          args: {
+            nocache: { value: true },
+            safemode: { value: true },
           }
         },
       },
     ]);
   }).then(finish, helpers.handleError);
 }
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -135,15 +135,16 @@ skip-if = os == 'linux' # Bug 1172120
 [browser_profiler_tree-view-05.js]
 [browser_profiler_tree-view-06.js]
 [browser_profiler_tree-view-07.js]
 [browser_profiler_tree-view-08.js]
 [browser_profiler_tree-view-09.js]
 [browser_profiler_tree-view-10.js]
 [browser_profiler_tree-view-11.js]
 [browser_timeline-filters-01.js]
+skip-if = true # Bug 1176370
 [browser_timeline-filters-02.js]
 [browser_timeline-waterfall-background.js]
 [browser_timeline-waterfall-generic.js]
 [browser_timeline-waterfall-rerender.js]
 skip-if = true # Bug 1170105
 [browser_timeline-waterfall-sidebar.js]
 skip-if = true # Bug 1161817
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -6,16 +6,17 @@ support-files =
   doc_content_stylesheet.xul
   doc_content_stylesheet_imported.css
   doc_content_stylesheet_imported2.css
   doc_content_stylesheet_linked.css
   doc_content_stylesheet_script.css
   doc_content_stylesheet_xul.css
   doc_copystyles.css
   doc_copystyles.html
+  doc_custom.html
   doc_filter.html
   doc_frame_script.js
   doc_keyframeanimation.html
   doc_keyframeanimation.css
   doc_matched_selectors.html
   doc_media_queries.html
   doc_pseudoelement.html
   doc_sourcemaps.css
@@ -78,16 +79,17 @@ support-files =
 skip-if = e10s # Bug 1039528: "inspect element" contextual-menu doesn't work with e10s
 [browser_ruleview_context-menu-show-mdn-docs-01.js]
 [browser_ruleview_context-menu-show-mdn-docs-02.js]
 [browser_ruleview_context-menu-show-mdn-docs-03.js]
 [browser_ruleview_copy_styles.js]
 [browser_ruleview_cubicbezier-appears-on-swatch-click.js]
 [browser_ruleview_cubicbezier-commit-on-ENTER.js]
 [browser_ruleview_cubicbezier-revert-on-ESC.js]
+[browser_ruleview_custom.js]
 [browser_ruleview_edit-property-commit.js]
 [browser_ruleview_edit-property-computed.js]
 [browser_ruleview_edit-property-increments.js]
 [browser_ruleview_edit-property-order.js]
 [browser_ruleview_edit-property_01.js]
 [browser_ruleview_edit-property_02.js]
 [browser_ruleview_edit-property_03.js]
 [browser_ruleview_edit-selector-commit.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_custom.js
@@ -0,0 +1,80 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = TEST_URL_ROOT + "doc_custom.html";
+
+// Test the display of custom declarations in the rule-view
+
+add_task(function*() {
+  yield addTab(TEST_URI);
+  let {inspector, view} = yield openRuleView();
+
+  yield simpleCustomOverride(inspector, view);
+  yield importantCustomOverride(inspector, view);
+  yield disableCustomOverride(inspector, view);
+});
+
+function* simpleCustomOverride(inspector, view) {
+  yield selectNode("#testidSimple", inspector);
+
+  let elementStyle = view._elementStyle;
+
+  let idRule = elementStyle.rules[1];
+  let idProp = idRule.textProps[0];
+  is(idProp.name, "--background-color",
+     "First ID prop should be --background-color");
+  ok(!idProp.overridden, "ID prop should not be overridden.");
+
+  let classRule = elementStyle.rules[2];
+  let classProp = classRule.textProps[0];
+  is(classProp.name, "--background-color",
+     "First class prop should be --background-color");
+  ok(classProp.overridden, "Class property should be overridden.");
+
+  // Override --background-color by changing the element style.
+  let elementRule = elementStyle.rules[0];
+  elementRule.createProperty("--background-color", "purple", "");
+  yield elementRule._applyingModifications;
+
+  let elementProp = elementRule.textProps[0];
+  is(classProp.name, "--background-color",
+     "First element prop should now be --background-color");
+  ok(!elementProp.overridden,
+     "Element style property should not be overridden");
+  ok(idProp.overridden, "ID property should be overridden");
+  ok(classProp.overridden, "Class property should be overridden");
+}
+
+function* importantCustomOverride(inspector, view) {
+  yield selectNode("#testidImportant", inspector);
+
+  let elementStyle = view._elementStyle;
+
+  let idRule = elementStyle.rules[1];
+  let idProp = idRule.textProps[0];
+  ok(idProp.overridden, "Not-important rule should be overridden.");
+
+  let classRule = elementStyle.rules[2];
+  let classProp = classRule.textProps[0];
+  ok(!classProp.overridden, "Important rule should not be overridden.");
+}
+
+function* disableCustomOverride(inspector, view) {
+  yield selectNode("#testidDisable", inspector);
+
+  let elementStyle = view._elementStyle;
+
+  let idRule = elementStyle.rules[1];
+  let idProp = idRule.textProps[0];
+
+  idProp.setEnabled(false);
+  yield idRule._applyingModifications;
+
+  let classRule = elementStyle.rules[2];
+  let classProp = classRule.textProps[0];
+  ok(!classProp.overridden,
+     "Class prop should not be overridden after id prop was disabled.");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/doc_custom.html
@@ -0,0 +1,33 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+  <head>
+    <style>
+      #testidSimple {
+	--background-color: blue;
+      }
+      .testclassSimple {
+	--background-color: green;
+      }
+
+      .testclassImportant {
+	--background-color: green !important;
+      }
+      #testidImportant {
+	--background-color: blue;
+      }
+
+      #testidDisable {
+	--background-color: blue;
+      }
+      .testclassDisable {
+	--background-color: green;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="testidSimple" class="testclassSimple">Styled Node</div>
+    <div id="testidImportant" class="testclassImportant">Styled Node</div>
+    <div id="testidDisable" class="testclassDisable">Styled Node</div>
+  </body>
+</html>
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -269,38 +269,23 @@ legal_text_tos = Terms of Use
 legal_text_privacy = Privacy Notice
 
 ## LOCALIZATION NOTE (powered_by_beforeLogo, powered_by_afterLogo):
 ## These 2 strings are displayed before and after a 'Telefonica'
 ## logo.
 powered_by_beforeLogo=Powered by
 powered_by_afterLogo=
 
-feedback_call_experience_heading2=How was your conversation?
-feedback_thank_you_heading=Thank you for your feedback!
-feedback_category_list_heading=What made you sad?
-feedback_category_audio_quality=Audio quality
-feedback_category_video_quality=Video quality
-feedback_category_was_disconnected=Was disconnected
-feedback_category_confusing2=Confusing controls
-feedback_category_other2=Other
-feedback_custom_category_text_placeholder=What went wrong?
-feedback_submit_button=Submit
-feedback_back_button=Back
-## LOCALIZATION NOTE (feedback_window_will_close_in2):
-## Semicolon-separated list of plural forms. See:
-## http://developer.mozilla.org/en/docs/Localization_and_Plurals
-## In this item, don't translate the part between {{..}}
-feedback_window_will_close_in2=This window will close in {{countdown}} second;This window will close in {{countdown}} seconds
 ## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
 ## a signed-in to signed-in user call.
 feedback_rejoin_button=Rejoin
 ## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
 ## an abusive user.
 feedback_report_user_button=Report User
+feedback_window_heading=How was your conversation?
 feedback_request_button=Leave Feedback
 
 help_label=Help
 tour_label=Tour
 
 ## LOCALIZATION NOTE(rooms_default_room_name_template): {{conversationLabel}}
 ## will be replaced by a number. For example "Conversation 1" or "Conversation 12".
 rooms_default_room_name_template=Conversation {{conversationLabel}}
--- a/browser/locales/en-US/chrome/browser/preferences/privacy.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/privacy.dtd
@@ -1,23 +1,23 @@
 <!-- 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/. -->
 
 <!ENTITY tracking.label                 "Tracking">
 
-<!ENTITY dntTrackingNotOkay.label2      "Tell sites that I do not want to be tracked">
-<!ENTITY dntTrackingNotOkay.accesskey   "n">
-<!ENTITY trackingProtection.label       "Prevent sites from tracking me">
-<!ENTITY trackingProtection.accesskey   "m">
+<!ENTITY dntTrackingNotOkay2.label     "Tell sites that I do not want to be tracked.">
+<!ENTITY dntTrackingNotOkay2.accesskey "n">
+<!ENTITY doNotTrackInfo.label          "Learn More">
+<!ENTITY trackingProtection2.label     "Prevent sites from tracking me.">
+<!ENTITY trackingProtection2.accesskey "m">
 <!ENTITY trackingProtectionLearnMore.label "Learn more">
-<!ENTITY trackingProtectionPBM.label          "Prevent sites from tracking my online activity in Private Windows">
-<!ENTITY trackingProtectionPBM.accesskey      "y">
+<!ENTITY trackingProtectionPBM2.label          "Prevent sites from tracking my online activity in Private Windows.">
+<!ENTITY trackingProtectionPBM2.accesskey      "y">
 <!ENTITY trackingProtectionPBMLearnMore.label "Learn more">
-<!ENTITY doNotTrackInfo.label           "Learn More">
 
 <!ENTITY  history.label                 "History">
 
 <!ENTITY  locationBar.label             "Location Bar">
 
 <!ENTITY  locbar.suggest.label          "When using the location bar, suggest:">
 <!ENTITY  locbar.history.label          "History">
 <!ENTITY  locbar.history.accesskey      "H">
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -391,21 +391,16 @@ description > html|a {
 #fxaSyncEngines > vbox:first-child {
   margin-right: 80px;
 }
 
 #fxaSyncComputerName {
   margin-left: 0px;
 }
 
-#fxaSyncComputerName.plain {
-  background-color: transparent;
-  opacity: 1;
-}
-
 #tosPP-small-ToS {
   margin-bottom: 1em;
 }
 
 #fxaLoginRejectedWarning {
   list-style-image: url(chrome://browser/skin/warning.svg);
   filter: drop-shadow(0 1px 0 hsla(206, 50%, 10%, .15));
   margin: 4px 8px 0px 0px;
copy from browser/themes/windows/actionicon-tab.png
copy to browser/themes/windows/actionicon-tab-XPVista7.png
index 8437c7655a799dde24f3d78575cbdd1e44f91b45..52169a4808aad9b89bb0102748cba3b0e504e815
GIT binary patch
literal 194
zc%17D@N?(olHy`uVBq!ia0vp^0zfRr!2%?c7hm}fq&hua978G?r(Q4>Yj$98y=d9)
z>Y|lZ_NV&Rj*^XL`=(|5d}P$LH@-;n$ey6!*ttim-M5CC|IgHz^EId7>w%kWKUOn|
zh&HOQh}7#HmtuGP^(W-5>p3yE)iDO9s*63R8_)0CvhsF9NA!8NaQ->D0xP%_w3s*-
u+U=h;qflVUdH+=ZIPSw*;v)ZFNe6wKQK7r@|2d$e7(8A5T-G@yGywqWfl5gL
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6a4be3741fac6448e4716371106c0a31ea7d1a52
GIT binary patch
literal 324
zc$@)50lWT*P)<h;3K|Lk000e1NJLTq001BW001lq1ONa4uCa0i0003CNkl<ZcmeH<
zHA16d7zV#Jw8Ku23*fS7l^U<or9{s2+yJLcodcvr+ITA5Wv~YM<;8WTo^daZ+BB!4
z@UW;^HHGEVIu!w)C?1p#=v;(&st*4_LKs~3A8eZjdH(?>H-84NH_$)h;KXkb5k?!l
z2b1Q(m{(q}ta*@9+1<ESLs}KGn^cdgZ{nCr;)&)IbvhK!Fz<BmtU=CcT$v~5>l$z$
z589Zch!;g>X$s4yIVFe_;z9WU=Y(-ehyNgfg=PQ2HWu>!0~?#KH~1Ow4;v?bg9ug{
zya$t58S~2P$zml%+1<F7Ay#<|NqVThiDQ&_DmkK#L!Lw4almuPImXHGRMr`A&*cIw
Wt4AG1ftB+B0000<MNUMnLSTaM-izA+
--- a/browser/themes/windows/browser-aero.css
+++ b/browser/themes/windows/browser-aero.css
@@ -139,17 +139,17 @@
         #main-window[sizemode="maximized"] #titlebar-max {
           list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore);
         }
 
         #titlebar-close {
           list-style-image: url(chrome://browser/skin/caption-buttons.svg#close);
         }
         #titlebar-close:hover {
-          list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highlight);
+          list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white);
         }
 
         /* the 12px image renders a 10px icon, and the 10px upscaled gets rounded to 12.5, which
          * rounds up to 13px, which makes the icon one pixel too big on 1.25dppx. Fix: */
         @media (min-resolution: 1.20dppx) and (max-resolution: 1.45dppx) {
           .titlebar-button > .toolbarbutton-icon {
             width: 11.5px;
             height: 11.5px;
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1625,26 +1625,41 @@ richlistitem[type~="action"][actiontype=
     color: Highlight;
   }
 }
 
 richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
   list-style-image: url("chrome://browser/skin/actionicon-tab.png");
   -moz-image-region: rect(0, 16px, 11px, 0);
   padding: 0 3px;
+  width: 22px;
+  height: 11px;
+}
+
+@media (min-resolution: 1.1dppx) {
+  richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
+    list-style-image: url("chrome://browser/skin/actionicon-tab@2x.png");
+    -moz-image-region: rect(0, 32px, 22px, 0);
+  }
 }
 
 @media not all and (-moz-os-version: windows-vista),
        not all and (-moz-windows-default-theme) {
   @media not all and (-moz-os-version: windows-win7),
          not all and (-moz-windows-default-theme) {
     richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon {
       -moz-image-region: rect(11px, 16px, 22px, 0);
     }
 
+    @media (min-resolution: 1.1dppx) {
+      richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon {
+        -moz-image-region: rect(22px, 32px, 44px, 0);
+      }
+    }
+
     .ac-comment[selected="true"],
     .ac-url-text[selected="true"],
     .ac-action-text[selected="true"] {
       color: inherit !important;
     }
   }
 }
 
--- a/browser/themes/windows/caption-buttons.svg
+++ b/browser/themes/windows/caption-buttons.svg
@@ -1,12 +1,12 @@
-<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
   <style>
     g {
       stroke: ButtonText;
       stroke-width: 0.9px;
       fill: none;
     }
 
     g:not(#close) {
@@ -16,31 +16,40 @@
     g:not(:target) {
       display: none;
     }
 
     use:target > g {
       display: initial;
     }
 
-    .highlight > g {
+    [id$="-highlight"] > g {
       stroke: HighlightText;
     }
+
+    [id$="-white"] > g {
+      stroke: #fff;
+    }
   </style>
   <g id="close">
     <line x1="1" y1="1" x2="11" y2="11"/>
     <line x1="11" y1="1" x2="1" y2="11"/>
   </g>
   <g id="maximize">
     <rect x="1.5" y="1.5" width="9" height="9"/>
   </g>
   <g id="minimize">
     <line x1="1" y1="5.5" x2="11" y2="5.5"/>
   </g>
   <g id="restore">
     <rect x="1.5" y="3.5" width="7" height="7"/>
     <polyline points="3.5,3.5 3.5,1.5 10.5,1.5 10.5,8.5 8.5,8.5"/>
   </g>
-  <use id="close-highlight" class="highlight" xlink:href="#close"/>
-  <use id="maximize-highlight" class="highlight" xlink:href="#maximize"/>
-  <use id="minimize-highlight" class="highlight" xlink:href="#minimize"/>
-  <use id="restore-highlight" class="highlight" xlink:href="#restore"/>
+  <use id="close-highlight" xlink:href="#close"/>
+  <use id="maximize-highlight" xlink:href="#maximize"/>
+  <use id="minimize-highlight" xlink:href="#minimize"/>
+  <use id="restore-highlight" xlink:href="#restore"/>
+
+  <use id="close-white" xlink:href="#close"/>
+  <use id="maximize-white" xlink:href="#maximize"/>
+  <use id="minimize-white" xlink:href="#minimize"/>
+  <use id="restore-white" xlink:href="#restore"/>
 </svg>
--- a/browser/themes/windows/devedition.css
+++ b/browser/themes/windows/devedition.css
@@ -235,9 +235,23 @@
 }
 
 @media (-moz-os-version: windows-win10) {
   /* Always keep draggable space on the sides of tabs since there is no top margin on Win10 */
   #main-window .tabbrowser-arrowscrollbox > .arrowscrollbox-scrollbox {
     padding-left: 15px;
     padding-right: 15px;
   }
+
+  /* Force white caption buttons for the dark theme on Windows 10 */
+  :root[devtoolstheme="dark"] #titlebar-min {
+    list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-white);
+  }
+  :root[devtoolstheme="dark"] #titlebar-max {
+    list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-white);
+  }
+  #main-window[devtoolstheme="dark"][sizemode="maximized"] #titlebar-max {
+    list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-white);
+  }
+  :root[devtoolstheme="dark"] #titlebar-close {
+    list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white);
+  }
 }
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -17,16 +17,18 @@ browser.jar:
         skin/classic/browser/aboutNetError_alert.svg                 (../shared/aboutNetError_alert.svg)
         skin/classic/browser/aboutSocialError.css                    (../shared/aboutSocialError.css)
 *       skin/classic/browser/aboutProviderDirectory.css              (../shared/aboutProviderDirectory.css)
 #ifdef MOZ_SERVICES_SYNC
         skin/classic/browser/aboutSyncTabs.css
 #endif
         skin/classic/browser/aboutTabCrashed.css                     (../shared/aboutTabCrashed.css)
         skin/classic/browser/actionicon-tab.png
+        skin/classic/browser/actionicon-tab@2x.png
+        skin/classic/browser/actionicon-tab-XPVista7.png
         skin/classic/browser/addons/addon-install-blocked.svg        (../shared/addons/addon-install-blocked.svg)
         skin/classic/browser/addons/addon-install-confirm.svg        (../shared/addons/addon-install-confirm.svg)
         skin/classic/browser/addons/addon-install-downloading.svg    (../shared/addons/addon-install-downloading.svg)
         skin/classic/browser/addons/addon-install-error.svg          (../shared/addons/addon-install-error.svg)
         skin/classic/browser/addons/addon-install-installed.svg      (../shared/addons/addon-install-installed.svg)
         skin/classic/browser/addons/addon-install-restart.svg        (../shared/addons/addon-install-restart.svg)
         skin/classic/browser/addons/addon-install-warning.svg        (../shared/addons/addon-install-warning.svg)
         skin/classic/browser/addons/addon-install-anchor.svg         (../shared/addons/addon-install-anchor.svg)
@@ -640,16 +642,17 @@ browser.jar:
 % override chrome://browser/skin/places/unsortedBookmarks.png         chrome://browser/skin/places/unsortedBookmarks-XP.png             os=WINNT osversion<6
 % override chrome://browser/skin/preferences/alwaysAsk.png            chrome://browser/skin/preferences/alwaysAsk-XP.png                os=WINNT osversion<6
 % override chrome://browser/skin/preferences/application.png          chrome://browser/skin/preferences/application-XP.png              os=WINNT osversion<6
 % override chrome://browser/skin/preferences/mail.png                 chrome://browser/skin/preferences/mail-XP.png                     os=WINNT osversion<6
 % override chrome://browser/skin/preferences/Options.png              chrome://browser/skin/preferences/Options-XP.png                  os=WINNT osversion<6
 % override chrome://browser/skin/preferences/saveFile.png             chrome://browser/skin/preferences/saveFile-XP.png                 os=WINNT osversion<6
 % override chrome://browser/skin/tabbrowser/tab-separator.png         chrome://browser/skin/tabbrowser/tab-separator-XP.png             os=WINNT osversion<6
 
+% override chrome://browser/skin/actionicon-tab.png                   chrome://browser/skin/actionicon-tab-XPVista7.png                 os=WINNT osversion<=6.1
 % override chrome://browser/skin/sync-horizontalbar.png               chrome://browser/skin/sync-horizontalbar-XPVista7.png             os=WINNT osversion<=6.1
 % override chrome://browser/skin/sync-horizontalbar@2x.png            chrome://browser/skin/sync-horizontalbar-XPVista7@2x.png          os=WINNT osversion<=6.1
 % override chrome://browser/skin/syncProgress-horizontalbar.png       chrome://browser/skin/syncProgress-horizontalbar-XPVista7.png     os=WINNT osversion<=6.1
 % override chrome://browser/skin/syncProgress-horizontalbar@2x.png    chrome://browser/skin/syncProgress-horizontalbar-XPVista7@2x.png  os=WINNT osversion<=6.1
 % override chrome://browser/skin/syncProgress-toolbar.png             chrome://browser/skin/syncProgress-toolbar-XPVista7.png           os=WINNT osversion<=6.1
 % override chrome://browser/skin/syncProgress-toolbar@2x.png          chrome://browser/skin/syncProgress-toolbar-XPVista7@2x.png        os=WINNT osversion<=6.1
 % override chrome://browser/skin/toolbarbutton-dropdown-arrow.png     chrome://browser/skin/toolbarbutton-dropdown-arrow-XPVista7.png   os=WINNT osversion<=6.1
 % override chrome://browser/skin/places/autocomplete-star.png         chrome://browser/skin/places/autocomplete-star-XPVista7.png       os=WINNT osversion<=6.1
--- a/build/mach_bootstrap.py
+++ b/build/mach_bootstrap.py
@@ -28,29 +28,35 @@ NO_MERCURIAL_SETUP = '''
 
 mach has detected that you have never run `mach mercurial-setup`.
 
 Running this command will ensure your Mercurial version control tool is up
 to date and optimally configured for a better, more productive experience
 when working on Mozilla projects.
 
 Please run `mach mercurial-setup` now.
+
+Note: `mach mercurial-setup` does not make any changes without prompting
+you first.
 '''.strip()
 
 OLD_MERCURIAL_TOOLS = '''
 *** MERCURIAL CONFIGURATION POTENTIALLY OUT OF DATE ***
 
 mach has detected that it has been a while since you have run
 `mach mercurial-setup`.
 
 Having the latest Mercurial tools and configuration should lead to a better,
 more productive experience when working on Mozilla projects.
 
 Please run `mach mercurial-setup` now.
 
+Reminder: `mach mercurial-setup` does not make any changes without
+prompting you first.
+
 To avoid this message in the future, run `mach mercurial-setup` once a month.
 Or, schedule `mach mercurial-setup --update-only` to run automatically in
 the background at least once a month.
 '''.strip()
 
 MERCURIAL_SETUP_FATAL_INTERVAL = 31 * 24 * 60 * 60
 
 
--- a/layout/inspector/inDOMUtils.cpp
+++ b/layout/inspector/inDOMUtils.cpp
@@ -618,22 +618,27 @@ static void GetOtherValuesForProperty(co
 NS_IMETHODIMP
 inDOMUtils::GetSubpropertiesForCSSProperty(const nsAString& aProperty,
                                            uint32_t* aLength,
                                            char16_t*** aValues)
 {
   nsCSSProperty propertyID =
     nsCSSProps::LookupProperty(aProperty, nsCSSProps::eEnabledForAllContent);
 
-  if (propertyID == eCSSProperty_UNKNOWN ||
-      propertyID == eCSSPropertyExtra_variable) {
+  if (propertyID == eCSSProperty_UNKNOWN) {
     return NS_ERROR_FAILURE;
   }
 
-  nsTArray<nsString> array;
+  if (propertyID == eCSSPropertyExtra_variable) {
+    *aValues = static_cast<char16_t**>(moz_xmalloc(sizeof(char16_t*)));
+    (*aValues)[0] = ToNewUnicode(aProperty);
+    *aLength = 1;
+    return NS_OK;
+  }
+
   if (!nsCSSProps::IsShorthand(propertyID)) {
     *aValues = static_cast<char16_t**>(moz_xmalloc(sizeof(char16_t*)));
     (*aValues)[0] = ToNewUnicode(nsCSSProps::GetStringValue(propertyID));
     *aLength = 1;
     return NS_OK;
   }
 
   // Count up how many subproperties we have.
--- a/layout/inspector/tests/mochitest.ini
+++ b/layout/inspector/tests/mochitest.ini
@@ -9,15 +9,14 @@ support-files =
 [test_bug536379.html]
 [test_bug536379-2.html]
 [test_bug557726.html]
 [test_bug609549.xhtml]
 [test_bug806192.html]
 [test_bug856317.html]
 [test_bug877690.html]
 [test_bug1006595.html]
-[test_bug1046140.html]
 [test_color_to_rgba.html]
 [test_css_property_is_valid.html]
 [test_get_all_style_sheets.html]
 [test_is_valid_css_color.html]
 [test_isinheritableproperty.html]
 [test_selectormatcheselement.html]
--- a/layout/inspector/tests/test_bug1006595.html
+++ b/layout/inspector/tests/test_bug1006595.html
@@ -27,16 +27,19 @@ https://bugzilla.mozilla.org/show_bug.cg
                 "padding-bottom",
                 "padding-left" ],
               "'padding' subproperties");
 
   var displaySubProps = utils.getSubpropertiesForCSSProperty("color");
   arraysEqual(displaySubProps, [ "color" ],
               "'color' subproperties");
 
+  var varProps = utils.getSubpropertiesForCSSProperty("--foo");
+  arraysEqual(varProps, ["--foo"], "'--foo' subproperties");
+
   ok(utils.cssPropertyIsShorthand("padding"), "'padding' is a shorthand")
   ok(!utils.cssPropertyIsShorthand("color"), "'color' is not a shorthand")
 
   ok(utils.cssPropertySupportsType("padding", utils.TYPE_LENGTH),
      "'padding' can be a length");
   ok(!utils.cssPropertySupportsType("padding", utils.TYPE_COLOR),
      "'padding' can't be a color");
 
deleted file mode 100644
--- a/layout/inspector/tests/test_bug1046140.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=1046140
--->
-<head>
-  <meta charset="utf-8">
-  <title>Test for Bug 1046140</title>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript">
-
-  var utils = SpecialPowers.Cc["@mozilla.org/inspector/dom-utils;1"]
-    .getService(SpecialPowers.Ci.inIDOMUtils);
-
-  try {
-    utils.getSubpropertiesForCSSProperty("--foo");
-    ok(false, "expected an exception");
-  } catch(e) {
-    ok(true, "getSubpropertiesForCSSProperty throws when passed a CSS variable");
-  }
-
-  </script>
-</head>
-<body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1046140">Mozilla Bug 1046140</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-
-</div>
-<pre id="test">
-</pre>
-</body>
-</html>
--- a/mobile/android/base/home/HomeConfigPrefsBackend.java
+++ b/mobile/android/base/home/HomeConfigPrefsBackend.java
@@ -81,30 +81,20 @@ class HomeConfigPrefsBackend implements 
         // We disable Synced Tabs for guest mode profiles.
         final PanelConfig remoteTabsEntry;
         if (RestrictedProfiles.isAllowed(mContext, RestrictedProfiles.Restriction.DISALLOW_MODIFY_ACCOUNTS)) {
             remoteTabsEntry = createBuiltinPanelConfig(mContext, PanelType.REMOTE_TABS);
         } else {
             remoteTabsEntry = null;
         }
 
-        // On tablets, we go [...|History|Recent Tabs|Synced Tabs].
-        // On phones, we go [Synced Tabs|Recent Tabs|History|...].
-        if (HardwareUtils.isTablet()) {
-            panelConfigs.add(historyEntry);
-            panelConfigs.add(recentTabsEntry);
-            if (remoteTabsEntry != null) {
-                panelConfigs.add(remoteTabsEntry);
-            }
-        } else {
-            panelConfigs.add(0, historyEntry);
-            panelConfigs.add(0, recentTabsEntry);
-            if (remoteTabsEntry != null) {
-                panelConfigs.add(0, remoteTabsEntry);
-            }
+        panelConfigs.add(historyEntry);
+        panelConfigs.add(recentTabsEntry);
+        if (remoteTabsEntry != null) {
+            panelConfigs.add(remoteTabsEntry);
         }
 
         return new State(panelConfigs, true);
     }
 
     /**
      * Iterate through the panels to check if they are all disabled.
      */
--- a/mobile/android/base/home/HomePager.java
+++ b/mobile/android/base/home/HomePager.java
@@ -186,18 +186,16 @@ public class HomePager extends ViewPager
             mTabStrip = child;
 
             mDecor.setOnTitleClickListener(new OnTitleClickListener() {
                 @Override
                 public void onTitleClicked(int index) {
                     setCurrentItem(index, true);
                 }
             });
-        } else if (child instanceof HomePagerTabStrip) {
-            mTabStrip = child;
         }
 
         super.addView(child, index, params);
     }
 
     /**
      * Loads and initializes the pager.
      *
--- a/mobile/android/base/home/SearchEngineBar.java
+++ b/mobile/android/base/home/SearchEngineBar.java
@@ -97,25 +97,34 @@ public class SearchEngineBar extends Two
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 
         final int searchEngineCount = adapter.getCount() - 1;
 
         if (searchEngineCount > 0) {
-            final float availableWidthPerContainer = (getMeasuredWidth() - labelContainerWidth) / searchEngineCount;
+            final int availableWidth = getMeasuredWidth() - labelContainerWidth;
+            final double searchEnginesToDisplay;
 
-            final int desiredIconContainerSize = (int) Math.max(
-                    availableWidthPerContainer,
-                    minIconContainerWidth
-            );
+            if (searchEngineCount * minIconContainerWidth <= availableWidth) {
+                // All search engines fit int: So let's just display all.
+                searchEnginesToDisplay = searchEngineCount;
+            } else {
+                // If only (n) search engines fit into the available space then display (n - 0.5): The last search
+                // engine will be cut-off to show ability to scroll this view
 
-            if (desiredIconContainerSize != iconContainerWidth) {
-                iconContainerWidth = desiredIconContainerSize;
+                searchEnginesToDisplay = Math.floor(availableWidth / minIconContainerWidth) - 0.5;
+            }
+
+            // Use all available width and spread search engine icons
+            final int availableWidthPerContainer = (int) (availableWidth / searchEnginesToDisplay);
+
+            if (availableWidthPerContainer != iconContainerWidth) {
+                iconContainerWidth = availableWidthPerContainer;
                 adapter.notifyDataSetChanged();
             }
         }
     }
 
     @Override
     protected void onDraw(Canvas canvas) {
         super.onDraw(canvas);
--- a/mobile/android/base/home/TabMenuStripLayout.java
+++ b/mobile/android/base/home/TabMenuStripLayout.java
@@ -23,62 +23,79 @@ import android.widget.TextView;
  * {@code TabMenuStripLayout} is the view that draws the {@code HomePager}
  * tabs that are displayed in {@code TabMenuStrip}.
  */
 class TabMenuStripLayout extends LinearLayout
                          implements View.OnFocusChangeListener {
 
     private HomePager.OnTitleClickListener onTitleClickListener;
     private Drawable strip;
-    private View selectedView;
+    private TextView selectedView;
 
     // Data associated with the scrolling of the strip drawable.
     private View toTab;
     private View fromTab;
+    private int fromPosition;
+    private int toPosition;
     private float progress;
 
     // This variable is used to predict the direction of scroll.
     private float prevProgress;
+    private int tabContentStart;
 
     TabMenuStripLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
 
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabMenuStrip);
         final int stripResId = a.getResourceId(R.styleable.TabMenuStrip_strip, -1);
+        tabContentStart = a.getDimensionPixelSize(R.styleable.TabMenuStrip_tabContentStart, 0);
         a.recycle();
 
         if (stripResId != -1) {
             strip = getResources().getDrawable(stripResId);
         }
 
         setWillNotDraw(false);
     }
 
     void onAddPagerView(String title) {
         final TextView button = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.tab_menu_strip, this, false);
         button.setText(title.toUpperCase());
+        button.setTextColor(getResources().getColorStateList(R.color.tab_text_color));
+
+        if (getChildCount() == 0) {
+            button.setPadding(button.getPaddingLeft() + tabContentStart,
+                              button.getPaddingTop(),
+                              button.getPaddingRight(),
+                              button.getPaddingBottom());
+        }
 
         addView(button);
         button.setOnClickListener(new ViewClickListener(getChildCount() - 1));
         button.setOnFocusChangeListener(this);
     }
 
     void onPageSelected(final int position) {
-        selectedView = getChildAt(position);
+        if (selectedView != null) {
+            selectedView.setTextColor(getResources().getColorStateList(R.color.tab_text_color));
+        }
+
+        selectedView = (TextView) getChildAt(position);
+        selectedView.setTextColor(getResources().getColor(R.color.placeholder_grey));
 
         // Callback to measure and draw the strip after the view is visible.
         ViewTreeObserver vto = selectedView.getViewTreeObserver();
         if (vto.isAlive()) {
             vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                 @Override
                 public void onGlobalLayout() {
                     selectedView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
 
                     if (strip != null) {
-                        strip.setBounds(selectedView.getLeft(),
+                        strip.setBounds(selectedView.getLeft() + (position == 0 ? tabContentStart : 0),
                                         selectedView.getTop(),
                                         selectedView.getRight(),
                                         selectedView.getBottom());
                     }
 
                     prevProgress = position;
                 }
             });
@@ -98,45 +115,63 @@ class TabMenuStripLayout extends LinearL
         }
 
         final int fromTabLeft =  fromTab.getLeft();
         final int fromTabRight = fromTab.getRight();
 
         final int toTabLeft =  toTab.getLeft();
         final int toTabRight = toTab.getRight();
 
-        strip.setBounds((int) (fromTabLeft + ((toTabLeft - fromTabLeft) * progress)),
-                         0,
-                         (int) (fromTabRight + ((toTabRight - fromTabRight) * progress)),
-                         getHeight());
+        // The first tab has a padding applied (tabContentStart). We don't want the 'strip' to jump around so we remove
+        // this padding slowly (modifier) when scrolling to or from the first tab.
+        final int modifier;
+
+        if (fromPosition == 0 && toPosition == 1) {
+            // Slowly remove extra padding (tabContentStart) based on scroll progress
+            modifier = (int) (tabContentStart * (1 - progress));
+        } else if (fromPosition == 1 && toPosition == 0) {
+            // Slowly add extra padding (tabContentStart) based on scroll progress
+            modifier = (int) (tabContentStart * progress);
+        } else {
+            // We are not scrolling tab 0 in any way, no modifier needed
+            modifier = 0;
+        }
+
+        strip.setBounds((int) (fromTabLeft + ((toTabLeft - fromTabLeft) * progress)) + modifier,
+                0,
+                (int) (fromTabRight + ((toTabRight - fromTabRight) * progress)),
+                getHeight());
         invalidate();
     }
 
     /*
      * position + positionOffset goes from 0 to 2 as we scroll from page 1 to 3.
      * Normalized progress is relative to the the direction the page is being scrolled towards.
      * For this, we maintain direction of scroll with a state, and the child view we are moving towards and away from.
      */
     void setScrollingData(int position, float positionOffset) {
         if (position >= getChildCount() - 1) {
             return;
         }
 
         final float currProgress = position + positionOffset;
 
         if (prevProgress > currProgress) {
-            toTab = getChildAt(position);
-            fromTab = getChildAt(position + 1);
+            toPosition = position;
+            fromPosition = position + 1;
             progress = 1 - positionOffset;
         } else {
-            toTab = getChildAt(position + 1);
-            fromTab = getChildAt(position);
+            toPosition = position + 1;
+            fromPosition = position;
             progress = positionOffset;
         }
 
+        toTab = getChildAt(toPosition);
+        fromTab = getChildAt(fromPosition);
+
         prevProgress = currProgress;
     }
 
     @Override
     public void onDraw(Canvas canvas) {
         super.onDraw(canvas);
 
         if (strip != null) {
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/color/tab_text_color.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true"
+          android:color="@color/placeholder_grey" />
+
+    <item android:color="@color/panel_tab_text_normal"/>
+</selector>
\ No newline at end of file
deleted file mode 100644
--- a/mobile/android/base/resources/layout-large-v11/home_pager.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<!-- This file is used to include the home pager in gecko app
-     layout based on screen size -->
-
-<org.mozilla.gecko.home.HomePager xmlns:android="http://schemas.android.com/apk/res/android"
-                                  xmlns:gecko="http://schemas.android.com/apk/res-auto"
-                                  android:id="@+id/home_pager"
-                                  android:layout_width="match_parent"
-                                  android:layout_height="match_parent"
-                                  android:background="@android:color/white">
-
-    <org.mozilla.gecko.home.TabMenuStrip android:layout_width="match_parent"
-                                         android:layout_height="@dimen/tabs_strip_height"
-                                         android:background="@color/about_page_header_grey"
-                                         android:layout_gravity="top"
-                                         gecko:strip="@drawable/home_tab_menu_strip"/>
-
-</org.mozilla.gecko.home.HomePager>
--- a/mobile/android/base/resources/layout/home_pager.xml
+++ b/mobile/android/base/resources/layout/home_pager.xml
@@ -8,17 +8,16 @@
 
 <org.mozilla.gecko.home.HomePager xmlns:android="http://schemas.android.com/apk/res/android"
                                   xmlns:gecko="http://schemas.android.com/apk/res-auto"
                                   android:id="@+id/home_pager"
                                   android:layout_width="match_parent"
                                   android:layout_height="match_parent"
                                   android:background="@android:color/white">
 
-    <org.mozilla.gecko.home.HomePagerTabStrip android:layout_width="match_parent"
-                                              android:layout_height="@dimen/tabs_strip_height"
-                                              android:layout_gravity="top"
-                                              android:gravity="center_vertical"
-                                              android:background="@color/about_page_header_grey"
-                                              gecko:tabIndicatorColor="@color/fennec_ui_orange"
-                                              android:textAppearance="@style/TextAppearance.Widget.HomePagerTabStrip"/>
+    <org.mozilla.gecko.home.TabMenuStrip android:layout_width="match_parent"
+                                         android:layout_height="@dimen/tabs_strip_height"
+                                         android:background="@color/about_page_header_grey"
+                                         android:layout_gravity="top"
+                                         gecko:strip="@drawable/home_tab_menu_strip"
+                                         gecko:tabContentStart="72dp" />
 
 </org.mozilla.gecko.home.HomePager>
--- a/mobile/android/base/resources/values/attrs.xml
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -164,16 +164,17 @@
 
     <declare-styleable name="TopSitesGridView">
         <attr name="android:horizontalSpacing"/>
         <attr name="android:verticalSpacing"/>
     </declare-styleable>
 
     <declare-styleable name="TabMenuStrip">
         <attr name="strip" format="reference"/>
+        <attr name="tabContentStart" format="dimension" />
     </declare-styleable>
 
     <declare-styleable name="TabPanelBackButton">
         <attr name="rightDivider" format="reference"/>
         <attr name="dividerVerticalPadding" format="dimension"/>
     </declare-styleable>
 
     <declare-styleable name="EllipsisTextView">
--- a/mobile/android/base/resources/values/colors.xml
+++ b/mobile/android/base/resources/values/colors.xml
@@ -111,16 +111,17 @@
   <color name="url_bar_domaintext_private">#FFF</color>
   <color name="url_bar_blockedtext">#b14646</color>
   <color name="url_bar_shadow">#12000000</color>
 
   <color name="home_button_bar_bg">#FFF5F7F9</color>
 
   <color name="panel_image_item_background">#D1D9E1</color>
   <color name="panel_icon_item_title_background">#32000000</color>
+  <color name="panel_tab_text_normal">#FFBFBFBF</color>
 
   <!-- Swipe to refresh colors for dynamic panel -->
   <color name="swipe_refresh_orange">#FFFFC26C</color>
   <color name="swipe_refresh_white">#FFFFFFFF</color>
 
   <!-- Remote tabs setup -->
   <color name="remote_tabs_setup_button_background_hit">#D95300</color>
 
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -124,17 +124,17 @@
     <dimen name="prompt_service_icon_text_padding">10dp</dimen>
     <dimen name="prompt_service_inputs_padding">16dp</dimen>
     <dimen name="prompt_service_left_right_text_with_icon_padding">10dp</dimen>
     <dimen name="prompt_service_top_bottom_text_with_icon_padding">8dp</dimen>
     <dimen name="tab_thumbnail_height">90dp</dimen>
     <dimen name="tab_thumbnail_width">160dp</dimen>
     <dimen name="tabs_panel_indicator_width">60dp</dimen>
     <dimen name="tabs_panel_button_width">48dp</dimen>
-    <dimen name="tabs_strip_height">40dp</dimen>
+    <dimen name="tabs_strip_height">48dp</dimen>
     <dimen name="tabs_strip_button_width">100dp</dimen>
     <dimen name="tabs_strip_button_padding">18dp</dimen>
     <dimen name="tabs_strip_shadow_size">1dp</dimen>
     <dimen name="tabs_layout_horizontal_height">156dp</dimen>
     <dimen name="text_selection_handle_width">47dp</dimen>
     <dimen name="text_selection_handle_height">58dp</dimen>
     <dimen name="text_selection_handle_shadow">11dp</dimen>
     <dimen name="validation_message_height">50dp</dimen>
--- a/mobile/android/tests/browser/robocop/components/AboutHomeComponent.java
+++ b/mobile/android/tests/browser/robocop/components/AboutHomeComponent.java
@@ -3,59 +3,47 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tests.components;
 
 import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
 import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
 
 import java.util.Arrays;
+import java.util.List;
 
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.home.HomeConfig.PanelType;
 import org.mozilla.gecko.tests.UITestContext;
 import org.mozilla.gecko.tests.helpers.WaitHelper;
-import org.mozilla.gecko.util.HardwareUtils;
 
 import android.os.Build;
 import android.support.v4.view.ViewPager;
 import android.view.View;
 import android.widget.TextView;
 
 import com.jayway.android.robotium.solo.Condition;
 import com.jayway.android.robotium.solo.Solo;
 
 /**
  * A class representing any interactions that take place on the Awesomescreen.
  */
 public class AboutHomeComponent extends BaseComponent {
     private static final String LOGTAG = AboutHomeComponent.class.getSimpleName();
 
-    // TODO: Having a specific ordering of panels is prone to fail and thus temporary.
-    // Hopefully the work in bug 940565 will alleviate the need for these enums.
-    // Explicit ordering of HomePager panels on a phone.
-    private static final PanelType[] PANEL_ORDERING_PHONE = {
-            PanelType.REMOTE_TABS,
-            PanelType.RECENT_TABS,
-            PanelType.HISTORY,
-            PanelType.TOP_SITES,
-            PanelType.BOOKMARKS,
-            PanelType.READING_LIST,
-    };
-
-    private static final PanelType[] PANEL_ORDERING_TABLET = {
+    private static final List<PanelType> PANEL_ORDERING = Arrays.asList(
             PanelType.TOP_SITES,
             PanelType.BOOKMARKS,
             PanelType.READING_LIST,
             PanelType.HISTORY,
             PanelType.RECENT_TABS,
-            PanelType.REMOTE_TABS,
-    };
+            PanelType.REMOTE_TABS
+    );
 
     // The percentage of the panel to swipe between 0 and 1. This value was set through
     // testing: 0.55f was tested on try and fails on armv6 devices.
     private static final float SWIPE_PERCENTAGE = 0.70f;
 
     public AboutHomeComponent(final UITestContext testContext) {
         super(testContext);
     }
@@ -73,17 +61,17 @@ public class AboutHomeComponent extends 
             return mSolo.getView(R.id.home_banner);
         }
         return null;
     }
 
     public AboutHomeComponent assertCurrentPanel(final PanelType expectedPanel) {
         assertVisible();
 
-        final int expectedPanelIndex = getPanelIndexForDevice(expectedPanel);
+        final int expectedPanelIndex = PANEL_ORDERING.indexOf(expectedPanel);
         fAssertEquals("The current HomePager panel is " + expectedPanel,
                      expectedPanelIndex, getHomePagerView().getCurrentItem());
         return this;
     }
 
     public AboutHomeComponent assertNotVisible() {
         fAssertTrue("The HomePager is not visible",
                     getHomePagerContainer().getVisibility() != View.VISIBLE ||
@@ -167,60 +155,42 @@ public class AboutHomeComponent extends 
         assertVisible();
 
         final int panelIndex = getHomePagerView().getCurrentItem();
 
         mSolo.scrollViewToSide(getHomePagerView(), panelDirection, SWIPE_PERCENTAGE);
 
         // The panel on the left is a lower index and vice versa.
         final int unboundedPanelIndex = panelIndex + (panelDirection == Solo.LEFT ? -1 : 1);
-        final int panelCount = getPanelOrderingForDevice().length;
-        final int maxPanelIndex = panelCount - 1;
+        final int maxPanelIndex = PANEL_ORDERING.size() - 1;
         final int expectedPanelIndex = Math.min(Math.max(0, unboundedPanelIndex), maxPanelIndex);
 
         waitForPanelIndex(expectedPanelIndex);
     }
 
     private void waitForPanelIndex(final int expectedIndex) {
-        final String panelName = getPanelOrderingForDevice()[expectedIndex].name();
+        final String panelName = PANEL_ORDERING.get(expectedIndex).name();
 
         WaitHelper.waitFor("HomePager " + panelName + " panel", new Condition() {
             @Override
             public boolean isSatisfied() {
                 return (getHomePagerView().getCurrentItem() == expectedIndex);
             }
         });
     }
 
     /**
-     * Get the expected panel index for the given PanelType on this device. Different panel
-     * orderings are expected on tables vs. phones.
-     */
-    private int getPanelIndexForDevice(final PanelType panelType) {
-        PanelType[] panelOrdering = getPanelOrderingForDevice();
-
-        return Arrays.asList(panelOrdering).indexOf(panelType);
-    }
-
-    /**
-     * Get an array of PanelType objects ordered as we want the panels to be ordered on this device.
-     */
-    public static PanelType[] getPanelOrderingForDevice() {
-        return HardwareUtils.isTablet() ? PANEL_ORDERING_TABLET : PANEL_ORDERING_PHONE;
-    }
-
-    /**
      * Navigate directly to a built-in panel by its panel type.
      * <p>
      * If the panel type is not part of the active Home Panel configuration, the
      * default about:home panel is displayed. If the panel type is not a
      * built-in panel, an IllegalArgumentException is thrown.
      *
      * @param panelType to navigate to.
      * @return self, for chaining.
      */
     public AboutHomeComponent navigateToBuiltinPanelType(PanelType panelType) throws IllegalArgumentException {
         Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(panelType));
-        final int expectedPanelIndex = getPanelIndexForDevice(panelType);
+        final int expectedPanelIndex = PANEL_ORDERING.indexOf(panelType);
         waitForPanelIndex(expectedPanelIndex);
         return this;
     }
 }
--- a/mobile/android/tests/browser/robocop/robocop.ini
+++ b/mobile/android/tests/browser/robocop/robocop.ini
@@ -148,16 +148,17 @@ skip-if = android_version == "18"
 [testVideoControls.java]
 # disabled on Android 2.3 due to video playback issues, bug 1088038; on 4.3, bug 1098532
 skip-if = android_version == "10" || android_version == "18"
 [testVideoDiscovery.java]
 [testWebChannel.java]
 
 # Used for Talos, please don't use in mochitest
 #[testCheck2.java]
+#[testCheck3.java] # and the autophone version
 
 # Using UITest
 #[testAboutHomePageNavigation.java] # see bug 947550, bug 979038 and bug 977952
 [testAboutHomeVisibility.java]
 # disabled on Android 2.3; bug 946656
 skip-if = android_version == "10"
 [testAppMenuPathways.java]
 # disabled on 4.3, bug 1158005
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testCheck3.java
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import org.json.JSONObject;
+
+public class testCheck3 extends PixelTest {
+    @Override
+    protected Type getTestType() {
+        return Type.TALOS;
+    }
+
+    public void testCheck3() {
+        String url = getAbsoluteUrl("/facebook.com/www.facebook.com/barackobama.html");
+
+        // Enable double-tap zooming
+        JSONObject jsonPref = new JSONObject();
+        try {
+            jsonPref.put("name", "browser.ui.zoom.force-user-scalable");
+            jsonPref.put("type", "bool");
+            jsonPref.put("value", true);
+            setPreferenceAndWaitForChange(jsonPref);
+        } catch (Exception ex) {
+            mAsserter.ok(false, "exception in testCheck3", ex.toString());
+        }
+
+        blockForGeckoReady();
+        loadAndPaint(url);
+
+        mDriver.setupScrollHandling();
+
+        /*
+         * for this test, we load the timecube page, and replay a recorded sequence of events
+         * that is a user panning/zooming around the page. specific things in the sequence
+         * include:
+         * - scroll on one axis followed by scroll on another axis
+         * - pinch zoom (in and out)
+         * - double-tap zoom (in and out)
+         * - multi-fling panning with different velocities on each fling
+         *
+         * this checkerboarding metric is going to be more of a "functional" style test than
+         * a "unit" style test; i.e. it covers a little bit of a lot of things to measure
+         * overall performance, but doesn't really allow identifying which part is slow.
+         */
+
+        MotionEventReplayer mer = new MotionEventReplayer(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop(),
+                mDriver.getGeckoWidth(), mDriver.getGeckoHeight());
+
+        float completeness = 0.0f;
+        mDriver.startCheckerboardRecording();
+        // replay the events
+        try {
+            mer.replayEvents(getAsset("testcheck2-motionevents"));
+            // give it some time to draw any final frames
+            Thread.sleep(1000);
+            completeness = mDriver.stopCheckerboardRecording();
+        } catch (Exception e) {
+            e.printStackTrace();
+            mAsserter.ok(false, "Exception while replaying events", e.toString());
+        }
+
+        mAsserter.dumpLog("__start_report" + completeness + "__end_report");
+        System.out.println("Completeness score: " + completeness);
+        long msecs = System.currentTimeMillis();
+        mAsserter.dumpLog("__startTimestamp" + msecs + "__endTimestamp");
+    }
+}
--- a/toolkit/components/perfmonitoring/PerformanceStats.jsm
+++ b/toolkit/components/perfmonitoring/PerformanceStats.jsm
@@ -678,43 +678,34 @@ function Snapshot({xpcom, childProcesses
 
   this.processData = new PerformanceData({xpcom: xpcom.getProcessData(), probes});
 }
 
 /**
  * Communication with other processes
  */
 let Process = {
-  // `true` once communications have been initialized
-  _initialized: false,
-
-  // the message manager
-  _loader: null,
-
   // a counter used to match responses to requests
   _idcounter: 0,
-
+  _loader: null,
   /**
    * If we are in a child process, return `null`.
    * Otherwise, return the global parent process message manager
    * and load the script to connect to children processes.
    */
   get loader() {
-    if (this._initialized) {
+    if (isContent) {
+      return null;
+    }
+    if (this._loader) {
       return this._loader;
     }
-    this._initialized = true;
-    this._loader = Services.ppmm;
-    if (!this._loader) {
-      // We are in a child process.
-      return null;
-    }
-    this._loader.loadProcessScript("resource://gre/modules/PerformanceStats-content.js",
+    Services.ppmm.loadProcessScript("resource://gre/modules/PerformanceStats-content.js",
       true/*including future processes*/);
-    return this._loader;
+    return this._loader = Services.ppmm;
   },
 
   /**
    * Broadcast a message to all children processes.
    *
    * NOOP if we are in a child process.
    */
   broadcast: function(topic, payload) {
@@ -746,31 +737,22 @@ let Process = {
     // The number of responses we are expecting. Note that we may
     // not receive all responses if a process is too long to respond.
     let expecting = this.loader.childCount;
 
     // The responses we have collected, in arbitrary order.
     let collected = [];
     let deferred = PromiseUtils.defer();
 
-    // The content script may be loaded more than once (bug 1184115).
-    // To avoid double-responses, we keep track of who has already responded.
-    // Note that we could it on the other end, at the expense of implementing
-    // an additional .jsm just for that purpose.
-    let responders = new Set();
     let observer = function({data, target}) {
       if (data.id != id) {
         // Collision between two collections,
         // ignore the other one.
         return;
       }
-      if (responders.has(target)) {
-        return;
-      }
-      responders.add(target);
       if (data.data) {
         collected.push(data.data)
       }
       if (--expecting > 0) {
         // We are still waiting for at least one response.
         return;
       }
       deferred.resolve();
--- a/toolkit/devtools/gcli/commands/restart.js
+++ b/toolkit/devtools/gcli/commands/restart.js
@@ -26,37 +26,52 @@ const BRAND_SHORT_NAME = Cc["@mozilla.or
  * - restarts immediately and starts Firefox without using cache
  */
 exports.items = [
   {
     item: "command",
     runAt: "client",
     name: "restart",
     description: l10n.lookupFormat("restartBrowserDesc", [ BRAND_SHORT_NAME ]),
-    params: [
-      {
-        name: "nocache",
-        type: "boolean",
-        description: l10n.lookup("restartBrowserNocacheDesc")
-      }
-    ],
+    params: [{
+      group: l10n.lookup("restartBrowserGroupOptions"),
+      params: [
+        {
+          name: "nocache",
+          type: "boolean",
+          description: l10n.lookup("restartBrowserNocacheDesc")
+        },
+        {
+          name: "safemode",
+          type: "boolean",
+          description: l10n.lookup("restartBrowserSafemodeDesc")
+        }
+      ]
+    }],
     returnType: "string",
     exec: function Restart(args, context) {
       let canceled = Cc["@mozilla.org/supports-PRBool;1"]
                       .createInstance(Ci.nsISupportsPRBool);
       Services.obs.notifyObservers(canceled, "quit-application-requested", "restart");
       if (canceled.data) {
         return l10n.lookup("restartBrowserRequestCancelled");
       }
 
       // disable loading content from cache.
       if (args.nocache) {
         Services.appinfo.invalidateCachesOnRestart();
       }
 
-      // restart
-      Cc["@mozilla.org/toolkit/app-startup;1"]
-          .getService(Ci.nsIAppStartup)
-          .quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
+      const appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]
+                           .getService(Ci.nsIAppStartup);
+
+      if (args.safemode) {
+        // restart in safemode
+        appStartup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit);
+      } else {
+        // restart normally
+        appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
+      }
+
       return l10n.lookupFormat("restartBrowserRestarting", [ BRAND_SHORT_NAME ]);
     }
   }
 ];
--- a/toolkit/locales/en-US/chrome/global/devtools/gclicommands.properties
+++ b/toolkit/locales/en-US/chrome/global/devtools/gclicommands.properties
@@ -277,16 +277,25 @@ restartBrowserNocacheDesc=Disables loadi
 # user when a scheduled restart has been aborted by the user.
 restartBrowserRequestCancelled=Restart request cancelled by user.
 
 # LOCALIZATION NOTE (restartBrowserRestarting) A string displayed to the
 # user when a restart has been initiated without a delay.
 # The argument (%1$S) is the browser name.
 restartBrowserRestarting=Restarting %1$S…
 
+# LOCALIZATION NOTE (restartBrowserGroupOptions) A label for the optional options of
+# the restart command.
+restartBrowserGroupOptions=Options
+
+# LOCALIZATION NOTE (restartBrowserSafemodeDesc) A very short string to
+# describe the 'safemode' parameter to the 'restart' command, which is
+# displayed in a dialog when the user is using this command.
+restartBrowserSafemodeDesc=Enables Safe Mode upon restart
+
 # LOCALIZATION NOTE (inspectDesc) A very short description of the 'inspect'
 # command. See inspectManual for a fuller description of what it does. This
 # string is designed to be shown in a menu alongside the command name, which
 # is why it should be as short as possible.
 inspectDesc=Inspect a node
 
 # LOCALIZATION NOTE (inspectManual) A fuller description of the 'inspect'
 # command, displayed when the user asks for help on what it does.
--- a/tools/mercurial/hgsetup/config.py
+++ b/tools/mercurial/hgsetup/config.py
@@ -181,16 +181,21 @@ class MercurialConfig(object):
         else:
             d['qnew'] = '-U ' + d['qnew']
 
     def get_bugzilla_credentials(self):
         if 'bugzilla' not in self._c:
             return None, None
 
         b = self._c['bugzilla']
-        return b.get('username', None), b.get('password', None)
+        return (
+            b.get('username', None),
+            b.get('password', None),
+            b.get('userid', None),
+            b.get('cookie', None),
+        )
 
     def set_bugzilla_credentials(self, username, password):
         b = self._c.setdefault('bugzilla', {})
         if username:
             b['username'] = username
         if password:
             b['password'] = password
--- a/tools/mercurial/hgsetup/wizard.py
+++ b/tools/mercurial/hgsetup/wizard.py
@@ -3,16 +3,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import unicode_literals
 
 import difflib
 import errno
 import os
 import shutil
+import stat
 import sys
 import which
 import subprocess
 
 from distutils.version import LooseVersion
 
 from configobj import ConfigObjError
 from StringIO import StringIO
@@ -48,56 +49,68 @@ You are running an out of date Mercurial
 For a faster and better Mercurial experience, we HIGHLY recommend you
 upgrade.
 '''.strip()
 
 MISSING_USERNAME = '''
 You don't have a username defined in your Mercurial config file. In order to
 send patches to Mozilla, you'll need to attach a name and email address. If you
 aren't comfortable giving us your full name, pseudonames are acceptable.
+
+(Relevant config option: ui.username)
 '''.strip()
 
 BAD_DIFF_SETTINGS = '''
 Mozilla developers produce patches in a standard format, but your Mercurial is
 not configured to produce patches in that format.
+
+(Relevant config options: diff.git, diff.showfunc, diff.unified)
 '''.strip()
 
 MQ_INFO = '''
 The mq extension manages patches as separate files. It provides an
 alternative to the recommended bookmark-based development workflow.
 
 If you are a newcomer to Mercurial or are coming from Git, it is
 recommended to avoid mq.
 
+(Relevant config option: extensions.mq)
+
 Would you like to activate the mq extension
 '''.strip()
 
 BZEXPORT_INFO = '''
 If you plan on uploading patches to Mozilla, there is an extension called
 bzexport that makes it easy to upload patches from the command line via the
 |hg bzexport| command. More info is available at
 https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/bzexport/README
 
+(Relevant config option: extensions.bzexport)
+
 Would you like to activate bzexport
 '''.strip()
 
 MQEXT_INFO = '''
 The mqext extension adds a number of features, including automatically committing
 changes to your mq patch queue. More info is available at
 https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/mqext/README.txt
 
+(Relevant config option: extensions.mqext)
+
 Would you like to activate mqext
 '''.strip()
 
 QIMPORTBZ_INFO = '''
 The qimportbz extension
 (https://hg.mozilla.org/hgcustom/version-control-tools/file/default/hgext/qimportbz/README) makes it possible to
 import patches from Bugzilla using a friendly bz:// URL handler. e.g.
 |hg qimport bz://123456|.
 
+(Relevant config option: extensions.qimportbz)
+
 Would you like to activate qimportbz
 '''.strip()
 
 QNEWCURRENTUSER_INFO = '''
 The mercurial queues command |hg qnew|, which creates new patches in your patch
 queue does not set patch author information by default. Author information
 should be included when uploading for review.
 '''.strip()
@@ -118,26 +131,32 @@ Please upgrade to Mercurial %s or newer 
 
 MISSING_BUGZILLA_CREDENTIALS = '''
 You do not have your Bugzilla credentials defined in your Mercurial config.
 
 Various extensions make use of your Bugzilla credentials to interface with
 Bugzilla to enrich your development experience.
 
 Bugzilla credentials are optional. If you do not provide them, associated
-functionality will not be enabled or you will be prompted for your
-Bugzilla credentials when they are needed.
+functionality will not be enabled, we will attempt to find a Bugzilla cookie
+from a Firefox profile, or you will be prompted for your Bugzilla credentials
+when they are needed.
+
+Your Bugzilla credentials will be stored in *PLAIN TEXT* in your hgrc config
+file. If this is not wanted, do not enter your credentials.
 '''.lstrip()
 
 BZPOST_MINIMUM_VERSION = LooseVersion('3.1')
 
 BZPOST_INFO = '''
 The bzpost extension automatically records the URLs of pushed commits to
 referenced Bugzilla bugs after push.
 
+(Relevant config option: extensions.bzpost)
+
 Would you like to activate bzpost
 '''.strip()
 
 FIREFOXTREE_MINIMUM_VERSION = LooseVersion('3.1')
 
 FIREFOXTREE_INFO = '''
 The firefoxtree extension makes interacting with the multiple Firefox
 repositories easier:
@@ -152,40 +171,52 @@ repositories easier:
 * A pre-push hook will prevent you from pushing multiple heads to known
   Firefox repos. This acts quicker than a server-side hook.
 
 The firefoxtree extension is *strongly* recommended if you:
 
 a) aggregate multiple Firefox repositories into a single local repo
 b) perform head/bookmark-based development (as opposed to mq)
 
+(Relevant config option: extensions.firefoxtree)
+
 Would you like to activate firefoxtree
 '''.strip()
 
 PUSHTOTRY_MINIMUM_VERSION = LooseVersion('3.3')
 
 PUSHTOTRY_INFO = '''
 The push-to-try extension generates a temporary commit with a given
 try syntax and pushes it to the try server. The extension is intended
 to be used in concert with other tools generating try syntax so that
 they can push to try without depending on mq or other workarounds.
 
+(Relevant config option: extensions.push-to-try)
+
 Would you like to activate push-to-try
 '''.strip()
 
 BUNDLECLONE_MINIMUM_VERSION = LooseVersion('3.1')
 
 BUNDLECLONE_INFO = '''
 The bundleclone extension makes cloning faster and saves server resources.
 
 We highly recommend you activate this extension.
 
+(Relevant config option: extensions.bundleclone)
+
 Would you like to activate bundleclone
 '''.strip()
 
+FILE_PERMISSIONS_WARNING = '''
+Your hgrc file is currently readable by others.
+
+Sensitive information such as your Bugzilla credentials could be
+stolen if others have access to this file/machine.
+'''.strip()
 
 class MercurialSetupWizard(object):
     """Command-line wizard to help users configure Mercurial."""
 
     def __init__(self, state_dir):
         # We use normpath since Mercurial expects the hgrc to use native path
         # separators, but state_dir uses unix style paths even on Windows.
         self.state_dir = os.path.normpath(state_dir)
@@ -267,47 +298,49 @@ class MercurialSetupWizard(object):
         if not c.have_recommended_diff_settings():
             print(BAD_DIFF_SETTINGS)
             print('')
             if self._prompt_yn('Would you like me to fix this for you'):
                 c.ensure_recommended_diff_settings()
                 print('Fixed patch settings.')
                 print('')
 
-        self.prompt_native_extension(c, 'progress',
-            'Would you like to see progress bars during Mercurial operations')
+        # Progress is built into core and enabled by default in Mercurial 3.5.
+        if hg_version < LooseVersion('3.5'):
+            self.prompt_native_extension(c, 'progress',
+                'Would you like to see progress bars during Mercurial operations')
 
         self.prompt_native_extension(c, 'color',
             'Would you like Mercurial to colorize output to your terminal')
 
         self.prompt_native_extension(c, 'rebase',
             'Would you like to enable the rebase extension to allow you to move'
             ' changesets around (which can help maintain a linear history)')
 
         self.prompt_native_extension(c, 'histedit',
             'Would you like to enable the histedit extension to allow history '
             'rewriting via the "histedit" command (similar to '
             '`git rebase -i`)')
 
         self.prompt_native_extension(c, 'mq', MQ_INFO)
 
-        self.prompt_external_extension(c, 'bzexport', BZEXPORT_INFO)
-
         if 'reviewboard' not in c.extensions:
             if hg_version < REVIEWBOARD_MINIMUM_VERSION:
                 print(REVIEWBOARD_INCOMPATIBLE % REVIEWBOARD_MINIMUM_VERSION)
             else:
                 p = os.path.join(self.vcs_tools_dir, 'hgext', 'reviewboard',
                     'client.py')
                 self.prompt_external_extension(c, 'reviewboard',
                     'Would you like to enable the reviewboard extension so '
                     'you can easily initiate code reviews against Mozilla '
                     'projects',
                     path=p)
 
+        self.prompt_external_extension(c, 'bzexport', BZEXPORT_INFO)
+
         if hg_version >= BZPOST_MINIMUM_VERSION:
             self.prompt_external_extension(c, 'bzpost', BZPOST_INFO)
 
         if hg_version >= FIREFOXTREE_MINIMUM_VERSION:
             self.prompt_external_extension(c, 'firefoxtree', FIREFOXTREE_INFO)
 
         if hg_version >= BUNDLECLONE_MINIMUM_VERSION:
             self.prompt_external_extension(c, 'bundleclone', BUNDLECLONE_INFO)
@@ -330,27 +363,28 @@ class MercurialSetupWizard(object):
                 print(QNEWCURRENTUSER_INFO)
                 if self._prompt_yn('Would you like qnew to set patch author by '
                                    'default'):
                     c.ensure_qnew_currentuser_default()
                     print('Configured qnew to set patch author by default.')
                     print('')
 
         if 'reviewboard' in c.extensions or 'bzpost' in c.extensions:
-            bzuser, bzpass = c.get_bugzilla_credentials()
+            bzuser, bzpass, bzuserid, bzcookie = c.get_bugzilla_credentials()
 
-            if not bzuser or not bzpass:
+            if (not bzuser or not bzpass) and (not bzuserid or not bzcookie):
                 print(MISSING_BUGZILLA_CREDENTIALS)
 
-            if not bzuser:
-                bzuser = self._prompt('What is your Bugzilla email address?',
+            # Don't prompt for username if cookie is set.
+            if not bzuser and not bzuserid:
+                bzuser = self._prompt('What is your Bugzilla email address? (optional)',
                     allow_empty=True)
 
             if bzuser and not bzpass:
-                bzpass = self._prompt('What is your Bugzilla password?',
+                bzpass = self._prompt('What is your Bugzilla password? (optional)',
                     allow_empty=True)
 
             if bzuser or bzpass:
                 c.set_bugzilla_credentials(bzuser, bzpass)
 
         if self.update_vcs_tools:
             self.updater.update_mercurial_repo(
                 hg,
@@ -397,16 +431,29 @@ class MercurialSetupWizard(object):
                     c.write(fh)
                 print('Wrote changes to %s.' % config_path)
             else:
                 print('hgrc changes not written to file. I would have '
                     'written the following:\n')
                 c.write(sys.stdout)
                 return 1
 
+        # Config file may contain sensitive content, such as passwords.
+        # Prompt to remove global permissions.
+        mode = os.stat(config_path).st_mode
+        if mode & (stat.S_IRWXG | stat.S_IRWXO):
+            print(FILE_PERMISSIONS_WARNING)
+            if self._prompt_yn('Remove permissions for others to read '
+                               'your hgrc file'):
+                # We don't care about sticky and set UID bits because this is
+                # a regular file.
+                mode = mode & stat.S_IRWXU
+                print('Changing permissions of %s' % config_path)
+                os.chmod(config_path, mode)
+
         print(FINISHED)
         return 0
 
     def prompt_native_extension(self, c, name, prompt_text):
         # Ask the user if the specified extension bundled with Mercurial should be enabled.
         if name in c.extensions:
             return
         if self._prompt_yn(prompt_text):
--- a/tools/mercurial/mach_commands.py
+++ b/tools/mercurial/mach_commands.py
@@ -62,9 +62,13 @@ class VersionControlCommands(object):
             from hgsetup.update import MercurialUpdater
             updater = MercurialUpdater(self._context.state_dir)
             result = updater.update_all()
         else:
             from hgsetup.wizard import MercurialSetupWizard
             wizard = MercurialSetupWizard(self._context.state_dir)
             result = wizard.run(map(os.path.expanduser, config_paths))
 
+        if result:
+            print('(despite the failure, mach will not nag you to run '
+                  '`mach mercurial-setup`)')
+
         return result
--- a/webapprt/test/chrome/browser_debugger.js
+++ b/webapprt/test/chrome/browser_debugger.js
@@ -13,17 +13,16 @@ function test() {
     client.connect(() => {
       client.listTabs((aResponse) => {
         is(aResponse.tabs[0].title, "Debugger Test Webapp", "Title correct");
         is(aResponse.tabs[0].url, "http://test/webapprtChrome/webapprt/test/chrome/debugger.html", "URL correct");
         ok(aResponse.tabs[0].consoleActor, "consoleActor set");
         ok(aResponse.tabs[0].gcliActor, "gcliActor set");
         ok(aResponse.tabs[0].styleEditorActor, "styleEditorActor set");
         ok(aResponse.tabs[0].inspectorActor, "inspectorActor set");
-        ok(aResponse.tabs[0].traceActor, "traceActor set");
         ok(aResponse.deviceActor, "deviceActor set");
 
         client.close(() => {
           finish();
         });
       });
     });
   });
--- a/widget/windows/nsWindow.cpp
+++ b/widget/windows/nsWindow.cpp
@@ -994,16 +994,22 @@ nsWindow::ReparentNativeWidget(nsIWidget
   return NS_OK;
 }
 
 nsIWidget* nsWindow::GetParent(void)
 {
   return GetParentWindow(false);
 }
 
+static int32_t RoundDown(double aDouble)
+{
+  return aDouble > 0 ? static_cast<int32_t>(floor(aDouble)) :
+                       static_cast<int32_t>(ceil(aDouble));
+}
+
 float nsWindow::GetDPI()
 {
   HDC dc = ::GetDC(mWnd);
   if (!dc)
     return 96.0f;
 
   double heightInches = ::GetDeviceCaps(dc, VERTSIZE)/MM_PER_INCH_FLOAT;
   int heightPx = ::GetDeviceCaps(dc, VERTRES);
@@ -3706,17 +3712,19 @@ nsWindow::UpdateThemeGeometries(const ns
   nsIntRegion clearRegion;
   if (!HasGlass() || !nsUXThemeData::CheckForCompositor()) {
     return;
   }
   // On Win10, force show the top border:
   if (IsWin10OrLater() && mCustomNonClient && mSizeMode == nsSizeMode_Normal) {
     RECT rect;
     ::GetWindowRect(mWnd, &rect);
-    clearRegion.Or(clearRegion, nsIntRect(0, 0, rect.right - rect.left, 1.0));
+    // We want 1 pixel of border for every whole 100% of scaling
+    double borderSize = RoundDown(GetDefaultScale().scale);
+    clearRegion.Or(clearRegion, nsIntRect(0, 0, rect.right - rect.left, borderSize));
   }
   if (!IsWin10OrLater()) {
     for (size_t i = 0; i < aThemeGeometries.Length(); i++) {
       if (aThemeGeometries[i].mType == nsNativeThemeWin::eThemeGeometryTypeWindowButtons)
       {
         nsIntRect bounds = aThemeGeometries[i].mRect;
         clearRegion.Or(clearRegion, nsIntRect(bounds.X(), bounds.Y(), bounds.Width(), bounds.Height() - 2.0));
         clearRegion.Or(clearRegion, nsIntRect(bounds.X() + 1.0, bounds.YMost() - 2.0, bounds.Width() - 1.0, 1.0));
@@ -6452,22 +6460,16 @@ bool nsWindow::OnTouch(WPARAM wParam, LP
     }
   }
 
   delete [] pInputs;
   mGesture.CloseTouchInputHandle((HTOUCHINPUT)lParam);
   return true;
 }
 
-static int32_t RoundDown(double aDouble)
-{
-  return aDouble > 0 ? static_cast<int32_t>(floor(aDouble)) :
-                       static_cast<int32_t>(ceil(aDouble));
-}
-
 // Gesture event processing. Handles WM_GESTURE events.
 bool nsWindow::OnGesture(WPARAM wParam, LPARAM lParam)
 {
   // Treatment for pan events which translate into scroll events:
   if (mGesture.IsPanEvent(lParam)) {
     if ( !mGesture.ProcessPanMessage(mWnd, wParam, lParam) )
       return false; // ignore