Merge m-c to b2ginbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Fri, 09 Oct 2015 16:46:09 -0700
changeset 300461 9f8c18106b1190489a5be68289569dfdba133e53
parent 300460 370e515aa882be1a70f9b16e71d2f662683eed2d (current diff)
parent 300450 d1bb0de19476541cd517ab14017e7fedbd9f13e3 (diff)
child 300462 8e6cf91601adcabe0002b229ba99cfebc11dce9a
push id5392
push userraliiev@mozilla.com
push dateMon, 14 Dec 2015 20:08:23 +0000
treeherdermozilla-beta@16ce8562a975 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone44.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to b2ginbound, a=merge
browser/components/loop/content/js/client.js
browser/components/loop/content/js/conversationViews.js
browser/components/loop/content/js/conversationViews.jsx
browser/components/loop/content/shared/img/audio-inverse-14x14.png
browser/components/loop/content/shared/img/audio-inverse-14x14@2x.png
browser/components/loop/content/shared/img/dropdown-inverse.png
browser/components/loop/content/shared/img/dropdown-inverse@2x.png
browser/components/loop/content/shared/img/video-inverse-14x14.png
browser/components/loop/content/shared/img/video-inverse-14x14@2x.png
browser/components/loop/content/shared/js/conversationStore.js
browser/components/loop/content/shared/js/websocket.js
browser/components/loop/test/desktop-local/client_test.js
browser/components/loop/test/desktop-local/conversationViews_test.js
browser/components/loop/test/shared/conversationStore_test.js
browser/components/loop/test/shared/websocket_test.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1702,17 +1702,16 @@ pref("loop.ringtone", "chrome://browser/
 pref("loop.retry_delay.start", 60000);
 pref("loop.retry_delay.limit", 300000);
 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", "https://www.mozilla.org/firefox/hello/npssurvey/");
 #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
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -196,17 +196,16 @@ skip-if = buildapp == 'mulet'
 skip-if = (os == 'linux' && e10s) # bug 1161699
 [browser_bug519216.js]
 [browser_bug520538.js]
 [browser_bug521216.js]
 [browser_bug533232.js]
 [browser_bug537013.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1134458 - Find bar doesn't work correctly in a detached tab
 [browser_bug537474.js]
-skip-if = e10s # Bug 1102020 - test tries to use browserDOMWindow.openURI to open a link, and gets a null rv where it expects a window
 [browser_bug550565.js]
 [browser_bug553455.js]
 skip-if = buildapp == 'mulet' # Bug 1066070 - I don't think either popup notifications nor addon install stuff works on mulet?
 [browser_bug555224.js]
 [browser_bug555767.js]
 [browser_bug556061.js]
 [browser_bug559991.js]
 [browser_bug561623.js]
--- a/browser/base/content/test/general/browser_bug537474.js
+++ b/browser/base/content/test/general/browser_bug537474.js
@@ -1,8 +1,8 @@
-function test() {
-  var currentWin = content;
-  var newWin =
-    browserDOMWindow.openURI(makeURI("about:"), null,
-                             Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW, null)
-  is(newWin, currentWin, "page loads in the current content window");
-  gBrowser.stop();
-}
+add_task(function *() {
+  let browserLoadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+  browserDOMWindow.openURI(makeURI("about:"), null,
+                           Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW, null)
+  yield browserLoadedPromise;
+  is(gBrowser.currentURI.spec, "about:", "page loads in the current content window");
+});
+
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -26,26 +26,21 @@
 
     <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/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/activeRoomStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.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/shared/js/linkifiedTextView.js"></script>
     <script type="text/javascript" src="loop/shared/js/urlRegExps.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>
     <script type="text/javascript" src="loop/js/roomViews.js"></script>
     <script type="text/javascript" src="loop/js/conversation.js"></script>
   </body>
 </html>
deleted file mode 100644
--- a/browser/components/loop/content/js/client.js
+++ /dev/null
@@ -1,119 +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.Client = (function() {
-  "use strict";
-
-  // THe expected properties to be returned from the POST /calls request.
-  var expectedPostCallProperties = [
-    "apiKey", "callId", "progressURL",
-    "sessionId", "sessionToken", "websocketToken"
-  ];
-
-  /**
-   * Loop server client.
-   *
-   * @param {Object} settings Settings object.
-   */
-  function Client(settings) {
-    if (!settings) {
-      settings = {};
-    }
-    // allowing an |in| test rather than a more type || allows us to dependency
-    // inject a non-existent mozLoop
-    if ("mozLoop" in settings) {
-      this.mozLoop = settings.mozLoop;
-    } else {
-      this.mozLoop = navigator.mozLoop;
-    }
-
-    this.settings = settings;
-  }
-
-  Client.prototype = {
-    /**
-     * Validates a data object to confirm it has the specified properties.
-     *
-     * @param  {Object} The data object to verify
-     * @param  {Array} The list of properties to verify within the object
-     * @return This returns either the specific property if only one
-     *         property is specified, or it returns all properties
-     */
-    _validate: function(data, properties) {
-      if (typeof data !== "object") {
-        throw new Error("Invalid data received from server");
-      }
-
-      properties.forEach(function (property) {
-        if (!data.hasOwnProperty(property)) {
-          throw new Error("Invalid data received from server - missing " +
-            property);
-        }
-      });
-
-      if (properties.length === 1) {
-        return data[properties[0]];
-      }
-
-      return data;
-    },
-
-    /**
-     * Generic handler for XHR failures.
-     *
-     * @param {Function} cb Callback(err)
-     * @param {Object} error See MozLoopAPI.hawkRequest
-     */
-    _failureHandler: function(cb, error) {
-      var message = "HTTP " + error.code + " " + error.error + "; " + error.message;
-      console.error(message);
-      cb(error);
-    },
-
-    /**
-     * Sets up an outgoing call, getting the relevant data from the server.
-     *
-     * Callback parameters:
-     * - err null on successful registration, non-null otherwise.
-     * - result an object of the obtained data for starting the call, if successful
-     *
-     * @param {Array} calleeIds an array of emails and phone numbers.
-     * @param {String} callType the type of call.
-     * @param {Function} cb Callback(err, result)
-     */
-    setupOutgoingCall: function(calleeIds, callType, cb) {
-      // For direct calls, we only ever use the logged-in session. Direct
-      // calls by guests aren't valid.
-      this.mozLoop.hawkRequest(this.mozLoop.LOOP_SESSION_TYPE.FXA,
-        "/calls", "POST", {
-          calleeId: calleeIds,
-          callType: callType,
-          channel: this.mozLoop.appVersionInfo ?
-                   this.mozLoop.appVersionInfo.channel : "unknown"
-        },
-        function (err, responseText) {
-          if (err) {
-            this._failureHandler(cb, err);
-            return;
-          }
-
-          try {
-            var postData = JSON.parse(responseText);
-
-            var outgoingCallData = this._validate(postData,
-              expectedPostCallProperties);
-
-            cb(null, outgoingCallData);
-          } catch (ex) {
-            console.log("Error requesting call info", ex);
-            cb(ex);
-          }
-        }.bind(this)
-      );
-    }
-  };
-
-  return Client;
-})();
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -3,21 +3,21 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var loop = loop || {};
 loop.conversation = (function(mozL10n) {
   "use strict";
 
   var sharedMixins = loop.shared.mixins;
   var sharedActions = loop.shared.actions;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
 
-  var CallControllerView = loop.conversationViews.CallControllerView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
   var FeedbackView = loop.feedbackViews.FeedbackView;
-  var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
+  var RoomFailureView = loop.roomViews.RoomFailureView;
 
   /**
    * 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,
@@ -63,39 +63,29 @@ loop.conversation = (function(mozL10n) {
     },
 
     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, {
-            chatWindowDetached: this.state.chatWindowDetached, 
-            dispatcher: this.props.dispatcher, 
-            mozLoop: this.props.mozLoop, 
-            onCallTerminated: this.handleCallTerminated}));
-        }
         case "room": {
           return (React.createElement(DesktopRoomConversationView, {
             chatWindowDetached: this.state.chatWindowDetached, 
             dispatcher: this.props.dispatcher, 
             mozLoop: this.props.mozLoop, 
             onCallTerminated: this.handleCallTerminated, 
             roomStore: this.props.roomStore}));
         }
         case "failed": {
-          return (React.createElement(DirectCallFailureView, {
-            contact: {}, 
+          return (React.createElement(RoomFailureView, {
             dispatcher: this.props.dispatcher, 
-            mozLoop: this.props.mozLoop, 
-            outgoing: false}));
+            failureReason: FAILURE_DETAILS.UNKNOWN, 
+            mozLoop: this.props.mozLoop}));
         }
         default: {
           // If we don't have a windowType, we don't know what we are yet,
           // so don't display anything.
           return null;
         }
       }
     }
@@ -122,35 +112,28 @@ loop.conversation = (function(mozL10n) {
         callback(null);
       }
     });
 
     // We want data channels only if the text chat preference is enabled.
     var useDataChannels = loop.shared.utils.getBoolPreference("textChat.enabled");
 
     var dispatcher = new loop.Dispatcher();
-    var client = new loop.Client();
     var sdkDriver = new loop.OTSdkDriver({
       isDesktop: true,
       useDataChannels: useDataChannels,
       dispatcher: dispatcher,
       sdk: OT,
       mozLoop: navigator.mozLoop
     });
 
     // expose for functional tests
     loop.conversation._sdkDriver = sdkDriver;
 
     // Create the stores.
-    var conversationStore = new loop.store.ConversationStore(dispatcher, {
-      client: client,
-      isDesktop: true,
-      mozLoop: navigator.mozLoop,
-      sdkDriver: sdkDriver
-    });
     var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       isDesktop: true,
       mozLoop: navigator.mozLoop,
       sdkDriver: sdkDriver
     });
     var conversationAppStore = new loop.store.ConversationAppStore({
       activeRoomStore: activeRoomStore,
       dispatcher: dispatcher,
@@ -161,17 +144,16 @@ loop.conversation = (function(mozL10n) {
       activeRoomStore: activeRoomStore
     });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: sdkDriver
     });
 
     loop.store.StoreMixin.register({
       conversationAppStore: conversationAppStore,
-      conversationStore: conversationStore,
       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
@@ -3,21 +3,21 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var loop = loop || {};
 loop.conversation = (function(mozL10n) {
   "use strict";
 
   var sharedMixins = loop.shared.mixins;
   var sharedActions = loop.shared.actions;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
 
-  var CallControllerView = loop.conversationViews.CallControllerView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
   var FeedbackView = loop.feedbackViews.FeedbackView;
-  var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
+  var RoomFailureView = loop.roomViews.RoomFailureView;
 
   /**
    * 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,
@@ -63,39 +63,29 @@ loop.conversation = (function(mozL10n) {
     },
 
     render: function() {
       if (this.state.showFeedbackForm) {
         return this._renderFeedbackForm();
       }
 
       switch(this.state.windowType) {
-        // CallControllerView is used for both.
-        case "incoming":
-        case "outgoing": {
-          return (<CallControllerView
-            chatWindowDetached={this.state.chatWindowDetached}
-            dispatcher={this.props.dispatcher}
-            mozLoop={this.props.mozLoop}
-            onCallTerminated={this.handleCallTerminated} />);
-        }
         case "room": {
           return (<DesktopRoomConversationView
             chatWindowDetached={this.state.chatWindowDetached}
             dispatcher={this.props.dispatcher}
             mozLoop={this.props.mozLoop}
             onCallTerminated={this.handleCallTerminated}
             roomStore={this.props.roomStore} />);
         }
         case "failed": {
-          return (<DirectCallFailureView
-            contact={{}}
+          return (<RoomFailureView
             dispatcher={this.props.dispatcher}
-            mozLoop={this.props.mozLoop}
-            outgoing={false} />);
+            failureReason={FAILURE_DETAILS.UNKNOWN}
+            mozLoop={this.props.mozLoop} />);
         }
         default: {
           // If we don't have a windowType, we don't know what we are yet,
           // so don't display anything.
           return null;
         }
       }
     }
@@ -122,35 +112,28 @@ loop.conversation = (function(mozL10n) {
         callback(null);
       }
     });
 
     // We want data channels only if the text chat preference is enabled.
     var useDataChannels = loop.shared.utils.getBoolPreference("textChat.enabled");
 
     var dispatcher = new loop.Dispatcher();
-    var client = new loop.Client();
     var sdkDriver = new loop.OTSdkDriver({
       isDesktop: true,
       useDataChannels: useDataChannels,
       dispatcher: dispatcher,
       sdk: OT,
       mozLoop: navigator.mozLoop
     });
 
     // expose for functional tests
     loop.conversation._sdkDriver = sdkDriver;
 
     // Create the stores.
-    var conversationStore = new loop.store.ConversationStore(dispatcher, {
-      client: client,
-      isDesktop: true,
-      mozLoop: navigator.mozLoop,
-      sdkDriver: sdkDriver
-    });
     var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
       isDesktop: true,
       mozLoop: navigator.mozLoop,
       sdkDriver: sdkDriver
     });
     var conversationAppStore = new loop.store.ConversationAppStore({
       activeRoomStore: activeRoomStore,
       dispatcher: dispatcher,
@@ -161,17 +144,16 @@ loop.conversation = (function(mozL10n) {
       activeRoomStore: activeRoomStore
     });
     var textChatStore = new loop.store.TextChatStore(dispatcher, {
       sdkDriver: sdkDriver
     });
 
     loop.store.StoreMixin.register({
       conversationAppStore: conversationAppStore,
-      conversationStore: conversationStore,
       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
@@ -137,20 +137,16 @@ loop.store.ConversationAppStore = (funct
     /**
      * Event handler; invoked when the 'LoopHangupNow' event is dispatched from
      * the window object.
      * It'll attempt to gracefully disconnect from an active session, or close
      * the window when no session is currently active.
      */
     LoopHangupNowHandler: function() {
       switch (this.getStoreState().windowType) {
-        case "incoming":
-        case "outgoing":
-          this._dispatcher.dispatch(new loop.shared.actions.HangupCall());
-          break;
         case "room":
           if (this._activeRoomStore.getStoreState().used &&
               !this._storeState.showFeedbackForm) {
             this._dispatcher.dispatch(new loop.shared.actions.LeaveRoom());
           } else {
             loop.shared.mixins.WindowCloseMixin.closeWindow();
           }
           break;
deleted file mode 100644
--- a/browser/components/loop/content/js/conversationViews.js
+++ /dev/null
@@ -1,870 +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.conversationViews = (function(mozL10n) {
-  "use strict";
-
-  var CALL_STATES = loop.store.CALL_STATES;
-  var CALL_TYPES = loop.shared.utils.CALL_TYPES;
-  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
-  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;
-
-  // 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.
-
-  function _getPreferredEmail(contact) {
-    // A contact may not contain email addresses, but only a phone number.
-    if (!contact.email || contact.email.length === 0) {
-      return { value: "" };
-    }
-    return contact.email.find(function find(e) { return e.pref; }) || contact.email[0];
-  }
-
-  function _getContactDisplayName(contact) {
-    if (contact.name && contact.name[0]) {
-      return contact.name[0];
-    }
-    return _getPreferredEmail(contact).value;
-  }
-
-  /**
-   * Displays information about the call
-   * Caller avatar, name & conversation creation date
-   */
-  var CallIdentifierView = React.createClass({displayName: "CallIdentifierView",
-    propTypes: {
-      peerIdentifier: React.PropTypes.string,
-      showIcons: React.PropTypes.bool.isRequired,
-      urlCreationDate: React.PropTypes.string,
-      video: React.PropTypes.bool
-    },
-
-    getDefaultProps: function() {
-      return {
-        peerIdentifier: "",
-        showLinkDetail: true,
-        urlCreationDate: "",
-        video: true
-      };
-    },
-
-    getInitialState: function() {
-      return {timestamp: 0};
-    },
-
-    /**
-     * Gets and formats the incoming call creation date
-     */
-    formatCreationDate: function() {
-      if (!this.props.urlCreationDate) {
-        return "";
-      }
-
-      var timestamp = this.props.urlCreationDate;
-      return "(" + loop.shared.utils.formatDate(timestamp) + ")";
-    },
-
-    render: function() {
-      var iconVideoClasses = React.addons.classSet({
-        "fx-embedded-tiny-video-icon": true,
-        "muted": !this.props.video
-      });
-      var callDetailClasses = React.addons.classSet({
-        "fx-embedded-call-detail": true,
-        "hide": !this.props.showIcons
-      });
-
-      return (
-        React.createElement("div", {className: "fx-embedded-call-identifier"}, 
-          React.createElement("div", {className: "fx-embedded-call-identifier-avatar fx-embedded-call-identifier-item"}), 
-          React.createElement("div", {className: "fx-embedded-call-identifier-info fx-embedded-call-identifier-item"}, 
-            React.createElement("div", {className: "fx-embedded-call-identifier-text overflow-text-ellipsis"}, 
-              this.props.peerIdentifier
-            ), 
-            React.createElement("div", {className: callDetailClasses}, 
-              React.createElement("span", {className: "fx-embedded-tiny-audio-icon"}), 
-              React.createElement("span", {className: iconVideoClasses}), 
-              React.createElement("span", {className: "fx-embedded-conversation-timestamp"}, 
-                this.formatCreationDate()
-              )
-            )
-          )
-        )
-      );
-    }
-  });
-
-  /**
-   * Displays details of the incoming/outgoing conversation
-   * (name, link, audio/video type etc).
-   *
-   * Allows the view to be extended with different buttons and progress
-   * via children properties.
-   */
-  var ConversationDetailView = React.createClass({displayName: "ConversationDetailView",
-    propTypes: {
-      children: React.PropTypes.oneOfType([
-        React.PropTypes.element,
-        React.PropTypes.arrayOf(React.PropTypes.element)
-      ]).isRequired,
-      contact: React.PropTypes.object
-    },
-
-    render: function() {
-      var contactName = _getContactDisplayName(this.props.contact);
-
-      return (
-        React.createElement("div", {className: "call-window"}, 
-          React.createElement(CallIdentifierView, {
-            peerIdentifier: contactName, 
-            showIcons: false}), 
-          React.createElement("div", null, this.props.children)
-        )
-      );
-    }
-  });
-
-  // Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
-  var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
-
-  var AcceptCallView = React.createClass({displayName: "AcceptCallView",
-    mixins: [sharedMixins.DropdownMenuMixin()],
-
-    propTypes: {
-      callType: React.PropTypes.string.isRequired,
-      callerId: React.PropTypes.string.isRequired,
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      mozLoop: React.PropTypes.object.isRequired,
-      // Only for use by the ui-showcase
-      showMenu: React.PropTypes.bool
-    },
-
-    getDefaultProps: function() {
-      return {
-        showMenu: false
-      };
-    },
-
-    componentDidMount: function() {
-      this.props.mozLoop.startAlerting();
-    },
-
-    componentWillUnmount: function() {
-      this.props.mozLoop.stopAlerting();
-    },
-
-    clickHandler: function(e) {
-      var target = e.target;
-      if (!target.classList.contains("btn-chevron")) {
-        this._hideDeclineMenu();
-      }
-    },
-
-    _handleAccept: function(callType) {
-      return function() {
-        this.props.dispatcher.dispatch(new sharedActions.AcceptCall({
-          callType: callType
-        }));
-      }.bind(this);
-    },
-
-    _handleDecline: function() {
-      this.props.dispatcher.dispatch(new sharedActions.DeclineCall({
-        blockCaller: false
-      }));
-    },
-
-    _handleDeclineBlock: function(e) {
-      this.props.dispatcher.dispatch(new sharedActions.DeclineCall({
-        blockCaller: true
-      }));
-
-      /* Prevent event propagation
-       * stop the click from reaching parent element */
-       e.stopPropagation();
-       e.preventDefault();
-    },
-
-    /*
-     * Generate props for <AcceptCallButton> component based on
-     * incoming call type. An incoming video call will render a video
-     * answer button primarily, an audio call will flip them.
-     **/
-    _answerModeProps: function() {
-      var videoButton = {
-        handler: this._handleAccept(CALL_TYPES.AUDIO_VIDEO),
-        className: "fx-embedded-btn-icon-video",
-        tooltip: "incoming_call_accept_audio_video_tooltip"
-      };
-      var audioButton = {
-        handler: this._handleAccept(CALL_TYPES.AUDIO_ONLY),
-        className: "fx-embedded-btn-audio-small",
-        tooltip: "incoming_call_accept_audio_only_tooltip"
-      };
-      var props = {};
-      props.primary = videoButton;
-      props.secondary = audioButton;
-
-      // When video is not enabled on this call, we swap the buttons around.
-      if (this.props.callType === CALL_TYPES.AUDIO_ONLY) {
-        audioButton.className = "fx-embedded-btn-icon-audio";
-        videoButton.className = "fx-embedded-btn-video-small";
-        props.primary = audioButton;
-        props.secondary = videoButton;
-      }
-
-      return props;
-    },
-
-    render: function() {
-      var dropdownMenuClassesDecline = React.addons.classSet({
-        "native-dropdown-menu": true,
-        "conversation-window-dropdown": true,
-        "visually-hidden": !this.state.showMenu
-      });
-
-      return (
-        React.createElement("div", {className: "call-window"}, 
-          React.createElement(CallIdentifierView, {
-            peerIdentifier: this.props.callerId, 
-            showIcons: true, 
-            video: this.props.callType === CALL_TYPES.AUDIO_VIDEO}), 
-
-          React.createElement("div", {className: "btn-group call-action-group"}, 
-
-            React.createElement("div", {className: "fx-embedded-call-button-spacer"}), 
-
-            React.createElement("div", {className: "btn-chevron-menu-group"}, 
-              React.createElement("div", {className: "btn-group-chevron"}, 
-                React.createElement("div", {className: "btn-group"}, 
-
-                  React.createElement("button", {className: "btn btn-decline", 
-                          onClick: this._handleDecline}, 
-                    mozL10n.get("incoming_call_cancel_button")
-                  ), 
-                  React.createElement("div", {className: "btn-chevron", 
-                       onClick: this.toggleDropdownMenu, 
-                       ref: "menu-button"})
-                ), 
-
-                React.createElement("ul", {className: dropdownMenuClassesDecline}, 
-                  React.createElement("li", {className: "btn-block", onClick: this._handleDeclineBlock}, 
-                    mozL10n.get("incoming_call_cancel_and_block_button")
-                  )
-                )
-
-              )
-            ), 
-
-            React.createElement("div", {className: "fx-embedded-call-button-spacer"}), 
-
-            React.createElement(AcceptCallButton, {mode: this._answerModeProps()}), 
-
-            React.createElement("div", {className: "fx-embedded-call-button-spacer"})
-
-          )
-        )
-      );
-    }
-  });
-
-  /**
-   * Incoming call view accept button, renders different primary actions
-   * (answer with video / with audio only) based on the props received
-   **/
-  var AcceptCallButton = React.createClass({displayName: "AcceptCallButton",
-
-    propTypes: {
-      mode: React.PropTypes.object.isRequired
-    },
-
-    render: function() {
-      var mode = this.props.mode;
-      return (
-        React.createElement("div", {className: "btn-chevron-menu-group"}, 
-          React.createElement("div", {className: "btn-group"}, 
-            React.createElement("button", {className: "btn btn-accept", 
-                    onClick: mode.primary.handler, 
-                    title: mozL10n.get(mode.primary.tooltip)}, 
-              React.createElement("span", {className: "fx-embedded-answer-btn-text"}, 
-                mozL10n.get("incoming_call_accept_button")
-              ), 
-              React.createElement("span", {className: mode.primary.className})
-            ), 
-            React.createElement("div", {className: mode.secondary.className, 
-                 onClick: mode.secondary.handler, 
-                 title: mozL10n.get(mode.secondary.tooltip)}
-            )
-          )
-        )
-      );
-    }
-  });
-
-  /**
-   * View for pending conversations. Displays a cancel button and appropriate
-   * pending/ringing strings.
-   */
-  var PendingConversationView = React.createClass({displayName: "PendingConversationView",
-    mixins: [sharedMixins.AudioMixin],
-
-    propTypes: {
-      callState: React.PropTypes.string,
-      contact: React.PropTypes.object,
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      enableCancelButton: React.PropTypes.bool
-    },
-
-    getDefaultProps: function() {
-      return {
-        enableCancelButton: false
-      };
-    },
-
-    componentDidMount: function() {
-      this.play("ringtone", {loop: true});
-    },
-
-    cancelCall: function() {
-      this.props.dispatcher.dispatch(new sharedActions.CancelCall());
-    },
-
-    render: function() {
-      var cx = React.addons.classSet;
-      var pendingStateString;
-      if (this.props.callState === CALL_STATES.ALERTING) {
-        pendingStateString = mozL10n.get("call_progress_ringing_description");
-      } else {
-        pendingStateString = mozL10n.get("call_progress_connecting_description");
-      }
-
-      var btnCancelStyles = cx({
-        "btn": true,
-        "btn-cancel": true,
-        "disabled": !this.props.enableCancelButton
-      });
-
-      return (
-        React.createElement(ConversationDetailView, {contact: this.props.contact}, 
-
-          React.createElement("p", {className: "btn-label"}, pendingStateString), 
-
-          React.createElement("div", {className: "btn-group call-action-group"}, 
-            React.createElement("button", {className: btnCancelStyles, 
-                    onClick: this.cancelCall}, 
-              mozL10n.get("initiate_call_cancel_button")
-            )
-          )
-
-        )
-      );
-    }
-  });
-
-  /**
-   * Used to display errors in direct calls and rooms to the user.
-   */
-  var FailureInfoView = React.createClass({displayName: "FailureInfoView",
-    propTypes: {
-      contact: React.PropTypes.object,
-      extraFailureMessage: React.PropTypes.string,
-      extraMessage: React.PropTypes.string,
-      failureReason: React.PropTypes.string.isRequired
-    },
-
-    /**
-     * Returns the translated message appropraite to the failure reason.
-     *
-     * @return {String} The translated message for the failure reason.
-     */
-    _getMessage: function() {
-      switch (this.props.failureReason) {
-        case FAILURE_DETAILS.USER_UNAVAILABLE:
-          var contactDisplayName = _getContactDisplayName(this.props.contact);
-          if (contactDisplayName.length) {
-            return mozL10n.get(
-              "contact_unavailable_title",
-              {"contactName": contactDisplayName});
-          }
-          return mozL10n.get("generic_contact_unavailable_title");
-        case FAILURE_DETAILS.NO_MEDIA:
-        case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
-          return mozL10n.get("no_media_failure_message");
-        case FAILURE_DETAILS.TOS_FAILURE:
-          return mozL10n.get("tos_failure_message",
-            { clientShortname: mozL10n.get("clientShortname2") });
-        case FAILURE_DETAILS.ICE_FAILED:
-          return mozL10n.get("ice_failure_message");
-        default:
-          return mozL10n.get("generic_failure_message");
-      }
-    },
-
-    _renderExtraMessage: function() {
-      if (this.props.extraMessage) {
-        return React.createElement("p", {className: "failure-info-extra"}, this.props.extraMessage);
-      }
-      return null;
-    },
-
-    _renderExtraFailureMessage: function() {
-      if (this.props.extraFailureMessage) {
-        return React.createElement("p", {className: "failure-info-extra-failure"}, this.props.extraFailureMessage);
-      }
-      return null;
-    },
-
-    render: function() {
-      return (
-        React.createElement("div", {className: "failure-info"}, 
-          React.createElement("div", {className: "failure-info-logo"}), 
-          React.createElement("h2", {className: "failure-info-message"}, this._getMessage()), 
-          this._renderExtraMessage(), 
-          this._renderExtraFailureMessage()
-        )
-      );
-    }
-  });
-
-  /**
-   * Direct Call failure view. Displayed when a call fails.
-   */
-  var DirectCallFailureView = React.createClass({displayName: "DirectCallFailureView",
-    mixins: [
-      Backbone.Events,
-      loop.store.StoreMixin("conversationStore"),
-      sharedMixins.AudioMixin,
-      sharedMixins.WindowCloseMixin
-    ],
-
-    propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      // This is used by the UI showcase.
-      emailLinkError: React.PropTypes.bool,
-      mozLoop: React.PropTypes.object.isRequired,
-      outgoing: React.PropTypes.bool.isRequired
-    },
-
-    getInitialState: function() {
-      return _.extend({
-        emailLinkError: this.props.emailLinkError,
-        emailLinkButtonDisabled: false
-      }, this.getStoreState());
-    },
-
-    componentDidMount: function() {
-      this.play("failure");
-      this.listenTo(this.getStore(), "change:emailLink",
-                    this._onEmailLinkReceived);
-      this.listenTo(this.getStore(), "error:emailLink",
-                    this._onEmailLinkError);
-    },
-
-    componentWillUnmount: function() {
-      this.stopListening(this.getStore());
-    },
-
-    _onEmailLinkReceived: function() {
-      var emailLink = this.getStoreState().emailLink;
-      var contactEmail = _getPreferredEmail(this.state.contact).value;
-      sharedUtils.composeCallUrlEmail(emailLink, contactEmail, null, "callfailed");
-      this.closeWindow();
-    },
-
-    _onEmailLinkError: function() {
-      this.setState({
-        emailLinkError: true,
-        emailLinkButtonDisabled: false
-      });
-    },
-
-    retryCall: function() {
-      this.props.dispatcher.dispatch(new sharedActions.RetryCall());
-    },
-
-    cancelCall: function() {
-      this.props.dispatcher.dispatch(new sharedActions.CancelCall());
-    },
-
-    emailLink: function() {
-      this.setState({
-        emailLinkError: false,
-        emailLinkButtonDisabled: true
-      });
-
-      this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
-        roomName: _getContactDisplayName(this.state.contact)
-      }));
-    },
-
-    render: function() {
-      var cx = React.addons.classSet;
-
-      var retryClasses = cx({
-        btn: true,
-        "btn-info": true,
-        "btn-retry": true,
-        hide: !this.props.outgoing
-      });
-      var emailClasses = cx({
-        btn: true,
-        "btn-info": true,
-        "btn-email": true,
-        hide: !this.props.outgoing
-      });
-
-      var settingsMenuItems = [
-        { id: "feedback" },
-        { id: "help" }
-      ];
-
-      var extraMessage;
-
-      if (this.props.outgoing) {
-        extraMessage = mozL10n.get("generic_failure_with_reason2");
-      }
-
-      var extraFailureMessage;
-
-      if (this.state.emailLinkError) {
-        extraFailureMessage = mozL10n.get("unable_retrieve_url");
-      }
-
-      return (
-        React.createElement("div", {className: "direct-call-failure"}, 
-          React.createElement(FailureInfoView, {
-            contact: this.state.contact, 
-            extraFailureMessage: extraFailureMessage, 
-            extraMessage: extraMessage, 
-            failureReason: this.getStoreState().callStateReason}), 
-
-          React.createElement("div", {className: "btn-group call-action-group"}, 
-            React.createElement("button", {className: "btn btn-cancel", 
-                    onClick: this.cancelCall}, 
-              mozL10n.get("cancel_button")
-            ), 
-            React.createElement("button", {className: retryClasses, 
-                    onClick: this.retryCall}, 
-              mozL10n.get("retry_call_button")
-            ), 
-            React.createElement("button", {className: emailClasses, 
-                    disabled: this.state.emailLinkButtonDisabled, 
-                    onClick: this.emailLink}, 
-              mozL10n.get("share_button3")
-            )
-          ), 
-          React.createElement(loop.shared.views.SettingsControlButton, {
-            menuBelow: true, 
-            menuItems: settingsMenuItems, 
-            mozLoop: this.props.mozLoop})
-        )
-      );
-    }
-  });
-
-  var OngoingConversationView = React.createClass({displayName: "OngoingConversationView",
-    mixins: [
-      sharedMixins.MediaSetupMixin
-    ],
-
-    propTypes: {
-      // local
-      audio: React.PropTypes.object,
-      chatWindowDetached: React.PropTypes.bool.isRequired,
-      // We pass conversationStore here rather than use the mixin, to allow
-      // easy configurability for the ui-showcase.
-      conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      // The poster URLs are for UI-showcase testing and development.
-      localPosterUrl: React.PropTypes.string,
-      // This is used from the props rather than the state to make it easier for
-      // the ui-showcase.
-      mediaConnected: React.PropTypes.bool,
-      mozLoop: React.PropTypes.object,
-      remotePosterUrl: React.PropTypes.string,
-      remoteVideoEnabled: React.PropTypes.bool,
-      // local
-      video: React.PropTypes.object
-    },
-
-    getDefaultProps: function() {
-      return {
-        video: {enabled: true, visible: true},
-        audio: {enabled: true, visible: true}
-      };
-    },
-
-    getInitialState: function() {
-      return this.props.conversationStore.getStoreState();
-    },
-
-    componentWillMount: function() {
-      this.props.conversationStore.on("change", function() {
-        this.setState(this.props.conversationStore.getStoreState());
-      }, this);
-    },
-
-    componentWillUnmount: function() {
-      this.props.conversationStore.off("change", null, this);
-    },
-
-    componentDidMount: function() {
-      // The SDK needs to know about the configuration and the elements to use
-      // for display. So the best way seems to pass the information here - ideally
-      // the sdk wouldn't need to know this, but we can't change that.
-      this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
-        publisherConfig: this.getDefaultPublisherConfig({
-          publishVideo: this.props.video.enabled
-        })
-      }));
-    },
-
-    /**
-     * Hangs up the call.
-     */
-    hangup: function() {
-      this.props.dispatcher.dispatch(
-        new sharedActions.HangupCall());
-    },
-
-    /**
-     * Used to control publishing a stream - i.e. to mute a stream
-     *
-     * @param {String} type The type of stream, e.g. "audio" or "video".
-     * @param {Boolean} enabled True to enable the stream, false otherwise.
-     */
-    publishStream: function(type, enabled) {
-      this.props.dispatcher.dispatch(
-        new sharedActions.SetMute({
-          type: type,
-          enabled: enabled
-        }));
-    },
-
-    /**
-     * Should we render a visual cue to the user (e.g. a spinner) that a local
-     * stream is on its way from the camera?
-     *
-     * @returns {boolean}
-     * @private
-     */
-    _isLocalLoading: function () {
-      return !this.state.localSrcMediaElement && !this.props.localPosterUrl;
-    },
-
-    /**
-     * Should we render a visual cue to the user (e.g. a spinner) that a remote
-     * stream is on its way from the other user?
-     *
-     * @returns {boolean}
-     * @private
-     */
-    _isRemoteLoading: function() {
-      return !!(!this.state.remoteSrcMediaElement &&
-                !this.props.remotePosterUrl &&
-                !this.state.mediaConnected);
-    },
-
-    shouldRenderRemoteVideo: function() {
-      if (this.props.mediaConnected) {
-        // If remote video is not enabled, we're muted, so we'll show an avatar
-        // instead.
-        return this.props.remoteVideoEnabled;
-      }
-
-      // We're not yet connected, but we don't want to show the avatar, and in
-      // the common case, we'll just transition to the video.
-      return true;
-    },
-
-    render: function() {
-      // 'visible' and 'enabled' are true by default.
-      var settingsMenuItems = [
-        {
-          id: "edit",
-          visible: false,
-          enabled: false
-        },
-        { id: "feedback" },
-        { id: "help" }
-      ];
-      return (
-        React.createElement("div", {className: "desktop-call-wrapper"}, 
-          React.createElement(sharedViews.MediaLayoutView, {
-            dispatcher: this.props.dispatcher, 
-            displayScreenShare: false, 
-            isLocalLoading: this._isLocalLoading(), 
-            isRemoteLoading: this._isRemoteLoading(), 
-            isScreenShareLoading: false, 
-            localPosterUrl: this.props.localPosterUrl, 
-            localSrcMediaElement: this.state.localSrcMediaElement, 
-            localVideoMuted: !this.props.video.enabled, 
-            matchMedia: this.state.matchMedia || window.matchMedia.bind(window), 
-            remotePosterUrl: this.props.remotePosterUrl, 
-            remoteSrcMediaElement: this.state.remoteSrcMediaElement, 
-            renderRemoteVideo: this.shouldRenderRemoteVideo(), 
-            screenShareMediaElement: this.state.screenShareMediaElement, 
-            screenSharePosterUrl: null, 
-            showContextRoomName: false, 
-            useDesktopPaths: true}, 
-            React.createElement(sharedViews.ConversationToolbar, {
-              audio: this.props.audio, 
-              dispatcher: this.props.dispatcher, 
-              hangup: this.hangup, 
-              mozLoop: this.props.mozLoop, 
-              publishStream: this.publishStream, 
-              settingsMenuItems: settingsMenuItems, 
-              show: true, 
-              showHangup: this.props.chatWindowDetached, 
-              video: this.props.video})
-          )
-        )
-      );
-    }
-  });
-
-  /**
-   * Master View Controller for outgoing calls. This manages
-   * the different views that need displaying.
-   */
-  var CallControllerView = React.createClass({displayName: "CallControllerView",
-    mixins: [
-      sharedMixins.AudioMixin,
-      sharedMixins.DocumentTitleMixin,
-      loop.store.StoreMixin("conversationStore"),
-      Backbone.Events
-    ],
-
-    propTypes: {
-      chatWindowDetached: React.PropTypes.bool.isRequired,
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      mozLoop: React.PropTypes.object.isRequired,
-      onCallTerminated: React.PropTypes.func.isRequired
-    },
-
-    getInitialState: function() {
-      return this.getStoreState();
-    },
-
-    _closeWindow: function() {
-      window.close();
-    },
-
-    /**
-     * 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;
-    },
-
-    _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, 
-          enableCancelButton: this._isCancellable()}));
-      }
-
-      // For incoming calls that are in accepting state, display the
-      // accept call view.
-      if (this.state.callState === CALL_STATES.ALERTING) {
-        return (React.createElement(AcceptCallView, {
-          callType: this.state.callType, 
-          callerId: this.state.callerId, 
-          dispatcher: this.props.dispatcher, 
-          mozLoop: this.props.mozLoop}
-        ));
-      }
-
-      // 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 || "");
-      }
-
-      switch (this.state.callState) {
-        case CALL_STATES.CLOSE: {
-          this._closeWindow();
-          return null;
-        }
-        case CALL_STATES.TERMINATED: {
-          return (React.createElement(DirectCallFailureView, {
-            dispatcher: this.props.dispatcher, 
-            mozLoop: this.props.mozLoop, 
-            outgoing: this.state.outgoing}));
-        }
-        case CALL_STATES.ONGOING: {
-          return (React.createElement(OngoingConversationView, {
-            audio: { enabled: !this.state.audioMuted, visible: true}, 
-            chatWindowDetached: this.props.chatWindowDetached, 
-            conversationStore: this.getStore(), 
-            dispatcher: this.props.dispatcher, 
-            mediaConnected: this.state.mediaConnected, 
-            mozLoop: this.props.mozLoop, 
-            remoteSrcMediaElement: this.state.remoteSrcMediaElement, 
-            remoteVideoEnabled: this.state.remoteVideoEnabled, 
-            video: { enabled: !this.state.videoMuted, visible: true}})
-          );
-        }
-        case CALL_STATES.FINISHED: {
-          this.play("terminated");
-
-          // 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();
-        }
-      }
-    }
-  });
-
-  return {
-    PendingConversationView: PendingConversationView,
-    CallIdentifierView: CallIdentifierView,
-    ConversationDetailView: ConversationDetailView,
-    _getContactDisplayName: _getContactDisplayName,
-    FailureInfoView: FailureInfoView,
-    DirectCallFailureView: DirectCallFailureView,
-    AcceptCallView: AcceptCallView,
-    OngoingConversationView: OngoingConversationView,
-    CallControllerView: CallControllerView
-  };
-
-})(document.mozL10n || navigator.mozL10n);
deleted file mode 100644
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ /dev/null
@@ -1,870 +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.conversationViews = (function(mozL10n) {
-  "use strict";
-
-  var CALL_STATES = loop.store.CALL_STATES;
-  var CALL_TYPES = loop.shared.utils.CALL_TYPES;
-  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
-  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;
-
-  // 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.
-
-  function _getPreferredEmail(contact) {
-    // A contact may not contain email addresses, but only a phone number.
-    if (!contact.email || contact.email.length === 0) {
-      return { value: "" };
-    }
-    return contact.email.find(function find(e) { return e.pref; }) || contact.email[0];
-  }
-
-  function _getContactDisplayName(contact) {
-    if (contact.name && contact.name[0]) {
-      return contact.name[0];
-    }
-    return _getPreferredEmail(contact).value;
-  }
-
-  /**
-   * Displays information about the call
-   * Caller avatar, name & conversation creation date
-   */
-  var CallIdentifierView = React.createClass({
-    propTypes: {
-      peerIdentifier: React.PropTypes.string,
-      showIcons: React.PropTypes.bool.isRequired,
-      urlCreationDate: React.PropTypes.string,
-      video: React.PropTypes.bool
-    },
-
-    getDefaultProps: function() {
-      return {
-        peerIdentifier: "",
-        showLinkDetail: true,
-        urlCreationDate: "",
-        video: true
-      };
-    },
-
-    getInitialState: function() {
-      return {timestamp: 0};
-    },
-
-    /**
-     * Gets and formats the incoming call creation date
-     */
-    formatCreationDate: function() {
-      if (!this.props.urlCreationDate) {
-        return "";
-      }
-
-      var timestamp = this.props.urlCreationDate;
-      return "(" + loop.shared.utils.formatDate(timestamp) + ")";
-    },
-
-    render: function() {
-      var iconVideoClasses = React.addons.classSet({
-        "fx-embedded-tiny-video-icon": true,
-        "muted": !this.props.video
-      });
-      var callDetailClasses = React.addons.classSet({
-        "fx-embedded-call-detail": true,
-        "hide": !this.props.showIcons
-      });
-
-      return (
-        <div className="fx-embedded-call-identifier">
-          <div className="fx-embedded-call-identifier-avatar fx-embedded-call-identifier-item"/>
-          <div className="fx-embedded-call-identifier-info fx-embedded-call-identifier-item">
-            <div className="fx-embedded-call-identifier-text overflow-text-ellipsis">
-              {this.props.peerIdentifier}
-            </div>
-            <div className={callDetailClasses}>
-              <span className="fx-embedded-tiny-audio-icon"></span>
-              <span className={iconVideoClasses}></span>
-              <span className="fx-embedded-conversation-timestamp">
-                {this.formatCreationDate()}
-              </span>
-            </div>
-          </div>
-        </div>
-      );
-    }
-  });
-
-  /**
-   * Displays details of the incoming/outgoing conversation
-   * (name, link, audio/video type etc).
-   *
-   * Allows the view to be extended with different buttons and progress
-   * via children properties.
-   */
-  var ConversationDetailView = React.createClass({
-    propTypes: {
-      children: React.PropTypes.oneOfType([
-        React.PropTypes.element,
-        React.PropTypes.arrayOf(React.PropTypes.element)
-      ]).isRequired,
-      contact: React.PropTypes.object
-    },
-
-    render: function() {
-      var contactName = _getContactDisplayName(this.props.contact);
-
-      return (
-        <div className="call-window">
-          <CallIdentifierView
-            peerIdentifier={contactName}
-            showIcons={false} />
-          <div>{this.props.children}</div>
-        </div>
-      );
-    }
-  });
-
-  // Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
-  var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
-
-  var AcceptCallView = React.createClass({
-    mixins: [sharedMixins.DropdownMenuMixin()],
-
-    propTypes: {
-      callType: React.PropTypes.string.isRequired,
-      callerId: React.PropTypes.string.isRequired,
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      mozLoop: React.PropTypes.object.isRequired,
-      // Only for use by the ui-showcase
-      showMenu: React.PropTypes.bool
-    },
-
-    getDefaultProps: function() {
-      return {
-        showMenu: false
-      };
-    },
-
-    componentDidMount: function() {
-      this.props.mozLoop.startAlerting();
-    },
-
-    componentWillUnmount: function() {
-      this.props.mozLoop.stopAlerting();
-    },
-
-    clickHandler: function(e) {
-      var target = e.target;
-      if (!target.classList.contains("btn-chevron")) {
-        this._hideDeclineMenu();
-      }
-    },
-
-    _handleAccept: function(callType) {
-      return function() {
-        this.props.dispatcher.dispatch(new sharedActions.AcceptCall({
-          callType: callType
-        }));
-      }.bind(this);
-    },
-
-    _handleDecline: function() {
-      this.props.dispatcher.dispatch(new sharedActions.DeclineCall({
-        blockCaller: false
-      }));
-    },
-
-    _handleDeclineBlock: function(e) {
-      this.props.dispatcher.dispatch(new sharedActions.DeclineCall({
-        blockCaller: true
-      }));
-
-      /* Prevent event propagation
-       * stop the click from reaching parent element */
-       e.stopPropagation();
-       e.preventDefault();
-    },
-
-    /*
-     * Generate props for <AcceptCallButton> component based on
-     * incoming call type. An incoming video call will render a video
-     * answer button primarily, an audio call will flip them.
-     **/
-    _answerModeProps: function() {
-      var videoButton = {
-        handler: this._handleAccept(CALL_TYPES.AUDIO_VIDEO),
-        className: "fx-embedded-btn-icon-video",
-        tooltip: "incoming_call_accept_audio_video_tooltip"
-      };
-      var audioButton = {
-        handler: this._handleAccept(CALL_TYPES.AUDIO_ONLY),
-        className: "fx-embedded-btn-audio-small",
-        tooltip: "incoming_call_accept_audio_only_tooltip"
-      };
-      var props = {};
-      props.primary = videoButton;
-      props.secondary = audioButton;
-
-      // When video is not enabled on this call, we swap the buttons around.
-      if (this.props.callType === CALL_TYPES.AUDIO_ONLY) {
-        audioButton.className = "fx-embedded-btn-icon-audio";
-        videoButton.className = "fx-embedded-btn-video-small";
-        props.primary = audioButton;
-        props.secondary = videoButton;
-      }
-
-      return props;
-    },
-
-    render: function() {
-      var dropdownMenuClassesDecline = React.addons.classSet({
-        "native-dropdown-menu": true,
-        "conversation-window-dropdown": true,
-        "visually-hidden": !this.state.showMenu
-      });
-
-      return (
-        <div className="call-window">
-          <CallIdentifierView
-            peerIdentifier={this.props.callerId}
-            showIcons={true}
-            video={this.props.callType === CALL_TYPES.AUDIO_VIDEO} />
-
-          <div className="btn-group call-action-group">
-
-            <div className="fx-embedded-call-button-spacer"></div>
-
-            <div className="btn-chevron-menu-group">
-              <div className="btn-group-chevron">
-                <div className="btn-group">
-
-                  <button className="btn btn-decline"
-                          onClick={this._handleDecline}>
-                    {mozL10n.get("incoming_call_cancel_button")}
-                  </button>
-                  <div className="btn-chevron"
-                       onClick={this.toggleDropdownMenu}
-                       ref="menu-button" />
-                </div>
-
-                <ul className={dropdownMenuClassesDecline}>
-                  <li className="btn-block" onClick={this._handleDeclineBlock}>
-                    {mozL10n.get("incoming_call_cancel_and_block_button")}
-                  </li>
-                </ul>
-
-              </div>
-            </div>
-
-            <div className="fx-embedded-call-button-spacer"></div>
-
-            <AcceptCallButton mode={this._answerModeProps()} />
-
-            <div className="fx-embedded-call-button-spacer"></div>
-
-          </div>
-        </div>
-      );
-    }
-  });
-
-  /**
-   * Incoming call view accept button, renders different primary actions
-   * (answer with video / with audio only) based on the props received
-   **/
-  var AcceptCallButton = React.createClass({
-
-    propTypes: {
-      mode: React.PropTypes.object.isRequired
-    },
-
-    render: function() {
-      var mode = this.props.mode;
-      return (
-        <div className="btn-chevron-menu-group">
-          <div className="btn-group">
-            <button className="btn btn-accept"
-                    onClick={mode.primary.handler}
-                    title={mozL10n.get(mode.primary.tooltip)}>
-              <span className="fx-embedded-answer-btn-text">
-                {mozL10n.get("incoming_call_accept_button")}
-              </span>
-              <span className={mode.primary.className}></span>
-            </button>
-            <div className={mode.secondary.className}
-                 onClick={mode.secondary.handler}
-                 title={mozL10n.get(mode.secondary.tooltip)}>
-            </div>
-          </div>
-        </div>
-      );
-    }
-  });
-
-  /**
-   * View for pending conversations. Displays a cancel button and appropriate
-   * pending/ringing strings.
-   */
-  var PendingConversationView = React.createClass({
-    mixins: [sharedMixins.AudioMixin],
-
-    propTypes: {
-      callState: React.PropTypes.string,
-      contact: React.PropTypes.object,
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      enableCancelButton: React.PropTypes.bool
-    },
-
-    getDefaultProps: function() {
-      return {
-        enableCancelButton: false
-      };
-    },
-
-    componentDidMount: function() {
-      this.play("ringtone", {loop: true});
-    },
-
-    cancelCall: function() {
-      this.props.dispatcher.dispatch(new sharedActions.CancelCall());
-    },
-
-    render: function() {
-      var cx = React.addons.classSet;
-      var pendingStateString;
-      if (this.props.callState === CALL_STATES.ALERTING) {
-        pendingStateString = mozL10n.get("call_progress_ringing_description");
-      } else {
-        pendingStateString = mozL10n.get("call_progress_connecting_description");
-      }
-
-      var btnCancelStyles = cx({
-        "btn": true,
-        "btn-cancel": true,
-        "disabled": !this.props.enableCancelButton
-      });
-
-      return (
-        <ConversationDetailView contact={this.props.contact}>
-
-          <p className="btn-label">{pendingStateString}</p>
-
-          <div className="btn-group call-action-group">
-            <button className={btnCancelStyles}
-                    onClick={this.cancelCall}>
-              {mozL10n.get("initiate_call_cancel_button")}
-            </button>
-          </div>
-
-        </ConversationDetailView>
-      );
-    }
-  });
-
-  /**
-   * Used to display errors in direct calls and rooms to the user.
-   */
-  var FailureInfoView = React.createClass({
-    propTypes: {
-      contact: React.PropTypes.object,
-      extraFailureMessage: React.PropTypes.string,
-      extraMessage: React.PropTypes.string,
-      failureReason: React.PropTypes.string.isRequired
-    },
-
-    /**
-     * Returns the translated message appropraite to the failure reason.
-     *
-     * @return {String} The translated message for the failure reason.
-     */
-    _getMessage: function() {
-      switch (this.props.failureReason) {
-        case FAILURE_DETAILS.USER_UNAVAILABLE:
-          var contactDisplayName = _getContactDisplayName(this.props.contact);
-          if (contactDisplayName.length) {
-            return mozL10n.get(
-              "contact_unavailable_title",
-              {"contactName": contactDisplayName});
-          }
-          return mozL10n.get("generic_contact_unavailable_title");
-        case FAILURE_DETAILS.NO_MEDIA:
-        case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
-          return mozL10n.get("no_media_failure_message");
-        case FAILURE_DETAILS.TOS_FAILURE:
-          return mozL10n.get("tos_failure_message",
-            { clientShortname: mozL10n.get("clientShortname2") });
-        case FAILURE_DETAILS.ICE_FAILED:
-          return mozL10n.get("ice_failure_message");
-        default:
-          return mozL10n.get("generic_failure_message");
-      }
-    },
-
-    _renderExtraMessage: function() {
-      if (this.props.extraMessage) {
-        return <p className="failure-info-extra">{this.props.extraMessage}</p>;
-      }
-      return null;
-    },
-
-    _renderExtraFailureMessage: function() {
-      if (this.props.extraFailureMessage) {
-        return <p className="failure-info-extra-failure">{this.props.extraFailureMessage}</p>;
-      }
-      return null;
-    },
-
-    render: function() {
-      return (
-        <div className="failure-info">
-          <div className="failure-info-logo" />
-          <h2 className="failure-info-message">{this._getMessage()}</h2>
-          {this._renderExtraMessage()}
-          {this._renderExtraFailureMessage()}
-        </div>
-      );
-    }
-  });
-
-  /**
-   * Direct Call failure view. Displayed when a call fails.
-   */
-  var DirectCallFailureView = React.createClass({
-    mixins: [
-      Backbone.Events,
-      loop.store.StoreMixin("conversationStore"),
-      sharedMixins.AudioMixin,
-      sharedMixins.WindowCloseMixin
-    ],
-
-    propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      // This is used by the UI showcase.
-      emailLinkError: React.PropTypes.bool,
-      mozLoop: React.PropTypes.object.isRequired,
-      outgoing: React.PropTypes.bool.isRequired
-    },
-
-    getInitialState: function() {
-      return _.extend({
-        emailLinkError: this.props.emailLinkError,
-        emailLinkButtonDisabled: false
-      }, this.getStoreState());
-    },
-
-    componentDidMount: function() {
-      this.play("failure");
-      this.listenTo(this.getStore(), "change:emailLink",
-                    this._onEmailLinkReceived);
-      this.listenTo(this.getStore(), "error:emailLink",
-                    this._onEmailLinkError);
-    },
-
-    componentWillUnmount: function() {
-      this.stopListening(this.getStore());
-    },
-
-    _onEmailLinkReceived: function() {
-      var emailLink = this.getStoreState().emailLink;
-      var contactEmail = _getPreferredEmail(this.state.contact).value;
-      sharedUtils.composeCallUrlEmail(emailLink, contactEmail, null, "callfailed");
-      this.closeWindow();
-    },
-
-    _onEmailLinkError: function() {
-      this.setState({
-        emailLinkError: true,
-        emailLinkButtonDisabled: false
-      });
-    },
-
-    retryCall: function() {
-      this.props.dispatcher.dispatch(new sharedActions.RetryCall());
-    },
-
-    cancelCall: function() {
-      this.props.dispatcher.dispatch(new sharedActions.CancelCall());
-    },
-
-    emailLink: function() {
-      this.setState({
-        emailLinkError: false,
-        emailLinkButtonDisabled: true
-      });
-
-      this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
-        roomName: _getContactDisplayName(this.state.contact)
-      }));
-    },
-
-    render: function() {
-      var cx = React.addons.classSet;
-
-      var retryClasses = cx({
-        btn: true,
-        "btn-info": true,
-        "btn-retry": true,
-        hide: !this.props.outgoing
-      });
-      var emailClasses = cx({
-        btn: true,
-        "btn-info": true,
-        "btn-email": true,
-        hide: !this.props.outgoing
-      });
-
-      var settingsMenuItems = [
-        { id: "feedback" },
-        { id: "help" }
-      ];
-
-      var extraMessage;
-
-      if (this.props.outgoing) {
-        extraMessage = mozL10n.get("generic_failure_with_reason2");
-      }
-
-      var extraFailureMessage;
-
-      if (this.state.emailLinkError) {
-        extraFailureMessage = mozL10n.get("unable_retrieve_url");
-      }
-
-      return (
-        <div className="direct-call-failure">
-          <FailureInfoView
-            contact={this.state.contact}
-            extraFailureMessage={extraFailureMessage}
-            extraMessage={extraMessage}
-            failureReason={this.getStoreState().callStateReason}/>
-
-          <div className="btn-group call-action-group">
-            <button className="btn btn-cancel"
-                    onClick={this.cancelCall}>
-              {mozL10n.get("cancel_button")}
-            </button>
-            <button className={retryClasses}
-                    onClick={this.retryCall}>
-              {mozL10n.get("retry_call_button")}
-            </button>
-            <button className={emailClasses}
-                    disabled={this.state.emailLinkButtonDisabled}
-                    onClick={this.emailLink}>
-              {mozL10n.get("share_button3")}
-            </button>
-          </div>
-          <loop.shared.views.SettingsControlButton
-            menuBelow={true}
-            menuItems={settingsMenuItems}
-            mozLoop={this.props.mozLoop} />
-        </div>
-      );
-    }
-  });
-
-  var OngoingConversationView = React.createClass({
-    mixins: [
-      sharedMixins.MediaSetupMixin
-    ],
-
-    propTypes: {
-      // local
-      audio: React.PropTypes.object,
-      chatWindowDetached: React.PropTypes.bool.isRequired,
-      // We pass conversationStore here rather than use the mixin, to allow
-      // easy configurability for the ui-showcase.
-      conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      // The poster URLs are for UI-showcase testing and development.
-      localPosterUrl: React.PropTypes.string,
-      // This is used from the props rather than the state to make it easier for
-      // the ui-showcase.
-      mediaConnected: React.PropTypes.bool,
-      mozLoop: React.PropTypes.object,
-      remotePosterUrl: React.PropTypes.string,
-      remoteVideoEnabled: React.PropTypes.bool,
-      // local
-      video: React.PropTypes.object
-    },
-
-    getDefaultProps: function() {
-      return {
-        video: {enabled: true, visible: true},
-        audio: {enabled: true, visible: true}
-      };
-    },
-
-    getInitialState: function() {
-      return this.props.conversationStore.getStoreState();
-    },
-
-    componentWillMount: function() {
-      this.props.conversationStore.on("change", function() {
-        this.setState(this.props.conversationStore.getStoreState());
-      }, this);
-    },
-
-    componentWillUnmount: function() {
-      this.props.conversationStore.off("change", null, this);
-    },
-
-    componentDidMount: function() {
-      // The SDK needs to know about the configuration and the elements to use
-      // for display. So the best way seems to pass the information here - ideally
-      // the sdk wouldn't need to know this, but we can't change that.
-      this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
-        publisherConfig: this.getDefaultPublisherConfig({
-          publishVideo: this.props.video.enabled
-        })
-      }));
-    },
-
-    /**
-     * Hangs up the call.
-     */
-    hangup: function() {
-      this.props.dispatcher.dispatch(
-        new sharedActions.HangupCall());
-    },
-
-    /**
-     * Used to control publishing a stream - i.e. to mute a stream
-     *
-     * @param {String} type The type of stream, e.g. "audio" or "video".
-     * @param {Boolean} enabled True to enable the stream, false otherwise.
-     */
-    publishStream: function(type, enabled) {
-      this.props.dispatcher.dispatch(
-        new sharedActions.SetMute({
-          type: type,
-          enabled: enabled
-        }));
-    },
-
-    /**
-     * Should we render a visual cue to the user (e.g. a spinner) that a local
-     * stream is on its way from the camera?
-     *
-     * @returns {boolean}
-     * @private
-     */
-    _isLocalLoading: function () {
-      return !this.state.localSrcMediaElement && !this.props.localPosterUrl;
-    },
-
-    /**
-     * Should we render a visual cue to the user (e.g. a spinner) that a remote
-     * stream is on its way from the other user?
-     *
-     * @returns {boolean}
-     * @private
-     */
-    _isRemoteLoading: function() {
-      return !!(!this.state.remoteSrcMediaElement &&
-                !this.props.remotePosterUrl &&
-                !this.state.mediaConnected);
-    },
-
-    shouldRenderRemoteVideo: function() {
-      if (this.props.mediaConnected) {
-        // If remote video is not enabled, we're muted, so we'll show an avatar
-        // instead.
-        return this.props.remoteVideoEnabled;
-      }
-
-      // We're not yet connected, but we don't want to show the avatar, and in
-      // the common case, we'll just transition to the video.
-      return true;
-    },
-
-    render: function() {
-      // 'visible' and 'enabled' are true by default.
-      var settingsMenuItems = [
-        {
-          id: "edit",
-          visible: false,
-          enabled: false
-        },
-        { id: "feedback" },
-        { id: "help" }
-      ];
-      return (
-        <div className="desktop-call-wrapper">
-          <sharedViews.MediaLayoutView
-            dispatcher={this.props.dispatcher}
-            displayScreenShare={false}
-            isLocalLoading={this._isLocalLoading()}
-            isRemoteLoading={this._isRemoteLoading()}
-            isScreenShareLoading={false}
-            localPosterUrl={this.props.localPosterUrl}
-            localSrcMediaElement={this.state.localSrcMediaElement}
-            localVideoMuted={!this.props.video.enabled}
-            matchMedia={this.state.matchMedia || window.matchMedia.bind(window)}
-            remotePosterUrl={this.props.remotePosterUrl}
-            remoteSrcMediaElement={this.state.remoteSrcMediaElement}
-            renderRemoteVideo={this.shouldRenderRemoteVideo()}
-            screenShareMediaElement={this.state.screenShareMediaElement}
-            screenSharePosterUrl={null}
-            showContextRoomName={false}
-            useDesktopPaths={true}>
-            <sharedViews.ConversationToolbar
-              audio={this.props.audio}
-              dispatcher={this.props.dispatcher}
-              hangup={this.hangup}
-              mozLoop={this.props.mozLoop}
-              publishStream={this.publishStream}
-              settingsMenuItems={settingsMenuItems}
-              show={true}
-              showHangup={this.props.chatWindowDetached}
-              video={this.props.video} />
-          </sharedViews.MediaLayoutView>
-        </div>
-      );
-    }
-  });
-
-  /**
-   * Master View Controller for outgoing calls. This manages
-   * the different views that need displaying.
-   */
-  var CallControllerView = React.createClass({
-    mixins: [
-      sharedMixins.AudioMixin,
-      sharedMixins.DocumentTitleMixin,
-      loop.store.StoreMixin("conversationStore"),
-      Backbone.Events
-    ],
-
-    propTypes: {
-      chatWindowDetached: React.PropTypes.bool.isRequired,
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      mozLoop: React.PropTypes.object.isRequired,
-      onCallTerminated: React.PropTypes.func.isRequired
-    },
-
-    getInitialState: function() {
-      return this.getStoreState();
-    },
-
-    _closeWindow: function() {
-      window.close();
-    },
-
-    /**
-     * 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;
-    },
-
-    _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}
-          enableCancelButton={this._isCancellable()} />);
-      }
-
-      // For incoming calls that are in accepting state, display the
-      // accept call view.
-      if (this.state.callState === CALL_STATES.ALERTING) {
-        return (<AcceptCallView
-          callType={this.state.callType}
-          callerId={this.state.callerId}
-          dispatcher={this.props.dispatcher}
-          mozLoop={this.props.mozLoop}
-        />);
-      }
-
-      // 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 || "");
-      }
-
-      switch (this.state.callState) {
-        case CALL_STATES.CLOSE: {
-          this._closeWindow();
-          return null;
-        }
-        case CALL_STATES.TERMINATED: {
-          return (<DirectCallFailureView
-            dispatcher={this.props.dispatcher}
-            mozLoop={this.props.mozLoop}
-            outgoing={this.state.outgoing} />);
-        }
-        case CALL_STATES.ONGOING: {
-          return (<OngoingConversationView
-            audio={{ enabled: !this.state.audioMuted, visible: true }}
-            chatWindowDetached={this.props.chatWindowDetached}
-            conversationStore={this.getStore()}
-            dispatcher={this.props.dispatcher}
-            mediaConnected={this.state.mediaConnected}
-            mozLoop={this.props.mozLoop}
-            remoteSrcMediaElement={this.state.remoteSrcMediaElement}
-            remoteVideoEnabled={this.state.remoteVideoEnabled}
-            video={{ enabled: !this.state.videoMuted, visible: true }} />
-          );
-        }
-        case CALL_STATES.FINISHED: {
-          this.play("terminated");
-
-          // 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();
-        }
-      }
-    }
-  });
-
-  return {
-    PendingConversationView: PendingConversationView,
-    CallIdentifierView: CallIdentifierView,
-    ConversationDetailView: ConversationDetailView,
-    _getContactDisplayName: _getContactDisplayName,
-    FailureInfoView: FailureInfoView,
-    DirectCallFailureView: DirectCallFailureView,
-    AcceptCallView: AcceptCallView,
-    OngoingConversationView: OngoingConversationView,
-    CallControllerView: CallControllerView
-  };
-
-})(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -71,16 +71,54 @@ loop.roomViews = (function(mozL10n) {
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState,
         savingContext: false
       }, storeState);
     }
   };
 
   /**
+   * Used to display errors in direct calls and rooms to the user.
+   */
+  var FailureInfoView = React.createClass({displayName: "FailureInfoView",
+    propTypes: {
+      failureReason: React.PropTypes.string.isRequired
+    },
+
+    /**
+     * Returns the translated message appropraite to the failure reason.
+     *
+     * @return {String} The translated message for the failure reason.
+     */
+    _getMessage: function() {
+      switch (this.props.failureReason) {
+        case FAILURE_DETAILS.NO_MEDIA:
+        case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
+          return mozL10n.get("no_media_failure_message");
+        case FAILURE_DETAILS.TOS_FAILURE:
+          return mozL10n.get("tos_failure_message",
+            { clientShortname: mozL10n.get("clientShortname2") });
+        case FAILURE_DETAILS.ICE_FAILED:
+          return mozL10n.get("ice_failure_message");
+        default:
+          return mozL10n.get("generic_failure_message");
+      }
+    },
+
+    render: function() {
+      return (
+        React.createElement("div", {className: "failure-info"}, 
+          React.createElement("div", {className: "failure-info-logo"}), 
+          React.createElement("h2", {className: "failure-info-message"}, this._getMessage())
+        )
+      );
+    }
+  });
+
+  /**
    * Something went wrong view. Displayed when there's a big problem.
    */
   var RoomFailureView = React.createClass({displayName: "RoomFailureView",
     mixins: [ sharedMixins.AudioMixin ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       failureReason: React.PropTypes.string,
@@ -105,18 +143,17 @@ loop.roomViews = (function(mozL10n) {
       if (this.props.failureReason === FAILURE_DETAILS.ICE_FAILED) {
         btnTitle = mozL10n.get("retry_call_button");
       } else {
         btnTitle = mozL10n.get("rejoin_button");
       }
 
       return (
         React.createElement("div", {className: "room-failure"}, 
-          React.createElement(loop.conversationViews.FailureInfoView, {
-            failureReason: this.props.failureReason}), 
+          React.createElement(FailureInfoView, {failureReason: this.props.failureReason}), 
           React.createElement("div", {className: "btn-group call-action-group"}, 
             React.createElement("button", {className: "btn btn-info btn-rejoin", 
                     onClick: this.handleRejoinCall}, 
               btnTitle
             )
           ), 
           React.createElement(loop.shared.views.SettingsControlButton, {
             menuBelow: true, 
@@ -815,16 +852,17 @@ loop.roomViews = (function(mozL10n) {
           );
         }
       }
     }
   });
 
   return {
     ActiveRoomStoreMixin: ActiveRoomStoreMixin,
+    FailureInfoView: FailureInfoView,
     RoomFailureView: RoomFailureView,
     SocialShareDropdown: SocialShareDropdown,
     DesktopRoomEditContextView: DesktopRoomEditContextView,
     DesktopRoomConversationView: DesktopRoomConversationView,
     DesktopRoomInvitationView: DesktopRoomInvitationView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -71,16 +71,54 @@ loop.roomViews = (function(mozL10n) {
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState,
         savingContext: false
       }, storeState);
     }
   };
 
   /**
+   * Used to display errors in direct calls and rooms to the user.
+   */
+  var FailureInfoView = React.createClass({
+    propTypes: {
+      failureReason: React.PropTypes.string.isRequired
+    },
+
+    /**
+     * Returns the translated message appropraite to the failure reason.
+     *
+     * @return {String} The translated message for the failure reason.
+     */
+    _getMessage: function() {
+      switch (this.props.failureReason) {
+        case FAILURE_DETAILS.NO_MEDIA:
+        case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
+          return mozL10n.get("no_media_failure_message");
+        case FAILURE_DETAILS.TOS_FAILURE:
+          return mozL10n.get("tos_failure_message",
+            { clientShortname: mozL10n.get("clientShortname2") });
+        case FAILURE_DETAILS.ICE_FAILED:
+          return mozL10n.get("ice_failure_message");
+        default:
+          return mozL10n.get("generic_failure_message");
+      }
+    },
+
+    render: function() {
+      return (
+        <div className="failure-info">
+          <div className="failure-info-logo" />
+          <h2 className="failure-info-message">{this._getMessage()}</h2>
+        </div>
+      );
+    }
+  });
+
+  /**
    * Something went wrong view. Displayed when there's a big problem.
    */
   var RoomFailureView = React.createClass({
     mixins: [ sharedMixins.AudioMixin ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       failureReason: React.PropTypes.string,
@@ -105,18 +143,17 @@ loop.roomViews = (function(mozL10n) {
       if (this.props.failureReason === FAILURE_DETAILS.ICE_FAILED) {
         btnTitle = mozL10n.get("retry_call_button");
       } else {
         btnTitle = mozL10n.get("rejoin_button");
       }
 
       return (
         <div className="room-failure">
-          <loop.conversationViews.FailureInfoView
-            failureReason={this.props.failureReason} />
+          <FailureInfoView failureReason={this.props.failureReason} />
           <div className="btn-group call-action-group">
             <button className="btn btn-info btn-rejoin"
                     onClick={this.handleRejoinCall}>
               {btnTitle}
             </button>
           </div>
           <loop.shared.views.SettingsControlButton
             menuBelow={true}
@@ -815,16 +852,17 @@ loop.roomViews = (function(mozL10n) {
           );
         }
       }
     }
   });
 
   return {
     ActiveRoomStoreMixin: ActiveRoomStoreMixin,
+    FailureInfoView: FailureInfoView,
     RoomFailureView: RoomFailureView,
     SocialShareDropdown: SocialShareDropdown,
     DesktopRoomEditContextView: DesktopRoomEditContextView,
     DesktopRoomConversationView: DesktopRoomConversationView,
     DesktopRoomInvitationView: DesktopRoomInvitationView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/shared/css/common.css
+++ b/browser/components/loop/content/shared/css/common.css
@@ -192,135 +192,67 @@ p {
   }
 
   .btn-info:active {
     background-color: #5cccee;
     border: 1px solid #5cccee;
   }
 
 .btn-accept,
-.btn-success,
-.btn-accept + .btn-chevron {
+.btn-success {
   background-color: #56b397;
   border: 1px solid #56b397;
 }
 
   .btn-accept:hover,
-  .btn-success:hover,
-  .btn-accept + .btn-chevron:hover {
+  .btn-success:hover {
     background-color: #50e3c2;
     border: 1px solid #50e3c2;
   }
 
   .btn-accept:active,
-  .btn-success:active,
-  .btn-accept + .btn-chevron:active {
+  .btn-success:active {
     background-color: #3aa689;
     border: 1px solid #3aa689;
   }
 
 .btn-warning {
   background-color: #f0ad4e;
 }
 
-.btn-cancel,
 .btn-error,
-.btn-decline,
-.btn-hangup,
-.btn-decline + .btn-chevron,
-.btn-error + .btn-chevron {
+.btn-hangup {
   background-color: #d74345;
   border: 1px solid #d74345;
 }
 
-  .btn-cancel:hover,
   .btn-error:hover,
-  .btn-decline:hover,
-  .btn-hangup:hover,
-  .btn-decline + .btn-chevron:hover,
-  .btn-error + .btn-chevron:hover {
+  .btn-hangup:hover {
     background-color: #c53436;
     border-color: #c53436;
   }
 
-  .btn-cancel:active,
   .btn-error:active,
-  .btn-decline:active,
-  .btn-hangup:active,
-  .btn-decline + .btn-chevron:active,
-  .btn-error + .btn-chevron:active {
+  .btn-hangup:active {
     background-color: #ae2325;
     border-color: #ae2325;
   }
 
-.btn-chevron {
-  width: 26px;
-  height: 26px;
-  border-top-right-radius: 2px;
-  border-bottom-right-radius: 2px;
-}
-
-/* Groups together a button and a chevron */
-.btn-group-chevron {
-  display: flex;
-  flex-direction: column;
-  flex: 1;
-}
-
-/* Groups together a button-group-chevron
- * and the dropdown menu */
-.btn-chevron-menu-group {
-  display: flex;
-  justify-content: space-between;
-  flex: 8;
-}
-
-.btn-group-chevron .btn {
-  border-radius: 2px;
-  border-top-right-radius: 0;
-  border-bottom-right-radius: 0;
-  flex: 2;
-}
-
-  .btn + .btn-chevron,
-  .btn + .btn-chevron:hover,
-  .btn + .btn-chevron:active {
-    border-left: 1px solid rgba(255,255,255,.4);
-    background-image: url("../img/dropdown-inverse.png");
-    background-repeat: no-repeat;
-    background-position: center;
-    background-size: 10px;
-  }
-
-@media (min-resolution: 2dppx) {
-  .btn-chevron {
-    background-image: url(../img/dropdown-inverse@2x.png);
-  }
-}
-
 .disabled, button[disabled] {
   cursor: not-allowed;
   pointer-events: none;
   opacity: 0.65;
 }
 
 .btn-group {
   display: flex;
   align-content: space-between;
   justify-content: center;
 }
 
-.btn-chevron-menu-group .btn {
-  flex: 1;
-  border-radius: 2px;
-  border-bottom-right-radius: 0;
-  border-top-right-radius: 0;
-  line-height: 0.9rem;
-}
-
 /* Alerts/Notifications */
 .notificationContainer {
   border-bottom: 2px solid #E9E9E9;
 }
 
 .messages > .notificationContainer > .alert {
   text-align: center;
 }
@@ -366,19 +298,17 @@ p {
   position: relative;
   top: -.1rem;
   right: -1rem;
 }
 
 /* Misc */
 
 .call-url,
-.overflow-text-ellipsis,
-.standalone-call-btn-text,
-.fx-embedded-answer-btn-text {
+.standalone-call-btn-text {
   display: inline-block;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
 
 
 .close {
@@ -404,19 +334,17 @@ p {
 
 /* Transitions */
 .fade-out {
   transition: opacity 0.5s ease-in;
   opacity: 0;
 }
 
 .icon,
-.icon-small,
-.icon-audio,
-.icon-video {
+.icon-small {
   background-size: 20px;
   background-repeat: no-repeat;
   vertical-align: top;
   background-position: 80% center;
 }
 
 .pseudo-icon:before {
   content: "";
@@ -427,34 +355,16 @@ p {
   vertical-align: top;
   margin: 0 .7rem;
 }
 
 .icon-small {
   background-size: 10px;
 }
 
-.icon-video {
-  background-image: url("../img/video-inverse-14x14.png");
-}
-
-.icon-audio {
-  background-image: url("../img/audio-default-16x16@1.5x.png");
-}
-
-@media (min-resolution: 2dppx) {
-  .icon-video {
-    background-image: url("../img/video-inverse-14x14@2x.png");
-  }
-
-  .icon-audio {
-    background-image: url("../img/audio-default-16x16@2x.png");
-  }
-}
-
 /*
  * Platform specific styles
  * The UI should match the user OS
  * Specific font sizes for text paragraphs to match
  * the interface on that platform.
  */
 
 .inverse {
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -98,67 +98,18 @@ html[dir="rtl"] .conversation-toolbar-me
 .conversation-toolbar-btn-box.btn-edit-entry {
   float: right;
 }
 
 html[dir="rtl"] .conversation-toolbar-btn-box.btn-edit-entry {
   float: left;
 }
 
-.fx-embedded-answer-btn-text {
-  /* always leave space for the icon (width and margin) */
-  max-width: calc(100% - 24px - .2rem);
-  height: 22px;
-  line-height: 22px;
-}
-
-.fx-embedded-btn-icon-video,
-.fx-embedded-btn-icon-audio {
-  width: 24px;
-  height: 24px;
-  display: inline-block;
-  background-repeat: no-repeat;
-  background-position: center;
-  background-size: 14px 14px;
-  border-radius: 50%;
-  -moz-margin-start: .2rem;
-}
-
 /* conversationViews.jsx */
 
-.fx-embedded-btn-icon-video,
-.fx-embedded-btn-video-small,
-.fx-embedded-tiny-video-icon {
-  background-image: url("../img/video-inverse-14x14.png");
-}
-
-.fx-embedded-btn-icon-audio,
-.fx-embedded-btn-audio-small,
-.fx-embedded-tiny-audio-icon {
-  background-image: url("../img/audio-inverse-14x14.png");
-}
-
-.fx-embedded-btn-audio-small,
-.fx-embedded-btn-video-small {
-  width: 26px;
-  height: 26px;
-  border-left: 1px solid rgba(255,255,255,.4);
-  background-color: #56b397;
-  border-top-right-radius: 2px;
-  border-bottom-right-radius: 2px;
-  background-position: center;
-  background-repeat: no-repeat;
-  cursor: pointer;
-}
-
-  .fx-embedded-btn-video-small:hover,
-  .fx-embedded-btn-audio-small:hover {
-    background-color: #50e3c2;
-  }
-
 .conversation-toolbar .btn-hangup {
   background-image: url("../img/svg/exit.svg");
   border: 0;
 }
 
 /* Audio mute button */
 .btn-mute-audio:after {
   content: url("../img/svg/audio.svg");
@@ -224,33 +175,16 @@ html[dir="rtl"] .conversation-toolbar-bt
 
 .btn-screen-share.disabled {
   /* The screen share button is in its pending state when its disabled. */
   background-image: url("../img/svg/sharing-pending.svg");
 }
 
 /* General Call (incoming or outgoing). */
 
-/* XXX call-window currently relates to multiple things like the AcceptCallView,
-   DirectCallFailureView, PendingConversationView. It doesn't relate to the direct
-   call media views. We should probably make this more explicit at some stage. */
-
-/*
- * Height matches the height of the docked window
- * but the UI breaks when you pop out
- * Bug 1040985
- */
-.call-window {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: space-between;
-  min-height: 230px;
-}
-
 .call-action-group {
   display: flex;
   padding: 0 4px;
   width: 100%;
 }
 
 .call-action-group > .btn,
 .room-context > .btn {
@@ -305,47 +239,42 @@ html[dir="rtl"] .conversation-toolbar-bt
   right: -10rem;
 }
 
 .call-action-group > .invite-button.triggered > p,
 .call-action-group > .invite-button:hover > p {
   display: block;
 }
 
-.direct-call-failure,
 .room-failure {
   /* This flex allows us to not calculate the height of the logo area
      versus the buttons */
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: space-between;
   min-height: 230px;
   height: 100%;
 }
 
-.direct-call-failure > .call-action-group,
 .room-failure > .call-action-group {
   flex: none;
   margin: 1rem 0 2rem;
 }
 
-.direct-call-failure > .failure-info,
 .room-failure > .failure-info {
   flex: auto;
 }
 
-.direct-call-failure > .settings-control,
 .room-failure > .settings-control {
   position: absolute;
   top: 1rem;
   right: .5rem;
 }
 
-html[dir="rtl"] .direct-call-failure > .settings-control,
 html[dir="rtl"] .room-failure > .settings-control {
   left: .5rem;
   right: auto;
 }
 
 .failure-info {
   text-align: center;
   /* This flex is designed to set the logo in a standard place, but if the
@@ -385,61 +314,16 @@ html[dir="rtl"] .room-failure > .setting
   margin: 0.25rem 0;
   flex: none;
 }
 
 .failure-info-extra-failure {
   color: #f00;
 }
 
-.fx-embedded-call-button-spacer {
-  display: flex;
-  flex: 1;
-}
-
-/*
- * Dropdown menu hidden behind a chevron
- *
- * .native-dropdown-menu Generic class, contains common styles
- * .conversation-window-dropdown Dropdown menu for answer/decline/block options
- */
-
-.native-dropdown-menu {
-  /* Should match a native select menu */
-  padding: 0;
-  position: absolute; /* element can be wider than the parent */
-  background: #fff;
-  margin: 0;
-  box-shadow: 0 4px 5px rgba(30,30,30,.3);
-  border-style: solid;
-  border-width: 1px 1px 1px 2px;
-  border-color: #aaa #111 #111 #aaa;
-}
-
-  .native-dropdown-menu li {
-    list-style: none;
-    cursor: pointer;
-    color: #000;
-  }
-
-  .native-dropdown-menu li:hover {
-    color: #fff;
-    background-color: #111;
-  }
-
-.conversation-window-dropdown {
-  bottom: 4rem;
-}
-
-.conversation-window-dropdown > li {
-  padding: .2rem;
-  font-size: 1rem;
-  white-space: nowrap;
-}
-
 .screen-share-menu.dropdown-menu,
 .settings-menu.dropdown-menu {
   bottom: 3.1rem;
 }
 
 .settings-menu.dropdown-menu {
   left: auto;
   /*offset dropdown menu to be above menu button*/
@@ -562,89 +446,21 @@ html[dir="rtl"] .settings-menu.dropdown-
   }
 }
 
 .remote > .avatar {
   /* make visually distinct from local avatar */
   opacity: 0.25;
 }
 
-.fx-embedded-call-identifier {
-  display: inline;
-  width: 100%;
-  padding: 1.2em;
-}
-
-.fx-embedded-call-identifier-item {
-  height: 50px;
-}
-
-.fx-embedded-call-identifier-avatar {
-  max-width: 50px;
-  min-width: 50px;
-  background: #ccc;
-  border-radius: 50%;
-  background-image: url("../img/audio-call-avatar.svg");
-  background-repeat: no-repeat;
-  background-color: #4ba6e7;
-  background-size: contain;
-  overflow: hidden;
-  box-shadow: inset 0 0 0 1px rgba(255,255,255,.3);
-  float: left;
-  -moz-margin-end: 1em;
-}
-
-.fx-embedded-call-identifier-text {
-  font-weight: bold;
-}
-
-.fx-embedded-call-identifier-info {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  -moz-margin-start: 1em;
-}
-
-.fx-embedded-conversation-timestamp {
-  font-size: .6rem;
-  line-height: 17px;
-  display: inline-block;
-  vertical-align: top;
-}
-
-.fx-embedded-call-detail {
-  padding-top: 0.6em;
-}
-
-.fx-embedded-tiny-video-icon {
-  margin: 0 .8em;
-}
-
-.fx-embedded-tiny-audio-icon,
-.fx-embedded-tiny-video-icon {
-  width: 24px;
-  height: 24px;
-  background-color: #00a9dc;
-  display: inline-block;
-  background-repeat: no-repeat;
-  background-position: center;
-  border-radius: 50%;
-}
-
-  .fx-embedded-tiny-video-icon.muted {
-    background-color: rgba(0,0,0,.2)
-  }
-
 /* Force full height on all parents up to the video elements
  * this way we can ensure the aspect ratio and use height 100%
  * on the video element
  * */
 html, .fx-embedded, #main,
-.desktop-call-wrapper,
 .desktop-room-wrapper {
   height: 100%;
 }
 
 /**
  * Rooms
  */
 
@@ -938,17 +754,16 @@ body[platform="win"] .share-service-drop
 }
 
 .media-wrapper.showing-local-streams.receiving-screen-share > .text-chat-view {
   /* When we're displaying the local streams, then we need to make the text
      chat view a bit shorter to give room. */
   height: calc(100% - 300px);
 }
 
-.desktop-call-wrapper > .media-layout > .media-wrapper > .text-chat-view,
 .desktop-room-wrapper > .media-layout > .media-wrapper > .text-chat-view {
   height: calc(100% - 150px);
 }
 
 /* Temporarily slaved from .media-wrapper until we use it in more places
    to avoid affecting the conversation window on desktop. */
 .media-wrapper > .text-chat-view > .text-chat-entries {
   /* 40px is the height of .text-chat-box. */
@@ -1065,17 +880,16 @@ body[platform="win"] .share-service-drop
   }
 
   .media-wrapper.receiving-screen-share > .remote > .conversation-toolbar,
   .media-wrapper.showing-local-streams.receiving-screen-share  > .remote > .conversation-toolbar {
     bottom: calc(30% + 1.5rem);
   }
 
 
-  .desktop-call-wrapper > .media-layout > .media-wrapper > .text-chat-view,
   .desktop-room-wrapper > .media-layout > .media-wrapper > .text-chat-view {
     /* This is temp, to echo the .media-wrapper > .text-chat-view above */
     height: 30%;
   }
 
   .media-wrapper.receiving-screen-share > .screen {
     order: 1;
   }
deleted file mode 100644
index 1884088ee5d7a5e221fb8611de0061c874225b6f..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 8443c4b67afda8196c3921ad0e69005aa3505dea..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index b30364466d9cb7dfd0a65c4b47656d08224f742b..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 63126f18ccadefd19278f625188738ee3ccbf2d2..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 2fbd6d10a0634ab6714088979122ce28906aea1c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index e596bdc4eda93a68224611505473b7c7808289c6..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ /dev/null
@@ -1,676 +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 || {};
-
-(function() {
-  "use strict";
-
-  var sharedActions = loop.shared.actions;
-  var CALL_TYPES = loop.shared.utils.CALL_TYPES;
-  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
-  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
-  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
-
-  /**
-   * Websocket states taken from:
-   * https://docs.services.mozilla.com/loop/apis.html#call-progress-state-change-progress
-   */
-  var WS_STATES = loop.store.WS_STATES = {
-    // The call is starting, and the remote party is not yet being alerted.
-    INIT: "init",
-    // The called party is being alerted.
-    ALERTING: "alerting",
-    // The call is no longer being set up and has been aborted for some reason.
-    TERMINATED: "terminated",
-    // The called party has indicated that he has answered the call,
-    // but the media is not yet confirmed.
-    CONNECTING: "connecting",
-    // One of the two parties has indicated successful media set up,
-    // but the other has not yet.
-    HALF_CONNECTED: "half-connected",
-    // Both endpoints have reported successfully establishing media.
-    CONNECTED: "connected"
-  };
-
-  var CALL_STATES = loop.store.CALL_STATES = {
-    // The initial state of the view.
-    INIT: "cs-init",
-    // The store is gathering the call data from the server.
-    GATHER: "cs-gather",
-    // The initial data has been gathered, the websocket is connecting, or has
-    // connected, and waiting for the other side to connect to the server.
-    CONNECTING: "cs-connecting",
-    // The websocket has received information that we're now alerting
-    // the peer.
-    ALERTING: "cs-alerting",
-    // The call is ongoing.
-    ONGOING: "cs-ongoing",
-    // The call ended successfully.
-    FINISHED: "cs-finished",
-    // The user has finished with the window.
-    CLOSE: "cs-close",
-    // The call was terminated due to an issue during connection.
-    TERMINATED: "cs-terminated"
-  };
-
-  /**
-   * Conversation store.
-   *
-   * @param {loop.Dispatcher} dispatcher  The dispatcher for dispatching actions
-   *                                      and registering to consume actions.
-   * @param {Object} options Options object:
-   * - {client}           client           The client object.
-   * - {mozLoop}          mozLoop          The MozLoop API object.
-   * - {loop.OTSdkDriver} loop.OTSdkDriver The SDK Driver
-   */
-  loop.store.ConversationStore = loop.store.createStore({
-    // Further actions are registered in setupWindowData when
-    // we know what window type this is.
-    actions: [
-      "setupWindowData"
-    ],
-
-    getInitialStoreState: function() {
-      return {
-        // The id of the window. Currently used for getting the window id.
-        windowId: undefined,
-        // The current state of the call
-        callState: CALL_STATES.INIT,
-        // The reason if a call was terminated
-        callStateReason: undefined,
-        // True if the call is outgoing, false if not, undefined if unknown
-        outgoing: undefined,
-        // The contact being called for outgoing calls
-        contact: undefined,
-        // The call type for the call.
-        // XXX Don't hard-code, this comes from the data in bug 1072323
-        callType: CALL_TYPES.AUDIO_VIDEO,
-        // A link for emailing once obtained from the server
-        emailLink: undefined,
-
-        // Call Connection information
-        // The call id from the loop-server
-        callId: undefined,
-        // The caller id of the contacting side
-        callerId: undefined,
-        // True if media has been connected both-ways.
-        mediaConnected: false,
-        // The connection progress url to connect the websocket
-        progressURL: undefined,
-        // The websocket token that allows connection to the progress url
-        websocketToken: undefined,
-        // SDK API key
-        apiKey: undefined,
-        // SDK session ID
-        sessionId: undefined,
-        // SDK session token
-        sessionToken: undefined,
-        // If the local audio is muted
-        audioMuted: false,
-        // If the local video is muted
-        videoMuted: false,
-        remoteVideoEnabled: false
-      };
-    },
-
-    /**
-     * Handles initialisation of the store.
-     *
-     * @param  {Object} options    Options object.
-     */
-    initialize: function(options) {
-      options = options || {};
-
-      if (!options.client) {
-        throw new Error("Missing option client");
-      }
-      if (!options.sdkDriver) {
-        throw new Error("Missing option sdkDriver");
-      }
-      if (!options.mozLoop) {
-        throw new Error("Missing option mozLoop");
-      }
-
-      this.client = options.client;
-      this.sdkDriver = options.sdkDriver;
-      this.mozLoop = options.mozLoop;
-      this._isDesktop = options.isDesktop || false;
-    },
-
-    /**
-     * Handles the connection failure action, setting the state to
-     * terminated.
-     *
-     * @param {sharedActions.ConnectionFailure} actionData The action data.
-     */
-    connectionFailure: function(actionData) {
-      this._endSession();
-      this.setStoreState({
-        callState: CALL_STATES.TERMINATED,
-        callStateReason: actionData.reason
-      });
-    },
-
-    /**
-     * Handles the connection progress action, setting the next state
-     * appropriately.
-     *
-     * @param {sharedActions.ConnectionProgress} actionData The action data.
-     */
-    connectionProgress: function(actionData) {
-      var state = this.getStoreState();
-
-      switch(actionData.wsState) {
-        case WS_STATES.INIT: {
-          if (state.callState === CALL_STATES.GATHER) {
-            this.setStoreState({callState: CALL_STATES.CONNECTING});
-          }
-          break;
-        }
-        case WS_STATES.ALERTING: {
-          this.setStoreState({callState: CALL_STATES.ALERTING});
-          break;
-        }
-        case WS_STATES.CONNECTING: {
-          if (state.outgoing) {
-            // We can start the connection right away if we're the outgoing
-            // call because this is the only way we know the other peer has
-            // accepted.
-            this._startCallConnection();
-          } else if (state.callState !== CALL_STATES.ONGOING) {
-            console.error("Websocket unexpectedly changed to next state whilst waiting for call acceptance.");
-            // Abort and close the window.
-            this.declineCall(new sharedActions.DeclineCall({blockCaller: false}));
-          }
-          break;
-        }
-        case WS_STATES.HALF_CONNECTED:
-        case WS_STATES.CONNECTED: {
-          if (this.getStoreState("callState") !== CALL_STATES.ONGOING) {
-            console.error("Unexpected websocket state received in wrong callState");
-            this.setStoreState({callState: CALL_STATES.ONGOING});
-          }
-          break;
-        }
-        default: {
-          console.error("Unexpected websocket state passed to connectionProgress:",
-            actionData.wsState);
-        }
-      }
-    },
-
-    setupWindowData: function(actionData) {
-      var windowType = actionData.type;
-      if (windowType !== "outgoing" &&
-          windowType !== "incoming") {
-        // Not for this store, don't do anything.
-        return;
-      }
-
-      this.dispatcher.register(this, [
-        "connectionFailure",
-        "connectionProgress",
-        "acceptCall",
-        "declineCall",
-        "connectCall",
-        "hangupCall",
-        "remotePeerDisconnected",
-        "cancelCall",
-        "retryCall",
-        "mediaConnected",
-        "setMute",
-        "fetchRoomEmailLink",
-        "mediaStreamCreated",
-        "mediaStreamDestroyed",
-        "remoteVideoStatus",
-        "windowUnload"
-      ]);
-
-      this.setStoreState({
-        apiKey: actionData.apiKey,
-        callerId: actionData.callerId,
-        callId: actionData.callId,
-        callState: CALL_STATES.GATHER,
-        callType: actionData.callType,
-        contact: actionData.contact,
-        outgoing: windowType === "outgoing",
-        progressURL: actionData.progressURL,
-        sessionId: actionData.sessionId,
-        sessionToken: actionData.sessionToken,
-        videoMuted: actionData.callType === CALL_TYPES.AUDIO_ONLY,
-        websocketToken: actionData.websocketToken,
-        windowId: actionData.windowId
-      });
-
-      if (this.getStoreState("outgoing")) {
-        this._setupOutgoingCall();
-      } else {
-        this._setupIncomingCall();
-      }
-    },
-
-    /**
-     * Handles starting a call connection - connecting the session on the
-     * sdk, and setting the state appropriately.
-     */
-    _startCallConnection: function() {
-      var state = this.getStoreState();
-
-      this.sdkDriver.connectSession({
-        apiKey: state.apiKey,
-        sessionId: state.sessionId,
-        sessionToken: state.sessionToken,
-        sendTwoWayMediaTelemetry: state.outgoing // only one side of the call
-      });
-      this.mozLoop.addConversationContext(
-        state.windowId,
-        state.sessionId,
-        state.callId);
-      this.setStoreState({callState: CALL_STATES.ONGOING});
-    },
-
-    /**
-     * Accepts an incoming call.
-     *
-     * @param {sharedActions.AcceptCall} actionData
-     */
-    acceptCall: function(actionData) {
-      if (this.getStoreState("outgoing")) {
-        console.error("Received AcceptCall action in outgoing call state");
-        return;
-      }
-
-      this.setStoreState({
-        callType: actionData.callType,
-        videoMuted: actionData.callType === CALL_TYPES.AUDIO_ONLY
-      });
-
-      this._websocket.accept();
-
-      this._startCallConnection();
-    },
-
-    /**
-     * Declines an incoming call.
-     *
-     * @param {sharedActions.DeclineCall} actionData
-     */
-    declineCall: function(actionData) {
-      if (actionData.blockCaller) {
-        this.mozLoop.calls.blockDirectCaller(this.getStoreState("callerId"),
-          function(err) {
-            // XXX The conversation window will be closed when this cb is triggered
-            // figure out if there is a better way to report the error to the user
-            // (bug 1103150).
-            console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
-          });
-      }
-
-      this._websocket.decline();
-
-      // Now we've declined, end the session and close the window.
-      this._endSession();
-      this.setStoreState({callState: CALL_STATES.CLOSE});
-    },
-
-    /**
-     * Handles the connect call action, this saves the appropriate
-     * data and starts the connection for the websocket to notify the
-     * server of progress.
-     *
-     * @param {sharedActions.ConnectCall} actionData The action data.
-     */
-    connectCall: function(actionData) {
-      this.setStoreState(actionData.sessionData);
-      this._connectWebSocket();
-    },
-
-    /**
-     * Hangs up an ongoing call.
-     */
-    hangupCall: function() {
-      if (this._websocket) {
-        // Let the server know the user has hung up.
-        this._websocket.mediaFail();
-      }
-
-      this._endSession();
-      this.setStoreState({
-        callState: this._storeState.callState === CALL_STATES.ONGOING ?
-                   CALL_STATES.FINISHED : CALL_STATES.CLOSE
-      });
-    },
-
-    /**
-     * The remote peer disconnected from the session.
-     *
-     * @param {sharedActions.RemotePeerDisconnected} actionData
-     */
-    remotePeerDisconnected: function(actionData) {
-      this._endSession();
-
-      // If the peer hungup, we end normally, otherwise
-      // we treat this as a call failure.
-      if (actionData.peerHungup) {
-        this.setStoreState({callState: CALL_STATES.FINISHED});
-      } else {
-        this.setStoreState({
-          callState: CALL_STATES.TERMINATED,
-          callStateReason: "peerNetworkDisconnected"
-        });
-      }
-    },
-
-    /**
-     * Cancels a call. This can happen for incoming or outgoing calls.
-     * Although the user doesn't "cancel" an incoming call, it may be that
-     * the remote peer cancelled theirs before the incoming call was accepted.
-     */
-    cancelCall: function() {
-      if (this.getStoreState("outgoing")) {
-        var callState = this.getStoreState("callState");
-        if (this._websocket &&
-            (callState === CALL_STATES.CONNECTING ||
-             callState === CALL_STATES.ALERTING)) {
-          // Let the server know the user has hung up.
-          this._websocket.cancel();
-        }
-      }
-
-      this._endSession();
-      this.setStoreState({callState: CALL_STATES.CLOSE});
-    },
-
-    /**
-     * Retries a call
-     */
-    retryCall: function() {
-      var callState = this.getStoreState("callState");
-      if (callState !== CALL_STATES.TERMINATED) {
-        console.error("Unexpected retry in state", callState);
-        return;
-      }
-
-      this.setStoreState({callState: CALL_STATES.GATHER});
-      if (this.getStoreState("outgoing")) {
-        this._setupOutgoingCall();
-      }
-    },
-
-    /**
-     * Notifies that all media is now connected
-     */
-    mediaConnected: function() {
-      this._websocket.mediaUp();
-      this.setStoreState({mediaConnected: true});
-    },
-
-    /**
-     * Records the mute state for the stream.
-     *
-     * @param {sharedActions.setMute} actionData The mute state for the stream type.
-     */
-    setMute: function(actionData) {
-      var newState = {};
-      newState[actionData.type + "Muted"] = !actionData.enabled;
-      this.setStoreState(newState);
-    },
-
-    /**
-     * Fetches a new room URL intended to be sent over email when a contact
-     * can't be reached.
-     */
-    fetchRoomEmailLink: function(actionData) {
-      this.mozLoop.rooms.create({
-        decryptedContext: {
-          roomName: actionData.roomName
-        },
-        maxSize: loop.store.MAX_ROOM_CREATION_SIZE,
-        expiresIn: loop.store.DEFAULT_EXPIRES_IN
-      }, function(err, createdRoomData) {
-        var buckets = this.mozLoop.ROOM_CREATE;
-        if (err) {
-          this.trigger("error:emailLink");
-          this.mozLoop.telemetryAddValue("LOOP_ROOM_CREATE", buckets.CREATE_FAIL);
-          return;
-        }
-        this.setStoreState({"emailLink": createdRoomData.roomUrl});
-        this.mozLoop.telemetryAddValue("LOOP_ROOM_CREATE", buckets.CREATE_SUCCESS);
-      }.bind(this));
-    },
-
-    /**
-     * Handles a media stream being created. This may be a local or a remote stream.
-     *
-     * @param {sharedActions.MediaStreamCreated} actionData
-     */
-    mediaStreamCreated: function(actionData) {
-      if (actionData.isLocal) {
-        this.setStoreState({
-          localVideoEnabled: actionData.hasVideo,
-          localSrcMediaElement: actionData.srcMediaElement
-        });
-        return;
-      }
-
-      this.setStoreState({
-        remoteVideoEnabled: actionData.hasVideo,
-        remoteSrcMediaElement: actionData.srcMediaElement
-      });
-    },
-
-    /**
-     * Handles a media stream being destroyed. This may be a local or a remote stream.
-     *
-     * @param {sharedActions.MediaStreamDestroyed} actionData
-     */
-    mediaStreamDestroyed: function(actionData) {
-      if (actionData.isLocal) {
-        this.setStoreState({
-          localSrcMediaElement: null
-        });
-        return;
-      }
-
-      this.setStoreState({
-        remoteSrcMediaElement: null
-      });
-    },
-
-    /**
-     * Handles a remote stream having video enabled or disabled.
-     *
-     * @param {sharedActions.RemoteVideoStatus} actionData
-     */
-    remoteVideoStatus: function(actionData) {
-      this.setStoreState({
-        remoteVideoEnabled: actionData.videoEnabled
-      });
-    },
-
-    /**
-     * Called when the window is unloaded, either by code, or by the user
-     * explicitly closing it.  Expected to do any necessary housekeeping, such
-     * as shutting down the call cleanly and adding any relevant telemetry data.
-     */
-    windowUnload: function() {
-      if (!this.getStoreState("outgoing") &&
-          this.getStoreState("callState") === CALL_STATES.ALERTING &&
-          this._websocket) {
-        this._websocket.decline();
-      }
-
-      this._endSession();
-    },
-
-    /**
-     * Sets up an incoming call. All we really need to do here is
-     * to connect the websocket, as we've already got all the information
-     * when the window opened.
-     */
-    _setupIncomingCall: function() {
-      this._connectWebSocket();
-    },
-
-    /**
-     * Obtains the outgoing call data from the server and handles the
-     * result.
-     */
-    _setupOutgoingCall: function() {
-      var contactAddresses = [];
-      var contact = this.getStoreState("contact");
-
-      this.mozLoop.calls.setCallInProgress(this.getStoreState("windowId"));
-
-      function appendContactValues(property, strip) {
-        if (contact.hasOwnProperty(property)) {
-          contact[property].forEach(function(item) {
-            if (strip) {
-              contactAddresses.push(item.value
-                .replace(/^(\+)?(.*)$/g, function(m, prefix, number) {
-                  return (prefix || "") + number.replace(/[\D]+/g, "");
-                }));
-            } else {
-              contactAddresses.push(item.value);
-            }
-          });
-        }
-      }
-
-      appendContactValues("email");
-      appendContactValues("tel", true);
-
-      this.client.setupOutgoingCall(contactAddresses,
-        this.getStoreState("callType"),
-        function(err, result) {
-          if (err) {
-            console.error("Failed to get outgoing call data", err);
-            var failureReason = FAILURE_DETAILS.UNKNOWN;
-            if (err.errno === REST_ERRNOS.USER_UNAVAILABLE) {
-              failureReason = FAILURE_DETAILS.USER_UNAVAILABLE;
-            }
-            this.dispatcher.dispatch(
-              new sharedActions.ConnectionFailure({reason: failureReason}));
-            return;
-          }
-
-          // Success, dispatch a new action.
-          this.dispatcher.dispatch(
-            new sharedActions.ConnectCall({sessionData: result}));
-        }.bind(this)
-      );
-    },
-
-    /**
-     * Sets up and connects the websocket to the server. The websocket
-     * deals with sending and obtaining status via the server about the
-     * setup of the call.
-     */
-    _connectWebSocket: function() {
-      this._websocket = new loop.CallConnectionWebSocket({
-        url: this.getStoreState("progressURL"),
-        callId: this.getStoreState("callId"),
-        websocketToken: this.getStoreState("websocketToken")
-      });
-
-      this._websocket.promiseConnect().then(
-        function(progressState) {
-          this.dispatcher.dispatch(new sharedActions.ConnectionProgress({
-            // This is the websocket call state, i.e. waiting for the
-            // other end to connect to the server.
-            wsState: progressState
-          }));
-        }.bind(this),
-        function(error) {
-          console.error("Websocket failed to connect", error);
-          this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
-            reason: "websocket-setup"
-          }));
-        }.bind(this)
-      );
-
-      this.listenTo(this._websocket, "progress", this._handleWebSocketProgress);
-    },
-
-    /**
-     * Ensures the session is ended and the websocket is disconnected.
-     */
-    _endSession: function(nextState) {
-      this.sdkDriver.disconnectSession();
-      if (this._websocket) {
-        this.stopListening(this._websocket);
-
-        // Now close the websocket.
-        this._websocket.close();
-        delete this._websocket;
-      }
-
-      this.mozLoop.calls.clearCallInProgress(
-        this.getStoreState("windowId"));
-    },
-
-    /**
-     * If we hit any of the termination reasons, and the user hasn't accepted
-     * then it seems reasonable to close the window/abort the incoming call.
-     *
-     * If the user has accepted the call, and something's happened, display
-     * the call failed view.
-     *
-     * https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
-     *
-     * For outgoing calls, we treat all terminations as failures.
-     *
-     * @param {Object} progressData  The progress data received from the websocket.
-     * @param {String} previousState The previous state the websocket was in.
-     */
-    _handleWebSocketStateTerminated: function(progressData, previousState) {
-      if (this.getStoreState("outgoing") ||
-          (previousState !== WS_STATES.INIT &&
-           previousState !== WS_STATES.ALERTING)) {
-        // For outgoing calls we can treat everything as connection failure.
-
-        // XXX We currently fallback to the websocket reason, but really these should
-        // be fully migrated to use FAILURE_DETAILS, so as not to expose websocket
-        // states outside of this store. Bug 1124384 should help to fix this.
-        var reason = progressData.reason;
-
-        if (reason === WEBSOCKET_REASONS.REJECT ||
-            reason === WEBSOCKET_REASONS.BUSY) {
-          reason = FAILURE_DETAILS.USER_UNAVAILABLE;
-        }
-
-        this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
-          reason: reason
-        }));
-        return;
-      }
-
-      this.dispatcher.dispatch(new sharedActions.CancelCall());
-    },
-
-    /**
-     * Used to handle any progressed received from the websocket. This will
-     * dispatch new actions so that the data can be handled appropriately.
-     *
-     * @param {Object} progressData  The progress data received from the websocket.
-     * @param {String} previousState The previous state the websocket was in.
-     */
-    _handleWebSocketProgress: function(progressData, previousState) {
-      switch(progressData.state) {
-        case WS_STATES.TERMINATED: {
-          this._handleWebSocketStateTerminated(progressData, previousState);
-          break;
-        }
-        default: {
-          this.dispatcher.dispatch(new sharedActions.ConnectionProgress({
-            wsState: progressData.state
-          }));
-          break;
-        }
-      }
-    }
-  });
-})();
--- a/browser/components/loop/content/shared/js/utils.js
+++ b/browser/components/loop/content/shared/js/utils.js
@@ -57,26 +57,16 @@ var inChrome = typeof Components != "und
 
   var REST_ERRNOS = {
     INVALID_TOKEN: 105,
     EXPIRED: 111,
     USER_UNAVAILABLE: 122,
     ROOM_FULL: 202
   };
 
-  var WEBSOCKET_REASONS = {
-    ANSWERED_ELSEWHERE: "answered-elsewhere",
-    BUSY: "busy",
-    CANCEL: "cancel",
-    CLOSED: "closed",
-    MEDIA_FAIL: "media-fail",
-    REJECT: "reject",
-    TIMEOUT: "timeout"
-  };
-
   var FAILURE_DETAILS = {
     MEDIA_DENIED: "reason-media-denied",
     NO_MEDIA: "reason-no-media",
     ROOM_ALREADY_OPEN: "reason-room-already-open",
     UNABLE_TO_PUBLISH_MEDIA: "unable-to-publish-media",
     USER_UNAVAILABLE: "reason-user-unavailable",
     COULD_NOT_CONNECT: "reason-could-not-connect",
     NETWORK_DISCONNECTED: "reason-network-disconnected",
@@ -781,17 +771,16 @@ var inChrome = typeof Components != "und
     return node;
   }
 
   this.utils = {
     CALL_TYPES: CALL_TYPES,
     CHAT_CONTENT_TYPES: CHAT_CONTENT_TYPES,
     FAILURE_DETAILS: FAILURE_DETAILS,
     REST_ERRNOS: REST_ERRNOS,
-    WEBSOCKET_REASONS: WEBSOCKET_REASONS,
     STREAM_PROPERTIES: STREAM_PROPERTIES,
     SCREEN_SHARE_STATES: SCREEN_SHARE_STATES,
     ROOM_INFO_FAILURES: ROOM_INFO_FAILURES,
     setRootObjects: setRootObjects,
     composeCallUrlEmail: composeCallUrlEmail,
     findParentNode: findParentNode,
     formatDate: formatDate,
     formatURL: formatURL,
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -1113,17 +1113,16 @@ loop.shared.views = (function(_, mozL10n
               React.createElement(MediaView, {displayAvatar: !this.props.renderRemoteVideo, 
                 isLoading: this.props.isRemoteLoading, 
                 mediaType: "remote", 
                 posterUrl: this.props.remotePosterUrl, 
                 srcMediaElement: this.props.remoteSrcMediaElement}), 
                this.state.localMediaAboslutelyPositioned ?
                 this.renderLocalVideo() : null, 
                this.props.children
-
             ), 
             React.createElement("div", {className: screenShareStreamClasses}, 
               React.createElement(MediaView, {displayAvatar: false, 
                 isLoading: this.props.isScreenShareLoading, 
                 mediaType: "screen-share", 
                 posterUrl: this.props.screenSharePosterUrl, 
                 srcMediaElement: this.props.screenShareMediaElement})
             ), 
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -1113,17 +1113,16 @@ loop.shared.views = (function(_, mozL10n
               <MediaView displayAvatar={!this.props.renderRemoteVideo}
                 isLoading={this.props.isRemoteLoading}
                 mediaType="remote"
                 posterUrl={this.props.remotePosterUrl}
                 srcMediaElement={this.props.remoteSrcMediaElement} />
               { this.state.localMediaAboslutelyPositioned ?
                 this.renderLocalVideo() : null }
               { this.props.children }
-
             </div>
             <div className={screenShareStreamClasses}>
               <MediaView displayAvatar={false}
                 isLoading={this.props.isScreenShareLoading}
                 mediaType="screen-share"
                 posterUrl={this.props.screenSharePosterUrl}
                 srcMediaElement={this.props.screenShareMediaElement} />
             </div>
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/websocket.js
+++ /dev/null
@@ -1,299 +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.CallConnectionWebSocket = (function() {
-  "use strict";
-
-  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
-
-  // Response timeout is 5 seconds as per API.
-  var kResponseTimeout = 5000;
-
-  /**
-   * Handles a websocket specifically for a call connection.
-   *
-   * There should be one of these created for each call connection.
-   *
-   * options items:
-   * - url             The url of the websocket to connect to.
-   * - callId          The call id for the call
-   * - websocketToken  The authentication token for the websocket
-   *
-   * @param {Object} options The options for this websocket.
-   */
-  function CallConnectionWebSocket(options) {
-    this.options = options || {};
-
-    if (!this.options.url) {
-      throw new Error("No url in options");
-    }
-    if (!this.options.callId) {
-      throw new Error("No callId in options");
-    }
-    if (!this.options.websocketToken) {
-      throw new Error("No websocketToken in options");
-    }
-
-    this._lastServerState = "init";
-
-    // Set loop.debug.sdk to true in the browser, or standalone:
-    // localStorage.setItem("debug.websocket", true);
-    this._debugWebSocket =
-      loop.shared.utils.getBoolPreference("debug.websocket");
-
-    _.extend(this, Backbone.Events);
-  }
-
-  CallConnectionWebSocket.prototype = {
-    /**
-     * Start the connection to the websocket.
-     *
-     * @return {Promise} A promise that resolves when the websocket
-     *                   server connection is open and "hello"s have been
-     *                   exchanged. It is rejected if there is a failure in
-     *                   connection or the initial exchange of "hello"s.
-     */
-    promiseConnect: function() {
-      var promise = new Promise(
-        function(resolve, reject) {
-          this.socket = new WebSocket(this.options.url);
-          this.socket.onopen = this._onopen.bind(this);
-          this.socket.onmessage = this._onmessage.bind(this);
-          this.socket.onerror = this._onerror.bind(this);
-          this.socket.onclose = this._onclose.bind(this);
-
-          var timeout = setTimeout(function() {
-            if (this.connectDetails && this.connectDetails.reject) {
-              this.connectDetails.reject(WEBSOCKET_REASONS.TIMEOUT);
-              this._clearConnectionFlags();
-            }
-          }.bind(this), kResponseTimeout);
-          this.connectDetails = {
-            resolve: resolve,
-            reject: reject,
-            timeout: timeout
-          };
-        }.bind(this));
-
-      return promise;
-    },
-
-    /**
-     * Closes the websocket. This shouldn't be the normal action as the server
-     * will normally close the socket. Only in bad error cases, or where we need
-     * to close the socket just before closing the window (to avoid an error)
-     * should we call this.
-     */
-    close: function() {
-      if (this.socket) {
-        this.socket.close();
-      }
-    },
-
-    _clearConnectionFlags: function() {
-      clearTimeout(this.connectDetails.timeout);
-      delete this.connectDetails;
-    },
-
-    /**
-     * Internal function called to resolve the connection promise.
-     *
-     * It will log an error if no promise is found.
-     *
-     * @param {String} progressState The current state of progress of the
-     *                               websocket.
-     */
-    _completeConnection: function(progressState) {
-      if (this.connectDetails && this.connectDetails.resolve) {
-        this.connectDetails.resolve(progressState);
-        this._clearConnectionFlags();
-        return;
-      }
-
-      console.error("Failed to complete connection promise - no promise available");
-    },
-
-    /**
-     * Checks if the websocket is connecting, and rejects the connection
-     * promise if appropriate.
-     *
-     * @param {Object} event The event to reject the promise with if
-     *                       appropriate.
-     */
-    _checkConnectionFailed: function(event) {
-      if (this.connectDetails && this.connectDetails.reject) {
-        this.connectDetails.reject(event);
-        this._clearConnectionFlags();
-        return true;
-      }
-
-      return false;
-    },
-
-    /**
-     * Notifies the server that the user has declined the call.
-     */
-    decline: function() {
-      this._send({
-        messageType: "action",
-        event: "terminate",
-        reason: WEBSOCKET_REASONS.REJECT
-      });
-    },
-
-    /**
-     * Notifies the server that the user has accepted the call.
-     */
-    accept: function() {
-      this._send({
-        messageType: "action",
-        event: "accept"
-      });
-    },
-
-    /**
-     * Notifies the server that the outgoing media is up, and the
-     * incoming media is being received.
-     */
-    mediaUp: function() {
-      this._send({
-        messageType: "action",
-        event: "media-up"
-      });
-    },
-
-    /**
-     * Notifies the server that the outgoing call is cancelled by the
-     * user.
-     */
-    cancel: function() {
-      this._send({
-        messageType: "action",
-        event: "terminate",
-        reason: WEBSOCKET_REASONS.CANCEL
-      });
-    },
-
-    /**
-     * Notifies the server that something failed during setup.
-     */
-    mediaFail: function() {
-      this._send({
-        messageType: "action",
-        event: "terminate",
-        reason: WEBSOCKET_REASONS.MEDIA_FAIL
-      });
-    },
-
-    /**
-     * Sends data on the websocket.
-     *
-     * @param {Object} data The data to send.
-     */
-    _send: function(data) {
-      this._log("WS Sending", data);
-
-      this.socket.send(JSON.stringify(data));
-    },
-
-    /**
-     * Used to determine if the server state is in a completed state, i.e.
-     * the server has determined the connection is terminated or connected.
-     *
-     * @return True if the last received state is terminated or connected.
-     */
-    get _stateIsCompleted() {
-      return this._lastServerState === "terminated" ||
-             this._lastServerState === "connected";
-    },
-
-    /**
-     * Called when the socket is open. Automatically sends a "hello"
-     * message to the server.
-     */
-    _onopen: function() {
-      // Auto-register with the server.
-      this._send({
-        messageType: "hello",
-        callId: this.options.callId,
-        auth: this.options.websocketToken
-      });
-    },
-
-    /**
-     * Called when a message is received from the server.
-     *
-     * @param {Object} event The websocket onmessage event.
-     */
-    _onmessage: function(event) {
-      var msgData;
-      try {
-        msgData = JSON.parse(event.data);
-      } catch (x) {
-        console.error("Error parsing received message:", x);
-        return;
-      }
-
-      this._log("WS Receiving", event.data);
-
-      var previousState = this._lastServerState;
-      this._lastServerState = msgData.state;
-
-      switch(msgData.messageType) {
-        case "hello":
-          this._completeConnection(msgData.state);
-          break;
-        case "progress":
-          this.trigger("progress:" + msgData.state);
-          this.trigger("progress", msgData, previousState);
-          break;
-      }
-    },
-
-    /**
-     * Called when there is an error on the websocket.
-     *
-     * @param {Object} event A simple error event.
-     */
-    _onerror: function(event) {
-      this._log("WS Error", event);
-
-      if (!this._stateIsCompleted &&
-          !this._checkConnectionFailed(event)) {
-        this.trigger("error", event);
-      }
-    },
-
-    /**
-     * Called when the websocket is closed.
-     *
-     * @param {CloseEvent} event The details of the websocket closing.
-     */
-    _onclose: function(event) {
-      this._log("WS Close", event);
-
-      // If the websocket goes away when we're not in a completed state
-      // then its an error. So we either pass it back via the connection
-      // promise, or trigger the closed event.
-      if (!this._stateIsCompleted &&
-          !this._checkConnectionFailed(event)) {
-        this.trigger("closed", event);
-      }
-    },
-
-    /**
-     * Logs debug to the console.
-     *
-     * Parameters: same as console.log
-     */
-    _log: function() {
-      if (this._debugWebSocket) {
-        console.log.apply(console, arguments);
-      }
-    }
-  };
-
-  return CallConnectionWebSocket;
-})();
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -6,23 +6,21 @@ browser.jar:
   # Desktop html files
   content/browser/loop/conversation.html            (content/conversation.html)
   content/browser/loop/panel.html                   (content/panel.html)
 
   # Desktop libs (see bottom of this file for TokBox sdk assets)
   content/browser/loop/libs/l10n.js                 (content/libs/l10n.js)
 
   # Desktop script
-  content/browser/loop/js/client.js                 (content/js/client.js)
   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)
 
@@ -38,28 +36,22 @@ browser.jar:
   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/sad_hello_icon_64x64.svg      (content/shared/img/sad_hello_icon_64x64.svg)
   content/browser/loop/shared/img/chatbubble-arrow-left.svg     (content/shared/img/chatbubble-arrow-left.svg)
   content/browser/loop/shared/img/chatbubble-arrow-right.svg    (content/shared/img/chatbubble-arrow-right.svg)
-  content/browser/loop/shared/img/audio-inverse-14x14.png       (content/shared/img/audio-inverse-14x14.png)
-  content/browser/loop/shared/img/audio-inverse-14x14@2x.png    (content/shared/img/audio-inverse-14x14@2x.png)
   content/browser/loop/shared/img/facemute-14x14.png            (content/shared/img/facemute-14x14.png)
   content/browser/loop/shared/img/facemute-14x14@2x.png         (content/shared/img/facemute-14x14@2x.png)
   content/browser/loop/shared/img/hangup-inverse-14x14.png      (content/shared/img/hangup-inverse-14x14.png)
   content/browser/loop/shared/img/hangup-inverse-14x14@2x.png   (content/shared/img/hangup-inverse-14x14@2x.png)
   content/browser/loop/shared/img/mute-inverse-14x14.png        (content/shared/img/mute-inverse-14x14.png)
   content/browser/loop/shared/img/mute-inverse-14x14@2x.png     (content/shared/img/mute-inverse-14x14@2x.png)
-  content/browser/loop/shared/img/video-inverse-14x14.png       (content/shared/img/video-inverse-14x14.png)
-  content/browser/loop/shared/img/video-inverse-14x14@2x.png    (content/shared/img/video-inverse-14x14@2x.png)
-  content/browser/loop/shared/img/dropdown-inverse.png          (content/shared/img/dropdown-inverse.png)
-  content/browser/loop/shared/img/dropdown-inverse@2x.png       (content/shared/img/dropdown-inverse@2x.png)
   content/browser/loop/shared/img/svg/glyph-email-16x16.svg     (content/shared/img/svg/glyph-email-16x16.svg)
   content/browser/loop/shared/img/svg/glyph-facebook-16x16.svg  (content/shared/img/svg/glyph-facebook-16x16.svg)
   content/browser/loop/shared/img/svg/glyph-help-16x16.svg      (content/shared/img/svg/glyph-help-16x16.svg)
   content/browser/loop/shared/img/svg/glyph-link-16x16.svg      (content/shared/img/svg/glyph-link-16x16.svg)
   content/browser/loop/shared/img/svg/glyph-user-16x16.svg      (content/shared/img/svg/glyph-user-16x16.svg)
   content/browser/loop/shared/img/svg/exit.svg                  (content/shared/img/svg/exit.svg)
   content/browser/loop/shared/img/svg/audio.svg                 (content/shared/img/svg/audio.svg)
   content/browser/loop/shared/img/svg/audio-hover.svg           (content/shared/img/svg/audio-hover.svg)
@@ -98,31 +90,29 @@ browser.jar:
   content/browser/loop/shared/img/empty_conversations.svg       (content/shared/img/empty_conversations.svg)
   content/browser/loop/shared/img/empty_search.svg              (content/shared/img/empty_search.svg)
   content/browser/loop/shared/img/animated-spinner.svg          (content/shared/img/animated-spinner.svg)
   content/browser/loop/shared/img/avatars.svg                   (content/shared/img/avatars.svg)
   content/browser/loop/shared/img/firefox-avatar.svg            (content/shared/img/firefox-avatar.svg)
 
   # 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/activeRoomStore.js     (content/shared/js/activeRoomStore.js)
   content/browser/loop/shared/js/dispatcher.js          (content/shared/js/dispatcher.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/linkifiedTextView.js   (content/shared/js/linkifiedTextView.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/urlRegExps.js          (content/shared/js/urlRegExps.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
   content/browser/loop/shared/libs/react-0.12.2.js    (content/shared/libs/react-0.12.2.js)
 #else
   content/browser/loop/shared/libs/react-0.12.2.js    (content/shared/libs/react-0.12.2-prod.js)
 #endif
   content/browser/loop/shared/libs/lodash-3.9.3.js    (content/shared/libs/lodash-3.9.3.js)
--- a/browser/components/loop/run-all-loop-tests.sh
+++ b/browser/components/loop/run-all-loop-tests.sh
@@ -44,13 +44,21 @@ fi
 
 TESTS="
   ${LOOPDIR}/test/mochitest
   browser/components/uitour/test/browser_UITour_loop.js
   browser/base/content/test/general/browser_devices_get_user_media_about_urls.js
   browser/base/content/test/general/browser_parsable_css.js
 "
 
-./mach mochitest $TESTS
-
-if [ "$1" != "--skip-e10s" ]; then
-  ./mach mochitest --e10s $TESTS
-fi
+# Due to bug 1209463, we need to split these up and run them individually to
+# ensure we stop and report that there's an error.
+for test in $TESTS
+do
+  ./mach mochitest $test
+  # UITour & get user media aren't compatible with e10s currenly.
+  if [ "$1" != "--skip-e10s" ] && \
+     [ "$test" != "browser/components/uitour/test/browser_UITour_loop.js" ] && \
+     [ "$test" != "browser/base/content/test/general/browser_devices_get_user_media_about_urls.js" ];
+  then
+    ./mach mochitest --e10s $test
+  fi
+done
--- a/browser/components/loop/standalone/content/l10n/en-US/loop.properties
+++ b/browser/components/loop/standalone/content/l10n/en-US/loop.properties
@@ -1,12 +1,11 @@
 ## LOCALIZATION NOTE: In this file, don't translate the part between {{..}}
 conversation_has_ended=Your conversation has ended.
 generic_failure_message=We're having technical difficulties…
-generic_failure_with_reason2=You can try again or email a link to be reached at later.
 generic_failure_no_reason2=Would you like to try again?
 ## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname2}}
 ## as this will be replaced by the shortname.
 tos_failure_message={{clientShortname}} is not available in your country.
 
 retry_call_button=Retry
 unable_retrieve_call_info=Unable to retrieve conversation information.
 hangup_button_title=Hang up
@@ -24,17 +23,16 @@ incompatible_browser_message=Firefox Hel
 unsupported_platform_heading=Sorry!
 unsupported_platform_message={{platform}} does not currently support {{clientShortname}}
 unsupported_platform_ios=iOS
 unsupported_platform_windows_phone=Windows Phone
 unsupported_platform_blackberry=Blackberry
 unsupported_platform_learn_more_link=Learn more about why your platform doesn't support {{clientShortname}}
 promote_firefox_hello_heading=Download {{brandShortname}} to make free audio and video calls!
 get_firefox_button=Get {{brandShortname}}
-initiate_call_cancel_button=Cancel
 legal_text_and_links=By using {{clientShortname}} you agree to the {{terms_of_use_url}} and {{privacy_notice_url}}.
 terms_of_use_link_text=Terms of use
 privacy_notice_link_text=Privacy notice
 self_view_hidden_message=Self-view hidden but still being sent; resize window \
   to show
 
 ## LOCALIZATION NOTE(brandShortname): This should not be localized and
 ## should remain "Firefox" for all locales.
@@ -42,18 +40,16 @@ brandShortname=Firefox
 ## LOCALIZATION NOTE(clientShortname2): This should not be localized and
 ## should remain "Firefox Hello" for all locales.
 clientShortname2=Firefox Hello
 ## LOCALIZATION NOTE(vendorShortname): This should not be localized and
 ## should remain "Mozilla" for all locales.
 vendorShortname=Mozilla
 
 call_progress_getting_media_description={{clientShortname}} requires access to your camera and microphone.
-call_progress_connecting_description=Connecting…
-call_progress_ringing_description=Ringing…
 
 help_label=Help
 
 rooms_default_room_name_template=Conversation {{conversationLabel}}
 ## LOCALIZATION_NOTE(rooms_welcome_title): {{conversationName}} will be replaced
 ## by the user specified conversation name.
 rooms_welcome_title=Welcome to {{conversationName}}
 rooms_leave_button_label=Leave
--- a/browser/components/loop/standalone/content/webappEntryPoint.js
+++ b/browser/components/loop/standalone/content/webappEntryPoint.js
@@ -35,17 +35,16 @@ require("script!shared/libs/backbone-1.2
 require("script!shared/libs/react-0.12.2.js");
 
 require("script!shared/js/utils.js");
 require("script!shared/js/crypto.js");
 require("script!shared/js/mixins.js");
 require("script!shared/js/actions.js");
 require("script!shared/js/validate.js");
 require("script!shared/js/dispatcher.js");
-require("script!shared/js/websocket.js");
 require("script!shared/js/otSdkDriver.js");
 require("script!shared/js/store.js");
 require("script!shared/js/activeRoomStore.js");
 require("script!shared/js/views.js");
 require("script!shared/js/urlRegExps.js");
 require("script!shared/js/textChatStore.js");
 require("script!shared/js/textChatView.js");
 require("script!shared/js/linkifiedTextView.js");
deleted file mode 100644
--- a/browser/components/loop/test/desktop-local/client_test.js
+++ /dev/null
@@ -1,133 +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.Client", function() {
-  "use strict";
-
-  var expect = chai.expect;
-  var sandbox,
-      callback,
-      client,
-      mozLoop,
-      fakeToken,
-      hawkRequestStub;
-
-  var fakeErrorRes = {
-      code: 400,
-      errno: 400,
-      error: "Request Failed",
-      message: "invalid token"
-    };
-
-  beforeEach(function() {
-    sandbox = sinon.sandbox.create();
-    callback = sinon.spy();
-    fakeToken = "fakeTokenText";
-    mozLoop = {
-      getLoopPref: sandbox.stub()
-        .returns(null)
-        .withArgs("hawk-session-token")
-        .returns(fakeToken),
-      hawkRequest: sinon.stub(),
-      LOOP_SESSION_TYPE: {
-        GUEST: 1,
-        FXA: 2
-      },
-      userProfile: null,
-      telemetryAdd: sinon.spy()
-    };
-    // Alias for clearer tests.
-    hawkRequestStub = mozLoop.hawkRequest;
-    client = new loop.Client({
-      mozLoop: mozLoop
-    });
-  });
-
-  afterEach(function() {
-    sandbox.restore();
-  });
-
-  describe("loop.Client", function() {
-    describe("#setupOutgoingCall", function() {
-      var calleeIds, callType;
-
-      beforeEach(function() {
-        calleeIds = [
-          "fakeemail", "fake phone"
-        ];
-        callType = "audio";
-      });
-
-      it("should make a POST call to /calls", function() {
-        client.setupOutgoingCall(calleeIds, callType);
-
-        sinon.assert.calledOnce(hawkRequestStub);
-        sinon.assert.calledWith(hawkRequestStub,
-          mozLoop.LOOP_SESSION_TYPE.FXA,
-          "/calls",
-          "POST",
-          { calleeId: calleeIds, callType: callType, channel: "unknown" }
-        );
-      });
-
-      it("should include the channel when defined", function() {
-        mozLoop.appVersionInfo = {
-          channel: "beta"
-        };
-
-        client.setupOutgoingCall(calleeIds, callType);
-
-        sinon.assert.calledOnce(hawkRequestStub);
-        sinon.assert.calledWith(hawkRequestStub,
-          mozLoop.LOOP_SESSION_TYPE.FXA,
-          "/calls",
-          "POST",
-          { calleeId: calleeIds, callType: callType, channel: "beta" }
-        );
-      });
-
-      it("should call the callback if the request is successful", function() {
-        var requestData = {
-          apiKey: "fake",
-          callId: "fakeCall",
-          progressURL: "fakeurl",
-          sessionId: "12345678",
-          sessionToken: "15263748",
-          websocketToken: "13572468"
-        };
-
-        hawkRequestStub.callsArgWith(4, null, JSON.stringify(requestData));
-
-        client.setupOutgoingCall(calleeIds, callType, callback);
-
-        sinon.assert.calledOnce(callback);
-        sinon.assert.calledWithExactly(callback, null, requestData);
-      });
-
-      it("should send an error when the request fails", function() {
-        hawkRequestStub.callsArgWith(4, fakeErrorRes);
-
-        client.setupOutgoingCall(calleeIds, callType, callback);
-
-        sinon.assert.calledOnce(callback);
-        sinon.assert.calledWithExactly(callback, sinon.match(function(err) {
-          return err.code === 400 && err.message === "invalid token";
-        }));
-      });
-
-      it("should send an error if the data is not valid", function() {
-        // Sets up the hawkRequest stub to trigger the callback with
-        // an error
-        hawkRequestStub.callsArgWith(4, null, "{}");
-
-        client.setupOutgoingCall(calleeIds, callType, callback);
-
-        sinon.assert.calledOnce(callback);
-        sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
-          return /Invalid data received/.test(err.message);
-        }));
-      });
-    });
-  });
-});
--- a/browser/components/loop/test/desktop-local/conversationAppStore_test.js
+++ b/browser/components/loop/test/desktop-local/conversationAppStore_test.js
@@ -72,18 +72,18 @@ describe("loop.store.ConversationAppStor
   });
 
   describe("#getWindowData", function() {
     var fakeWindowData, fakeGetWindowData, fakeMozLoop, store, getLoopPrefStub;
     var setLoopPrefStub;
 
     beforeEach(function() {
       fakeWindowData = {
-        type: "incoming",
-        callId: "123456"
+        type: "room",
+        roomToken: "123456"
       };
 
       fakeGetWindowData = {
         windowId: "42"
       };
 
       getLoopPrefStub = sandbox.stub();
       setLoopPrefStub = sandbox.stub();
@@ -108,17 +108,17 @@ describe("loop.store.ConversationAppStor
 
     afterEach(function() {
       sandbox.restore();
     });
 
     it("should fetch the window type from the mozLoop API", function() {
       store.getWindowData(new sharedActions.GetWindowData(fakeGetWindowData));
 
-      expect(store.getStoreState().windowType).eql("incoming");
+      expect(store.getStoreState().windowType).eql("room");
     });
 
     it("should have the feedback period in initial state", function() {
       getLoopPrefStub.returns(42);
 
       // Expect ms.
       expect(store.getInitialStoreState().feedbackPeriod).to.eql(42 * 1000);
     });
@@ -214,36 +214,16 @@ describe("loop.store.ConversationAppStor
       });
     });
 
     describe("#LoopHangupNowHandler", function() {
       beforeEach(function() {
         sandbox.stub(loop.shared.mixins.WindowCloseMixin, "closeWindow");
       });
 
-      it("should dispatch the correct action for windowType 'incoming'", function() {
-        store.setStoreState({ windowType: "incoming" });
-
-        store.LoopHangupNowHandler();
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.HangupCall());
-        sinon.assert.notCalled(loop.shared.mixins.WindowCloseMixin.closeWindow);
-      });
-
-      it("should dispatch the correct action for windowType 'outgoing'", function() {
-        store.setStoreState({ windowType: "outgoing" });
-
-        store.LoopHangupNowHandler();
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.HangupCall());
-        sinon.assert.notCalled(loop.shared.mixins.WindowCloseMixin.closeWindow);
-      });
-
       it("should dispatch the correct action when a room was used", function() {
         store.setStoreState({ windowType: "room" });
         roomUsed = true;
 
         store.LoopHangupNowHandler();
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.LeaveRoom());
deleted file mode 100644
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ /dev/null
@@ -1,965 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-describe("loop.conversationViews", function () {
-  "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, 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();
-    fakeClock = sandbox.useFakeTimers();
-
-    sandbox.stub(document.mozL10n, "get", function(x) {
-      return x;
-    });
-
-    dispatcher = new loop.Dispatcher();
-    sandbox.stub(dispatcher, "dispatch");
-
-    contact = {
-      name: [ "mrsmith" ],
-      email: [{
-        type: "home",
-        value: "fakeEmail",
-        pref: true
-      }]
-    };
-    fakeAudioXHR = {
-      open: sinon.spy(),
-      send: function() {},
-      abort: function() {},
-      getResponseHeader: function(header) {
-        if (header === "Content-Type") {
-          return "audio/ogg";
-        }
-      },
-      responseType: null,
-      response: new ArrayBuffer(10),
-      onload: null
-    };
-
-    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",
-          platform: "test"
-        };
-      },
-      getAudioBlob: sinon.spy(function(name, callback) {
-        callback(null, new Blob([new ArrayBuffer(10)], {type: "audio/ogg"}));
-      }),
-      startAlerting: sinon.stub(),
-      stopAlerting: sinon.stub(),
-      userProfile: {
-        email: "bob@invalid.tld"
-      }
-    };
-    sinon.stub(fakeMozLoop, "getLoopPref", function(pref) {
-        if (pref === "fake") {
-          return "http://fakeurl";
-        }
-
-        return false;
-    });
-
-    fakeWindow = {
-      navigator: { mozLoop: fakeMozLoop },
-      close: sinon.stub(),
-      document: {},
-      addEventListener: function() {},
-      removeEventListener: function() {}
-    };
-    loop.shared.mixins.setRootObject(fakeWindow);
-
-    conversationStore = new loop.store.ConversationStore(dispatcher, {
-      client: {},
-      mozLoop: fakeMozLoop,
-      sdkDriver: {}
-    });
-
-    var textChatStore = new loop.store.TextChatStore(dispatcher, {
-      sdkDriver: {}
-    });
-
-    loop.store.StoreMixin.register({
-      conversationStore: conversationStore,
-      textChatStore: textChatStore
-    });
-  });
-
-  afterEach(function() {
-    loop.shared.mixins.setRootObject(window);
-    view = undefined;
-    delete navigator.mozLoop;
-    sandbox.restore();
-  });
-
-  describe("CallIdentifierView", function() {
-    function mountTestComponent(props) {
-      return TestUtils.renderIntoDocument(
-        React.createElement(loop.conversationViews.CallIdentifierView, props));
-    }
-
-    it("should set display the peer identifer", function() {
-      view = mountTestComponent({
-        showIcons: false,
-        peerIdentifier: "mrssmith"
-      });
-
-      expect(TestUtils.findRenderedDOMComponentWithClass(
-        view, "fx-embedded-call-identifier-text").props.children).eql("mrssmith");
-    });
-
-    it("should not display the icons if showIcons is false", function() {
-      view = mountTestComponent({
-        showIcons: false,
-        peerIdentifier: "mrssmith"
-      });
-
-      expect(TestUtils.findRenderedDOMComponentWithClass(
-        view, "fx-embedded-call-detail").props.className).to.contain("hide");
-    });
-
-    it("should display the icons if showIcons is true", function() {
-      view = mountTestComponent({
-        showIcons: true,
-        peerIdentifier: "mrssmith"
-      });
-
-      expect(TestUtils.findRenderedDOMComponentWithClass(
-        view, "fx-embedded-call-detail").props.className).to.not.contain("hide");
-    });
-
-    it("should display the url timestamp", function() {
-      sandbox.stub(loop.shared.utils, "formatDate").returns(("October 9, 2014"));
-
-      view = mountTestComponent({
-        showIcons: true,
-        peerIdentifier: "mrssmith",
-        urlCreationDate: (new Date() / 1000).toString()
-      });
-
-      expect(TestUtils.findRenderedDOMComponentWithClass(
-        view, "fx-embedded-conversation-timestamp").props.children).eql("(October 9, 2014)");
-    });
-
-    it("should show video as muted if video is false", function() {
-      view = mountTestComponent({
-        showIcons: true,
-        peerIdentifier: "mrssmith",
-        video: false
-      });
-
-      expect(TestUtils.findRenderedDOMComponentWithClass(
-        view, "fx-embedded-tiny-video-icon").props.className).to.contain("muted");
-    });
-  });
-
-  describe("PendingConversationView", function() {
-    function mountTestComponent(props) {
-      return TestUtils.renderIntoDocument(
-        React.createElement(loop.conversationViews.PendingConversationView, props));
-    }
-
-    it("should set display connecting string when the state is not alerting",
-      function() {
-        view = mountTestComponent({
-          callState: CALL_STATES.CONNECTING,
-          contact: contact,
-          dispatcher: dispatcher
-        });
-
-        var label = TestUtils.findRenderedDOMComponentWithClass(
-          view, "btn-label").props.children;
-
-        expect(label).to.have.string("connecting");
-    });
-
-    it("should set display ringing string when the state is alerting",
-      function() {
-        view = mountTestComponent({
-          callState: CALL_STATES.ALERTING,
-          contact: contact,
-          dispatcher: dispatcher
-        });
-
-        var label = TestUtils.findRenderedDOMComponentWithClass(
-          view, "btn-label").props.children;
-
-        expect(label).to.have.string("ringing");
-    });
-
-    it("should disable the cancel button if enableCancelButton is false",
-      function() {
-        view = mountTestComponent({
-          callState: CALL_STATES.CONNECTING,
-          contact: contact,
-          dispatcher: dispatcher,
-          enableCancelButton: false
-        });
-
-        var cancelBtn = view.getDOMNode().querySelector(".btn-cancel");
-
-        expect(cancelBtn.classList.contains("disabled")).eql(true);
-      });
-
-    it("should enable the cancel button if enableCancelButton is false",
-      function() {
-        view = mountTestComponent({
-          callState: CALL_STATES.CONNECTING,
-          contact: contact,
-          dispatcher: dispatcher,
-          enableCancelButton: true
-        });
-
-        var cancelBtn = view.getDOMNode().querySelector(".btn-cancel");
-
-        expect(cancelBtn.classList.contains("disabled")).eql(false);
-      });
-
-    it("should dispatch a cancelCall action when the cancel button is pressed",
-      function() {
-        view = mountTestComponent({
-          callState: CALL_STATES.CONNECTING,
-          contact: contact,
-          dispatcher: dispatcher
-        });
-
-        var cancelBtn = view.getDOMNode().querySelector(".btn-cancel");
-
-        React.addons.TestUtils.Simulate.click(cancelBtn);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("name", "cancelCall"));
-      });
-  });
-
-  describe("FailureInfoView", function() {
-    function mountTestComponent(options) {
-      return TestUtils.renderIntoDocument(
-        React.createElement(loop.conversationViews.FailureInfoView, options));
-    }
-
-    it("should display a generic failure message by default", function() {
-      view = mountTestComponent({
-        failureReason: "fake"
-      });
-
-      var message = view.getDOMNode().querySelector(".failure-info-message");
-
-      expect(message.textContent).eql("generic_failure_message");
-    });
-
-    it("should display a no media message for the no media reason", function() {
-      view = mountTestComponent({
-        failureReason: FAILURE_DETAILS.NO_MEDIA
-      });
-
-      var message = view.getDOMNode().querySelector(".failure-info-message");
-
-      expect(message.textContent).eql("no_media_failure_message");
-    });
-
-    it("should display a no media message for the unable to publish reason", function() {
-      view = mountTestComponent({
-        failureReason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
-      });
-
-      var message = view.getDOMNode().querySelector(".failure-info-message");
-
-      expect(message.textContent).eql("no_media_failure_message");
-    });
-
-    it("should display a user unavailable message for the unavailable reason", function() {
-      view = mountTestComponent({
-        contact: {email: [{value: "test@test.tld"}]},
-        failureReason: FAILURE_DETAILS.USER_UNAVAILABLE
-      });
-
-      var message = view.getDOMNode().querySelector(".failure-info-message");
-
-      expect(message.textContent).eql("contact_unavailable_title");
-    });
-
-    it("should display a ToS failure message for the ToS failure reason", function() {
-      view = mountTestComponent({
-        failureReason: FAILURE_DETAILS.TOS_FAILURE
-      });
-
-      var message = view.getDOMNode().querySelector(".failure-info-message");
-
-      expect(message.textContent).eql("tos_failure_message");
-    });
-
-    it("should display a generic unavailable message if the contact doesn't have a display name", function() {
-      view = mountTestComponent({
-        contact: {
-          tel: [{"pref": true, type: "work", value: ""}]
-        },
-        failureReason: FAILURE_DETAILS.USER_UNAVAILABLE
-      });
-
-      var message = view.getDOMNode().querySelector(".failure-info-message");
-
-      expect(message.textContent).eql("generic_contact_unavailable_title");
-    });
-
-    it("should display an extra message", function() {
-      view = mountTestComponent({
-        extraMessage: "Fake message",
-        failureReason: FAILURE_DETAILS.UNKNOWN
-      });
-
-      var extraMessage = view.getDOMNode().querySelector(".failure-info-extra");
-
-      expect(extraMessage.textContent).eql("Fake message");
-    });
-
-    it("should display an extra failure message", function() {
-      view = mountTestComponent({
-        extraFailureMessage: "Fake failure message",
-        failureReason: FAILURE_DETAILS.UNKNOWN
-      });
-
-      var extraFailureMessage = view.getDOMNode().querySelector(".failure-info-extra-failure");
-
-      expect(extraFailureMessage.textContent).eql("Fake failure message");
-    });
-
-    it("should display an ICE failure message", function() {
-      view = mountTestComponent({
-        failureReason: FAILURE_DETAILS.ICE_FAILED
-      });
-
-      var message = view.getDOMNode().querySelector(".failure-info-message");
-
-      expect(message.textContent).eql("ice_failure_message");
-    });
-  });
-
-  describe("DirectCallFailureView", function() {
-    var fakeAudio, composeCallUrlEmail;
-
-    var fakeContact = {email: [{value: "test@test.tld"}]};
-
-    function mountTestComponent(options) {
-      var props = _.extend({
-          dispatcher: dispatcher,
-          mozLoop: fakeMozLoop,
-          outgoing: true
-        }, options);
-      return TestUtils.renderIntoDocument(
-        React.createElement(loop.conversationViews.DirectCallFailureView, props));
-    }
-
-    beforeEach(function() {
-      fakeAudio = {
-        play: sinon.spy(),
-        pause: sinon.spy(),
-        removeAttribute: sinon.spy()
-      };
-      sandbox.stub(window, "Audio").returns(fakeAudio);
-      composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
-      conversationStore.setStoreState({
-        callStateReason: FAILURE_DETAILS.UNKNOWN,
-        contact: fakeContact
-      });
-    });
-
-    it("should not display the retry button for incoming calls", function() {
-      view = mountTestComponent({outgoing: false});
-
-      var retryBtn = view.getDOMNode().querySelector(".btn-retry");
-
-      expect(retryBtn.classList.contains("hide")).eql(true);
-    });
-
-    it("should not display the email button for incoming calls", function() {
-      view = mountTestComponent({outgoing: false});
-
-      var retryBtn = view.getDOMNode().querySelector(".btn-email");
-
-      expect(retryBtn.classList.contains("hide")).eql(true);
-    });
-
-    it("should dispatch a retryCall action when the retry button is pressed",
-      function() {
-        view = mountTestComponent();
-
-        var retryBtn = view.getDOMNode().querySelector(".btn-retry");
-
-        React.addons.TestUtils.Simulate.click(retryBtn);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("name", "retryCall"));
-      });
-
-    it("should dispatch a cancelCall action when the cancel button is pressed",
-      function() {
-        view = mountTestComponent();
-
-        var cancelBtn = view.getDOMNode().querySelector(".btn-cancel");
-
-        React.addons.TestUtils.Simulate.click(cancelBtn);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("name", "cancelCall"));
-      });
-
-    it("should dispatch a fetchRoomEmailLink action when the email button is pressed",
-      function() {
-        view = mountTestComponent();
-
-        var emailLinkBtn = view.getDOMNode().querySelector(".btn-email");
-
-        React.addons.TestUtils.Simulate.click(emailLinkBtn);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("name", "fetchRoomEmailLink"));
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("roomName", "test@test.tld"));
-      });
-
-    it("should name the created room using the contact name when available",
-      function() {
-        conversationStore.setStoreState({
-          contact: {
-            email: [{value: "test@test.tld"}],
-            name: ["Mr Fake ContactName"]
-          }
-        });
-
-        view = mountTestComponent();
-
-        var emailLinkBtn = view.getDOMNode().querySelector(".btn-email");
-
-        React.addons.TestUtils.Simulate.click(emailLinkBtn);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("roomName", "Mr Fake ContactName"));
-      });
-
-    it("should disable the email link button once the action is dispatched",
-      function() {
-        view = mountTestComponent();
-        var emailLinkBtn = view.getDOMNode().querySelector(".btn-email");
-        React.addons.TestUtils.Simulate.click(emailLinkBtn);
-
-        expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(true);
-      });
-
-    it("should compose an email once the email link is received", function() {
-      view = mountTestComponent();
-      conversationStore.setStoreState({emailLink: "http://fake.invalid/"});
-
-      sinon.assert.calledOnce(composeCallUrlEmail);
-      sinon.assert.calledWithExactly(composeCallUrlEmail,
-        "http://fake.invalid/", "test@test.tld", null, "callfailed");
-    });
-
-    it("should close the conversation window once the email link is received",
-      function() {
-        view = mountTestComponent();
-
-        conversationStore.setStoreState({emailLink: "http://fake.invalid/"});
-
-        sinon.assert.calledOnce(fakeWindow.close);
-      });
-
-    it("should display an error message in case email link retrieval failed",
-      function() {
-        view = mountTestComponent();
-
-        conversationStore.trigger("error:emailLink");
-
-        expect(view.getDOMNode().querySelector(".failure-info-extra-failure")).not.eql(null);
-      });
-
-    it("should allow retrying to get a call url if it failed previously",
-      function() {
-        view = mountTestComponent();
-
-        conversationStore.trigger("error:emailLink");
-
-        expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(false);
-      });
-
-    it("should play a failure sound, once", function() {
-      view = mountTestComponent();
-
-      sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
-      sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
-                                     "failure", sinon.match.func);
-      sinon.assert.calledOnce(fakeAudio.play);
-      expect(fakeAudio.loop).to.equal(false);
-    });
-
-    it("should display an additional message for outgoing calls", function() {
-      view = mountTestComponent({
-        outgoing: true
-      });
-
-      var extraMessage = view.getDOMNode().querySelector(".failure-info-extra");
-
-      expect(extraMessage.textContent).eql("generic_failure_with_reason2");
-    });
-  });
-
-  describe("OngoingConversationView", function() {
-    function mountTestComponent(extraProps) {
-      var props = _.extend({
-        chatWindowDetached: false,
-        conversationStore: conversationStore,
-        dispatcher: dispatcher,
-        mozLoop: {},
-        matchMedia: window.matchMedia
-      }, extraProps);
-      return TestUtils.renderIntoDocument(
-        React.createElement(loop.conversationViews.OngoingConversationView, props));
-    }
-
-    it("should dispatch a setupStreamElements action when the view is created",
-      function() {
-        view = mountTestComponent();
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("name", "setupStreamElements"));
-      });
-
-    it("should display the remote video when the stream is enabled", function() {
-      conversationStore.setStoreState({
-        remoteSrcMediaElement: { fake: 1 }
-      });
-
-      view = mountTestComponent({
-        mediaConnected: true,
-        remoteVideoEnabled: true
-      });
-
-      expect(view.getDOMNode().querySelector(".remote video")).not.eql(null);
-    });
-
-    it("should display the local video when the stream is enabled", function() {
-      conversationStore.setStoreState({
-        localSrcMediaElement: { fake: 1 }
-      });
-
-      view = mountTestComponent({
-        video: {
-          enabled: true
-        }
-      });
-
-      expect(view.getDOMNode().querySelector(".local video")).not.eql(null);
-    });
-
-    it("should dispatch a setMute action when the audio mute button is pressed",
-      function() {
-        view = mountTestComponent({
-          audio: {enabled: false}
-        });
-
-        var muteBtn = view.getDOMNode().querySelector(".btn-mute-audio");
-
-        React.addons.TestUtils.Simulate.click(muteBtn);
-
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("name", "setMute"));
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("enabled", true));
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("type", "audio"));
-      });
-
-    it("should dispatch a setMute action when the video mute button is pressed",
-      function() {
-        view = mountTestComponent({
-          video: {enabled: true}
-        });
-
-        var muteBtn = view.getDOMNode().querySelector(".btn-mute-video");
-
-        React.addons.TestUtils.Simulate.click(muteBtn);
-
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("name", "setMute"));
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("enabled", false));
-        sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("type", "video"));
-      });
-
-    it("should set the mute button as mute off", function() {
-      view = mountTestComponent({
-        video: {enabled: true}
-      });
-
-      var muteBtn = view.getDOMNode().querySelector(".btn-mute-video");
-
-      expect(muteBtn.classList.contains("muted")).eql(false);
-    });
-
-    it("should set the mute button as mute on", function() {
-      view = mountTestComponent({
-        audio: {enabled: false}
-      });
-
-      var muteBtn = view.getDOMNode().querySelector(".btn-mute-audio");
-
-      expect(muteBtn.classList.contains("muted")).eql(true);
-    });
-  });
-
-  describe("CallControllerView", function() {
-    var onCallTerminatedStub;
-
-    function mountTestComponent() {
-      return TestUtils.renderIntoDocument(
-        React.createElement(loop.conversationViews.CallControllerView, {
-          chatWindowDetached: false,
-          dispatcher: dispatcher,
-          mozLoop: fakeMozLoop,
-          onCallTerminated: onCallTerminatedStub
-        }));
-    }
-
-    beforeEach(function() {
-      onCallTerminatedStub = sandbox.stub();
-    });
-
-    afterEach(function() {
-      sandbox.restore();
-    });
-
-    it("should set the document title to the callerId", function() {
-      conversationStore.setStoreState({
-        contact: contact
-      });
-
-      mountTestComponent();
-
-      expect(fakeWindow.document.title).eql("mrsmith");
-    });
-
-    it("should fallback to the contact email if the contact name is not defined", function() {
-      delete contact.name;
-      conversationStore.setStoreState({
-        contact: contact
-      });
-
-      mountTestComponent({contact: contact});
-
-      expect(fakeWindow.document.title).eql("fakeEmail");
-    });
-
-    it("should fallback to the caller id if no contact is defined", function() {
-      conversationStore.setStoreState({
-        callerId: "fakeId"
-      });
-
-      mountTestComponent({contact: contact});
-
-      expect(fakeWindow.document.title).eql("fakeId");
-    });
-
-    it("should render the DirectCallFailureView when the call state is 'terminated'",
-      function() {
-        conversationStore.setStoreState({
-          callState: CALL_STATES.TERMINATED,
-          contact: contact,
-          callStateReason: WEBSOCKET_REASONS.CLOSED,
-          outgoing: true
-        });
-
-        view = mountTestComponent();
-
-        TestUtils.findRenderedComponentWithType(view,
-          loop.conversationViews.DirectCallFailureView);
-    });
-
-    it("should render the PendingConversationView for outgoing calls when the call state is 'gather'",
-      function() {
-        conversationStore.setStoreState({
-          callState: CALL_STATES.GATHER,
-          contact: contact,
-          outgoing: true
-        });
-
-        view = mountTestComponent();
-
-        TestUtils.findRenderedComponentWithType(view,
-          loop.conversationViews.PendingConversationView);
-    });
-
-    it("should render the AcceptCallView for incoming calls when the call state is 'alerting'", function() {
-      conversationStore.setStoreState({
-        callState: CALL_STATES.ALERTING,
-        outgoing: false,
-        callerId: "fake@invalid.com"
-      });
-
-      view = mountTestComponent();
-
-      TestUtils.findRenderedComponentWithType(view,
-        loop.conversationViews.AcceptCallView);
-    });
-
-    it("should render the OngoingConversationView when the call state is 'ongoing'",
-      function() {
-        conversationStore.setStoreState({callState: CALL_STATES.ONGOING});
-
-        view = mountTestComponent();
-
-        TestUtils.findRenderedComponentWithType(view,
-          loop.conversationViews.OngoingConversationView);
-    });
-
-    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);
-
-        conversationStore.setStoreState({callState: CALL_STATES.FINISHED});
-
-        view = mountTestComponent();
-
-        sinon.assert.calledOnce(fakeAudio.play);
-    });
-
-    it("should update the rendered views when the state is changed.",
-      function() {
-        conversationStore.setStoreState({
-          callState: CALL_STATES.GATHER,
-          contact: contact,
-          outgoing: true
-        });
-        view = mountTestComponent();
-        TestUtils.findRenderedComponentWithType(view,
-          loop.conversationViews.PendingConversationView);
-        conversationStore.setStoreState({
-          callState: CALL_STATES.TERMINATED,
-          callStateReason: WEBSOCKET_REASONS.CLOSED
-        });
-        TestUtils.findRenderedComponentWithType(view,
-          loop.conversationViews.DirectCallFailureView);
-    });
-
-    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(
-        React.createElement(loop.conversationViews.AcceptCallView, props));
-    }
-
-    afterEach(function() {
-      callView = null;
-    });
-
-    it("should start alerting on display", function() {
-      callView = mountTestComponent({
-        callType: CALL_TYPES.AUDIO_VIDEO,
-        callerId: "fake@invalid.com"
-      });
-
-      sinon.assert.calledOnce(fakeMozLoop.startAlerting);
-    });
-
-    it("should stop alerting when removed from the display", function() {
-      callView = mountTestComponent({
-        callType: CALL_TYPES.AUDIO_VIDEO,
-        callerId: "fake@invalid.com"
-      });
-
-      callView.componentWillUnmount();
-
-      sinon.assert.calledOnce(fakeMozLoop.stopAlerting);
-    });
-
-    describe("default answer mode", function() {
-      it("should display video as primary answer mode", function() {
-        callView = mountTestComponent({
-          callType: CALL_TYPES.AUDIO_VIDEO,
-          callerId: "fake@invalid.com"
-        });
-
-        var primaryBtn = callView.getDOMNode()
-                                 .querySelector(".fx-embedded-btn-icon-video");
-
-        expect(primaryBtn).not.to.eql(null);
-      });
-
-      it("should display audio as primary answer mode", function() {
-        callView = mountTestComponent({
-          callType: CALL_TYPES.AUDIO_ONLY,
-          callerId: "fake@invalid.com"
-        });
-
-        var primaryBtn = callView.getDOMNode()
-                                 .querySelector(".fx-embedded-btn-icon-audio");
-
-        expect(primaryBtn).not.to.eql(null);
-      });
-
-      it("should accept call with video", function() {
-        callView = mountTestComponent({
-          callType: CALL_TYPES.AUDIO_VIDEO,
-          callerId: "fake@invalid.com"
-        });
-
-        var primaryBtn = callView.getDOMNode()
-                                 .querySelector(".fx-embedded-btn-icon-video");
-
-        React.addons.TestUtils.Simulate.click(primaryBtn);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.AcceptCall({
-            callType: CALL_TYPES.AUDIO_VIDEO
-          }));
-      });
-
-      it("should accept call with audio", function() {
-        callView = mountTestComponent({
-          callType: CALL_TYPES.AUDIO_ONLY,
-          callerId: "fake@invalid.com"
-        });
-
-        var primaryBtn = callView.getDOMNode()
-                                 .querySelector(".fx-embedded-btn-icon-audio");
-
-        React.addons.TestUtils.Simulate.click(primaryBtn);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.AcceptCall({
-            callType: CALL_TYPES.AUDIO_ONLY
-          }));
-      });
-
-      it("should accept call with video when clicking on secondary btn",
-        function() {
-          callView = mountTestComponent({
-            callType: CALL_TYPES.AUDIO_ONLY,
-            callerId: "fake@invalid.com"
-          });
-
-          var secondaryBtn = callView.getDOMNode()
-          .querySelector(".fx-embedded-btn-video-small");
-
-          React.addons.TestUtils.Simulate.click(secondaryBtn);
-
-          sinon.assert.calledOnce(dispatcher.dispatch);
-          sinon.assert.calledWithExactly(dispatcher.dispatch,
-            new sharedActions.AcceptCall({
-              callType: CALL_TYPES.AUDIO_VIDEO
-            }));
-        });
-
-      it("should accept call with audio when clicking on secondary btn",
-        function() {
-          callView = mountTestComponent({
-            callType: CALL_TYPES.AUDIO_VIDEO,
-            callerId: "fake@invalid.com"
-          });
-
-          var secondaryBtn = callView.getDOMNode()
-          .querySelector(".fx-embedded-btn-audio-small");
-
-          React.addons.TestUtils.Simulate.click(secondaryBtn);
-
-          sinon.assert.calledOnce(dispatcher.dispatch);
-          sinon.assert.calledWithExactly(dispatcher.dispatch,
-            new sharedActions.AcceptCall({
-              callType: CALL_TYPES.AUDIO_ONLY
-            }));
-        });
-    });
-
-    describe("click event on .btn-decline", function() {
-      it("should dispatch a DeclineCall action", function() {
-        callView = mountTestComponent({
-          callType: CALL_TYPES.AUDIO_VIDEO,
-          callerId: "fake@invalid.com"
-        });
-
-        var buttonDecline = callView.getDOMNode().querySelector(".btn-decline");
-
-        TestUtils.Simulate.click(buttonDecline);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.DeclineCall({blockCaller: false}));
-      });
-    });
-
-    describe("click event on .btn-block", function() {
-      it("should dispatch a DeclineCall action with blockCaller true", function() {
-        callView = mountTestComponent({
-          callType: CALL_TYPES.AUDIO_VIDEO,
-          callerId: "fake@invalid.com"
-        });
-
-        var buttonBlock = callView.getDOMNode().querySelector(".btn-block");
-
-        TestUtils.Simulate.click(buttonBlock);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.DeclineCall({blockCaller: true}));
-      });
-    });
-  });
-});
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -131,114 +131,76 @@ describe("loop.conversation", function()
       sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
         new loop.shared.actions.GetWindowData({
           windowId: "42"
         }));
     });
   });
 
   describe("AppControllerView", function() {
-    var conversationStore, activeRoomStore, client, ccView, dispatcher;
+    var activeRoomStore, ccView, dispatcher;
     var conversationAppStore, roomStore, feedbackPeriodMs = 15770000000;
     var ROOM_STATES = loop.store.ROOM_STATES;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversation.AppControllerView, {
           roomStore: roomStore,
           dispatcher: dispatcher,
           mozLoop: navigator.mozLoop
         }));
     }
 
     beforeEach(function() {
-      client = new loop.Client();
       dispatcher = new loop.Dispatcher();
-      conversationStore = new loop.store.ConversationStore(
-        dispatcher, {
-          client: client,
-          mozLoop: navigator.mozLoop,
-          sdkDriver: {}
-        });
-
-      conversationStore.setStoreState({contact: {
-        name: [ "Mr Smith" ],
-        email: [{
-          type: "home",
-          value: "fakeEmail",
-          pref: true
-        }]
-      }});
 
       activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
         mozLoop: {},
         sdkDriver: {}
       });
       roomStore = new loop.store.RoomStore(dispatcher, {
         mozLoop: navigator.mozLoop,
         activeRoomStore: activeRoomStore
       });
       conversationAppStore = new loop.store.ConversationAppStore({
         activeRoomStore: activeRoomStore,
         dispatcher: dispatcher,
         mozLoop: navigator.mozLoop
       });
 
       loop.store.StoreMixin.register({
-        conversationAppStore: conversationAppStore,
-        conversationStore: conversationStore
+        conversationAppStore: conversationAppStore
       });
     });
 
     afterEach(function() {
       ccView = undefined;
     });
 
-    it("should display the CallControllerView for outgoing calls", function() {
-      conversationAppStore.setStoreState({windowType: "outgoing"});
-
-      ccView = mountTestComponent();
-
-      TestUtils.findRenderedComponentWithType(ccView,
-        loop.conversationViews.CallControllerView);
-    });
-
-    it("should display the CallControllerView for incoming calls", function() {
-      conversationAppStore.setStoreState({windowType: "incoming"});
-
-      ccView = mountTestComponent();
-
-      TestUtils.findRenderedComponentWithType(ccView,
-        loop.conversationViews.CallControllerView);
-    });
-
     it("should display the RoomView for rooms", function() {
       conversationAppStore.setStoreState({windowType: "room"});
       activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
         loop.roomViews.DesktopRoomConversationView);
     });
 
-    it("should display the DirectCallFailureView for failures", function() {
+    it("should display the RoomFailureView for failures", function() {
       conversationAppStore.setStoreState({
         contact: {},
         outgoing: false,
         windowType: "failed"
       });
-      conversationStore.setStoreState({
-        callStateReason: FAILURE_DETAILS.UNKNOWN
-      });
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
-        loop.conversationViews.DirectCallFailureView);
+        loop.roomViews.RoomFailureView);
     });
 
     it("should set the correct title when rendering feedback view", function() {
       conversationAppStore.setStoreState({showFeedbackForm: true});
 
       ccView = mountTestComponent();
 
       sinon.assert.calledWithExactly(mozL10nGet, "conversation_has_ended");
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -43,45 +43,39 @@
     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/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/activeRoomStore.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/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
     document.removeEventListener('DOMContentLoaded', loop.panel.init);
     document.removeEventListener('DOMContentLoaded', loop.conversation.init);
 
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -157,17 +157,17 @@ describe("loop.roomViews", function () {
       };
       sandbox.stub(window, "Audio").returns(fakeAudio);
     });
 
     it("should render the FailureInfoView", function() {
       view = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(view,
-        loop.conversationViews.FailureInfoView);
+        loop.roomViews.FailureInfoView);
     });
 
     it("should dispatch a JoinRoom action when the rejoin call button is pressed", function() {
       view = mountTestComponent();
 
       var rejoinBtn = view.getDOMNode().querySelector(".btn-rejoin");
 
       React.addons.TestUtils.Simulate.click(rejoinBtn);
--- a/browser/components/loop/test/karma/karma.coverage.desktop.js
+++ b/browser/components/loop/test/karma/karma.coverage.desktop.js
@@ -14,33 +14,29 @@ module.exports = function(config) {
     "content/shared/libs/react-0.12.2.js",
     "content/shared/libs/lodash-3.9.3.js",
     "content/shared/libs/backbone-1.2.1.js",
     "test/shared/vendor/*.js",
     "test/karma/head.js", // Stub out DOM event listener due to races.
     "content/shared/js/utils.js",
     "content/shared/js/models.js",
     "content/shared/js/mixins.js",
-    "content/shared/js/websocket.js",
     "content/shared/js/actions.js",
     "content/shared/js/otSdkDriver.js",
     "content/shared/js/validate.js",
     "content/shared/js/dispatcher.js",
     "content/shared/js/store.js",
-    "content/shared/js/conversationStore.js",
     "content/shared/js/activeRoomStore.js",
     "content/shared/js/views.js",
     "content/shared/js/textChatStore.js",
     "content/shared/js/textChatView.js",
     "content/js/feedbackViews.js",
-    "content/js/client.js",
     "content/js/conversationAppStore.js",
     "content/js/roomStore.js",
     "content/js/roomViews.js",
-    "content/js/conversationViews.js",
     "content/js/conversation.js",
     "test/desktop-local/*.js"
   ]);
 
   // List of files to exclude.
   baseConfig.exclude = baseConfig.exclude.concat([
     "test/desktop-local/panel_test.js",
     "test/desktop-local/contacts_test.js"
--- a/browser/components/loop/test/karma/karma.coverage.shared_standalone.js
+++ b/browser/components/loop/test/karma/karma.coverage.shared_standalone.js
@@ -17,23 +17,21 @@ module.exports = function(config) {
     "content/shared/libs/sdk.js",
     "test/shared/vendor/*.js",
     "test/karma/head.js", // Add test fixture container
     "content/shared/js/utils.js",
     "content/shared/js/store.js",
     "content/shared/js/models.js",
     "content/shared/js/mixins.js",
     "content/shared/js/crypto.js",
-    "content/shared/js/websocket.js",
     "content/shared/js/validate.js",
     "content/shared/js/actions.js",
     "content/shared/js/dispatcher.js",
     "content/shared/js/otSdkDriver.js",
     "content/shared/js/activeRoomStore.js",
-    "content/shared/js/conversationStore.js",
     "content/shared/js/views.js",
     "content/shared/js/textChatStore.js",
     "content/shared/js/textChatView.js",
     "content/shared/js/urlRegExps.js",
     "content/shared/js/linkifiedTextView.js",
     "standalone/content/js/standaloneAppStore.js",
     "standalone/content/js/standaloneMozLoop.js",
     "standalone/content/js/standaloneRoomViews.js",
deleted file mode 100644
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ /dev/null
@@ -1,1318 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-describe("loop.store.ConversationStore", function () {
-  "use strict";
-
-  var expect = chai.expect;
-  var CALL_STATES = loop.store.CALL_STATES;
-  var WS_STATES = loop.store.WS_STATES;
-  var CALL_TYPES = loop.shared.utils.CALL_TYPES;
-  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
-  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
-  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
-  var sharedActions = loop.shared.actions;
-  var sharedUtils = loop.shared.utils;
-  var sandbox, dispatcher, client, store, fakeSessionData, sdkDriver;
-  var contact, fakeMozLoop, fakeStreamElement;
-  var connectPromise, resolveConnectPromise, rejectConnectPromise;
-  var wsCancelSpy, wsCloseSpy, wsDeclineSpy, wsMediaUpSpy, fakeWebsocket;
-
-  function checkFailures(done, f) {
-    try {
-      f();
-      done();
-    } catch (err) {
-      done(err);
-    }
-  }
-
-  beforeEach(function() {
-    sandbox = sinon.sandbox.create();
-
-    contact = {
-      name: [ "Mr Smith" ],
-      email: [{
-        type: "home",
-        value: "fakeEmail",
-        pref: true
-      }]
-    };
-
-    fakeMozLoop = {
-      ROOM_CREATE: {
-        CREATE_SUCCESS: 0,
-        CREATE_FAIL: 1
-      },
-      getLoopPref: sandbox.stub(),
-      addConversationContext: sandbox.stub(),
-      calls: {
-        setCallInProgress: sandbox.stub(),
-        clearCallInProgress: sandbox.stub(),
-        blockDirectCaller: sandbox.stub()
-      },
-      rooms: {
-        create: sandbox.stub()
-      },
-      telemetryAddValue: sandbox.stub()
-    };
-
-    dispatcher = new loop.Dispatcher();
-    client = {
-      setupOutgoingCall: sinon.stub(),
-      requestCallUrl: sinon.stub()
-    };
-    sdkDriver = {
-      connectSession: sinon.stub(),
-      disconnectSession: sinon.stub(),
-      retryPublishWithoutVideo: sinon.stub()
-    };
-
-    wsCancelSpy = sinon.spy();
-    wsCloseSpy = sinon.spy();
-    wsDeclineSpy = sinon.spy();
-    wsMediaUpSpy = sinon.spy();
-
-    fakeWebsocket = {
-      cancel: wsCancelSpy,
-      close: wsCloseSpy,
-      decline: wsDeclineSpy,
-      mediaUp: wsMediaUpSpy
-    };
-
-    store = new loop.store.ConversationStore(dispatcher, {
-      client: client,
-      mozLoop: fakeMozLoop,
-      sdkDriver: sdkDriver
-    });
-    fakeSessionData = {
-      apiKey: "fakeKey",
-      callId: "142536",
-      sessionId: "321456",
-      sessionToken: "341256",
-      websocketToken: "543216",
-      windowId: "28",
-      progressURL: "fakeURL"
-    };
-
-    fakeStreamElement = { id: "fakeStreamElement" };
-
-    var dummySocket = {
-      close: sinon.spy(),
-      send: sinon.spy()
-    };
-
-    connectPromise = new Promise(function(resolve, reject) {
-      resolveConnectPromise = resolve;
-      rejectConnectPromise = reject;
-    });
-
-    sandbox.stub(loop.CallConnectionWebSocket.prototype,
-      "promiseConnect").returns(connectPromise);
-  });
-
-  afterEach(function() {
-    sandbox.restore();
-  });
-
-  describe("#initialize", function() {
-    it("should throw an error if the client is missing", function() {
-      expect(function() {
-        new loop.store.ConversationStore(dispatcher, {
-          sdkDriver: sdkDriver
-        });
-      }).to.Throw(/client/);
-    });
-
-    it("should throw an error if the sdkDriver is missing", function() {
-      expect(function() {
-        new loop.store.ConversationStore(dispatcher, {
-          client: client
-        });
-      }).to.Throw(/sdkDriver/);
-    });
-
-    it("should throw an error if mozLoop is missing", function() {
-      expect(function() {
-        new loop.store.ConversationStore(dispatcher, {
-          sdkDriver: sdkDriver,
-          client: client
-        });
-      }).to.Throw(/mozLoop/);
-    });
-  });
-
-  describe("#connectionFailure", function() {
-    beforeEach(function() {
-      store._websocket = fakeWebsocket;
-      store.setStoreState({windowId: "42"});
-    });
-
-    it("should disconnect the session", function() {
-      store.connectionFailure(
-        new sharedActions.ConnectionFailure({reason: "fake"}));
-
-      sinon.assert.calledOnce(sdkDriver.disconnectSession);
-    });
-
-    it("should ensure the websocket is closed", function() {
-      store.connectionFailure(
-        new sharedActions.ConnectionFailure({reason: "fake"}));
-
-      sinon.assert.calledOnce(wsCloseSpy);
-    });
-
-    it("should set the state to 'terminated'", function() {
-      store.setStoreState({callState: CALL_STATES.ALERTING});
-
-      store.connectionFailure(
-        new sharedActions.ConnectionFailure({reason: "fake"}));
-
-      expect(store.getStoreState("callState")).eql(CALL_STATES.TERMINATED);
-      expect(store.getStoreState("callStateReason")).eql("fake");
-    });
-
-    it("should release mozLoop callsData", function() {
-      store.connectionFailure(
-        new sharedActions.ConnectionFailure({reason: "fake"}));
-
-      sinon.assert.calledOnce(fakeMozLoop.calls.clearCallInProgress);
-      sinon.assert.calledWithExactly(
-        fakeMozLoop.calls.clearCallInProgress, "42");
-    });
-  });
-
-  describe("#connectionProgress", function() {
-    describe("progress: init", function() {
-      it("should change the state from 'gather' to 'connecting'", function() {
-        store.setStoreState({callState: CALL_STATES.GATHER});
-
-        store.connectionProgress(
-          new sharedActions.ConnectionProgress({wsState: WS_STATES.INIT}));
-
-        expect(store.getStoreState("callState")).eql(CALL_STATES.CONNECTING);
-      });
-    });
-
-    describe("progress: alerting", function() {
-      it("should change the state from 'gather' to 'alerting'", function() {
-        store.setStoreState({callState: CALL_STATES.GATHER});
-
-        store.connectionProgress(
-          new sharedActions.ConnectionProgress({wsState: WS_STATES.ALERTING}));
-
-        expect(store.getStoreState("callState")).eql(CALL_STATES.ALERTING);
-      });
-
-      it("should change the state from 'init' to 'alerting'", function() {
-        store.setStoreState({callState: CALL_STATES.INIT});
-
-        store.connectionProgress(
-          new sharedActions.ConnectionProgress({wsState: WS_STATES.ALERTING}));
-
-        expect(store.getStoreState("callState")).eql(CALL_STATES.ALERTING);
-      });
-    });
-
-    describe("progress: connecting (outgoing calls)", function() {
-      beforeEach(function() {
-        store.setStoreState({
-          callState: CALL_STATES.ALERTING,
-          outgoing: true
-        });
-      });
-
-      it("should change the state to 'ongoing'", function() {
-        store.connectionProgress(
-          new sharedActions.ConnectionProgress({wsState: WS_STATES.CONNECTING}));
-
-        expect(store.getStoreState("callState")).eql(CALL_STATES.ONGOING);
-      });
-
-      it("should connect the session", function() {
-        store.setStoreState(fakeSessionData);
-
-        store.connectionProgress(
-          new sharedActions.ConnectionProgress({wsState: WS_STATES.CONNECTING}));
-
-        sinon.assert.calledOnce(sdkDriver.connectSession);
-        sinon.assert.calledWithExactly(sdkDriver.connectSession, {
-          apiKey: "fakeKey",
-          sessionId: "321456",
-          sessionToken: "341256",
-          sendTwoWayMediaTelemetry: true
-        });
-      });
-
-      it("should call mozLoop.addConversationContext", function() {
-        store.setStoreState(fakeSessionData);
-
-        store.connectionProgress(
-          new sharedActions.ConnectionProgress({wsState: WS_STATES.CONNECTING}));
-
-        sinon.assert.calledOnce(fakeMozLoop.addConversationContext);
-        sinon.assert.calledWithExactly(fakeMozLoop.addConversationContext,
-                                       "28", "321456", "142536");
-      });
-    });
-
-    describe("progress: connecting (incoming calls)", function() {
-      beforeEach(function() {
-        store.setStoreState({
-          callState: CALL_STATES.ALERTING,
-          outgoing: false,
-          windowId: 42
-        });
-
-        sandbox.stub(console, "error");
-        store._websocket = fakeWebsocket;
-      });
-
-      it("should log an error", function() {
-        store.connectionProgress(
-          new sharedActions.ConnectionProgress({wsState: WS_STATES.CONNECTING}));
-
-        sinon.assert.calledOnce(console.error);
-      });
-
-      it("should call decline on the websocket", function() {
-        store.connectionProgress(
-          new sharedActions.ConnectionProgress({wsState: WS_STATES.CONNECTING}));
-
-        sinon.assert.calledOnce(fakeWebsocket.decline);
-      });
-
-      it("should close the websocket", function() {
-        store.connectionProgress(
-          new sharedActions.ConnectionProgress({wsState: WS_STATES.CONNECTING}));
-
-        sinon.assert.calledOnce(fakeWebsocket.close);
-      });
-
-      it("should clear the call in progress for the backend", function() {
-        store.connectionProgress(
-          new sharedActions.ConnectionProgress({wsState: WS_STATES.CONNECTING}));
-
-        sinon.assert.calledOnce(fakeMozLoop.calls.clearCallInProgress);
-        sinon.assert.calledWithExactly(fakeMozLoop.calls.clearCallInProgress, 42);
-      });
-
-      it("should set the call state to CLOSE", function() {
-        store.connectionProgress(
-          new sharedActions.ConnectionProgress({wsState: WS_STATES.CONNECTING}));
-
-        expect(store.getStoreState("callState")).eql(CALL_STATES.CLOSE);
-      });
-    });
-  });
-
-  describe("#setupWindowData", function() {
-    var fakeSetupWindowData;
-
-    beforeEach(function() {
-      store.setStoreState({callState: CALL_STATES.INIT});
-      fakeSetupWindowData = {
-        windowId: "123456",
-        type: "outgoing",
-        contact: contact,
-        callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO
-      };
-    });
-
-    it("should set the state to 'gather'", function() {
-      dispatcher.dispatch(
-        new sharedActions.SetupWindowData(fakeSetupWindowData));
-
-      expect(store.getStoreState("callState")).eql(CALL_STATES.GATHER);
-    });
-
-    it("should save the basic call information", function() {
-      dispatcher.dispatch(
-        new sharedActions.SetupWindowData(fakeSetupWindowData));
-
-      expect(store.getStoreState("windowId")).eql("123456");
-      expect(store.getStoreState("outgoing")).eql(true);
-    });
-
-    it("should save the basic information from the mozLoop api", function() {
-      dispatcher.dispatch(
-        new sharedActions.SetupWindowData(fakeSetupWindowData));
-
-      expect(store.getStoreState("contact")).eql(contact);
-      expect(store.getStoreState("callType"))
-        .eql(sharedUtils.CALL_TYPES.AUDIO_VIDEO);
-    });
-
-    describe("incoming calls", function() {
-      beforeEach(function() {
-        store.setStoreState({outgoing: false});
-      });
-
-      it("should initialize the websocket", function() {
-        sandbox.stub(loop, "CallConnectionWebSocket").returns({
-          promiseConnect: function() { return connectPromise; },
-          on: sinon.spy()
-        });
-
-        store.connectCall(
-          new sharedActions.ConnectCall({sessionData: fakeSessionData}));
-
-        sinon.assert.calledOnce(loop.CallConnectionWebSocket);
-        sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
-          url: "fakeURL",
-          callId: "142536",
-          websocketToken: "543216"
-        });
-      });
-
-      it("should connect the websocket to the server", function() {
-        store.connectCall(
-          new sharedActions.ConnectCall({sessionData: fakeSessionData}));
-
-        sinon.assert.calledOnce(store._websocket.promiseConnect);
-      });
-
-      describe("WebSocket connection result", function() {
-        beforeEach(function() {
-          store.connectCall(
-            new sharedActions.ConnectCall({sessionData: fakeSessionData}));
-
-          sandbox.stub(dispatcher, "dispatch");
-          // This is already covered by a test. Stub just prevents console msgs.
-          sandbox.stub(window.console, "error");
-        });
-
-        it("should dispatch a connection progress action on success", function(done) {
-          resolveConnectPromise(WS_STATES.INIT);
-
-          connectPromise.then(function() {
-            checkFailures(done, function() {
-              sinon.assert.calledOnce(dispatcher.dispatch);
-              sinon.assert.calledWithExactly(dispatcher.dispatch,
-                new sharedActions.ConnectionProgress({
-                  wsState: WS_STATES.INIT
-                }));
-            });
-          }, function() {
-            done(new Error("Promise should have been resolve, not rejected"));
-          });
-        });
-
-        it("should dispatch a connection failure action on failure", function(done) {
-          rejectConnectPromise();
-
-          connectPromise.then(function() {
-            done(new Error("Promise should have been rejected, not resolved"));
-          }, function() {
-            checkFailures(done, function() {
-              sinon.assert.calledOnce(dispatcher.dispatch);
-              sinon.assert.calledOnce(dispatcher.dispatch,
-                new sharedActions.ConnectionFailure({
-                  reason: "websocket-setup"
-                }));
-             });
-          });
-        });
-      });
-    });
-
-    describe("outgoing calls", function() {
-      it("should request the outgoing call data", function() {
-        dispatcher.dispatch(
-          new sharedActions.SetupWindowData(fakeSetupWindowData));
-
-        sinon.assert.calledOnce(client.setupOutgoingCall);
-        sinon.assert.calledWith(client.setupOutgoingCall,
-          ["fakeEmail"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
-      });
-
-      it("should include all email addresses in the call data", function() {
-        fakeSetupWindowData.contact = {
-          name: [ "Mr Smith" ],
-          email: [{
-            type: "home",
-            value: "fakeEmail",
-            pref: true
-          },
-          {
-            type: "work",
-            value: "emailFake",
-            pref: false
-          }]
-        };
-
-        dispatcher.dispatch(
-          new sharedActions.SetupWindowData(fakeSetupWindowData));
-
-        sinon.assert.calledOnce(client.setupOutgoingCall);
-        sinon.assert.calledWith(client.setupOutgoingCall,
-          ["fakeEmail", "emailFake"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
-      });
-
-      it("should include trim phone numbers for the call data", function() {
-        fakeSetupWindowData.contact = {
-          name: [ "Mr Smith" ],
-          tel: [{
-            type: "home",
-            value: "+44-5667+345 496(2335)45+ 456+",
-            pref: true
-          }]
-        };
-
-        dispatcher.dispatch(
-          new sharedActions.SetupWindowData(fakeSetupWindowData));
-
-        sinon.assert.calledOnce(client.setupOutgoingCall);
-        sinon.assert.calledWith(client.setupOutgoingCall,
-          ["+445667345496233545456"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
-      });
-
-      it("should include all email and telephone values in the call data", function() {
-        fakeSetupWindowData.contact = {
-          name: [ "Mr Smith" ],
-          email: [{
-            type: "home",
-            value: "fakeEmail",
-            pref: true
-          }, {
-            type: "work",
-            value: "emailFake",
-            pref: false
-          }],
-          tel: [{
-            type: "work",
-            value: "01234567890",
-            pref: false
-          }, {
-            type: "home",
-            value: "09876543210",
-            pref: false
-          }]
-        };
-
-        dispatcher.dispatch(
-          new sharedActions.SetupWindowData(fakeSetupWindowData));
-
-        sinon.assert.calledOnce(client.setupOutgoingCall);
-        sinon.assert.calledWith(client.setupOutgoingCall,
-          ["fakeEmail", "emailFake", "01234567890", "09876543210"],
-          sharedUtils.CALL_TYPES.AUDIO_VIDEO);
-      });
-
-      describe("server response handling", function() {
-        beforeEach(function() {
-          sandbox.stub(dispatcher, "dispatch");
-          sandbox.stub(window.console, "error");
-        });
-
-        it("should dispatch a connect call action on success", function() {
-          var callData = {
-            apiKey: "fakeKey"
-          };
-
-          client.setupOutgoingCall.callsArgWith(2, null, callData);
-
-          store.setupWindowData(
-            new sharedActions.SetupWindowData(fakeSetupWindowData));
-
-          sinon.assert.calledOnce(dispatcher.dispatch);
-          // Can't use instanceof here, as that matches any action
-          sinon.assert.calledWithMatch(dispatcher.dispatch,
-            sinon.match.hasOwn("name", "connectCall"));
-          sinon.assert.calledWithMatch(dispatcher.dispatch,
-            sinon.match.hasOwn("sessionData", callData));
-        });
-
-        it("should dispatch a connection failure action on failure", function() {
-          client.setupOutgoingCall.callsArgWith(2, {});
-
-          store.setupWindowData(
-            new sharedActions.SetupWindowData(fakeSetupWindowData));
-
-          sinon.assert.calledOnce(dispatcher.dispatch);
-          // Can't use instanceof here, as that matches any action
-          sinon.assert.calledWithMatch(dispatcher.dispatch,
-            sinon.match.hasOwn("name", "connectionFailure"));
-          sinon.assert.calledWithMatch(dispatcher.dispatch,
-            sinon.match.hasOwn("reason", FAILURE_DETAILS.UNKNOWN));
-        });
-
-        it("should dispatch a connection failure action on failure with user unavailable", function() {
-          client.setupOutgoingCall.callsArgWith(2, {
-            errno: REST_ERRNOS.USER_UNAVAILABLE
-          });
-
-          store.setupWindowData(
-            new sharedActions.SetupWindowData(fakeSetupWindowData));
-
-          sinon.assert.calledOnce(dispatcher.dispatch);
-          // Can't use instanceof here, as that matches any action
-          sinon.assert.calledWithMatch(dispatcher.dispatch,
-            sinon.match.hasOwn("name", "connectionFailure"));
-          sinon.assert.calledWithMatch(dispatcher.dispatch,
-            sinon.match.hasOwn("reason", FAILURE_DETAILS.USER_UNAVAILABLE));
-        });
-      });
-    });
-  });
-
-  describe("#acceptCall", function() {
-    beforeEach(function() {
-      store._websocket = {
-        accept: sinon.stub()
-      };
-    });
-
-    it("should save the call type", function() {
-      store.acceptCall(
-        new sharedActions.AcceptCall({callType: CALL_TYPES.AUDIO_ONLY}));
-
-      expect(store.getStoreState("callType")).eql(CALL_TYPES.AUDIO_ONLY);
-      expect(store.getStoreState("videoMuted")).eql(true);
-    });
-
-    it("should call accept on the websocket", function() {
-      store.acceptCall(
-        new sharedActions.AcceptCall({callType: CALL_TYPES.AUDIO_ONLY}));
-
-      sinon.assert.calledOnce(store._websocket.accept);
-    });
-
-    it("should change the state to 'ongoing'", function() {
-      store.acceptCall(
-        new sharedActions.AcceptCall({callType: CALL_TYPES.AUDIO_ONLY}));
-
-      expect(store.getStoreState("callState")).eql(CALL_STATES.ONGOING);
-    });
-
-    it("should connect the session with sendTwoWayMediaTelemetry set as falsy", function() {
-      store.setStoreState(fakeSessionData);
-
-      store.acceptCall(
-        new sharedActions.AcceptCall({callType: CALL_TYPES.AUDIO_ONLY}));
-
-      sinon.assert.calledOnce(sdkDriver.connectSession);
-      sinon.assert.calledWithExactly(sdkDriver.connectSession, {
-        apiKey: "fakeKey",
-        sessionId: "321456",
-        sessionToken: "341256",
-        sendTwoWayMediaTelemetry: undefined
-      });
-    });
-
-    it("should call mozLoop.addConversationContext", function() {
-      store.setStoreState(fakeSessionData);
-
-      store.acceptCall(
-        new sharedActions.AcceptCall({callType: CALL_TYPES.AUDIO_ONLY}));
-
-      sinon.assert.calledOnce(fakeMozLoop.addConversationContext);
-      sinon.assert.calledWithExactly(fakeMozLoop.addConversationContext,
-                                     "28", "321456", "142536");
-    });
-  });
-
-  describe("#declineCall", function() {
-    beforeEach(function() {
-      store._websocket = fakeWebsocket;
-
-      store.setStoreState({windowId: 42});
-    });
-
-    it("should block the caller if necessary", function() {
-      store.declineCall(new sharedActions.DeclineCall({blockCaller: true}));
-
-      sinon.assert.calledOnce(fakeMozLoop.calls.blockDirectCaller);
-    });
-
-    it("should call decline on the websocket", function() {
-      store.declineCall(new sharedActions.DeclineCall({blockCaller: false}));
-
-      sinon.assert.calledOnce(fakeWebsocket.decline);
-    });
-
-    it("should close the websocket", function() {
-      store.declineCall(new sharedActions.DeclineCall({blockCaller: false}));
-
-      sinon.assert.calledOnce(fakeWebsocket.close);
-    });
-
-    it("should clear the call in progress for the backend", function() {
-      store.declineCall(new sharedActions.DeclineCall({blockCaller: false}));
-
-      sinon.assert.calledOnce(fakeMozLoop.calls.clearCallInProgress);
-      sinon.assert.calledWithExactly(fakeMozLoop.calls.clearCallInProgress, 42);
-    });
-
-    it("should set the call state to CLOSE", function() {
-      store.declineCall(new sharedActions.DeclineCall({blockCaller: false}));
-
-      expect(store.getStoreState("callState")).eql(CALL_STATES.CLOSE);
-    });
-  });
-
-  describe("#connectCall", function() {
-    it("should save the call session data", function() {
-      store.connectCall(
-        new sharedActions.ConnectCall({sessionData: fakeSessionData}));
-
-      expect(store.getStoreState("apiKey")).eql("fakeKey");
-      expect(store.getStoreState("callId")).eql("142536");
-      expect(store.getStoreState("sessionId")).eql("321456");
-      expect(store.getStoreState("sessionToken")).eql("341256");
-      expect(store.getStoreState("websocketToken")).eql("543216");
-      expect(store.getStoreState("progressURL")).eql("fakeURL");
-    });
-
-    it("should initialize the websocket", function() {
-      sandbox.stub(loop, "CallConnectionWebSocket").returns({
-        promiseConnect: function() { return connectPromise; },
-        on: sinon.spy()
-      });
-
-      store.connectCall(
-        new sharedActions.ConnectCall({sessionData: fakeSessionData}));
-
-      sinon.assert.calledOnce(loop.CallConnectionWebSocket);
-      sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
-        url: "fakeURL",
-        callId: "142536",
-        websocketToken: "543216"
-      });
-    });
-
-    it("should connect the websocket to the server", function() {
-      store.connectCall(
-        new sharedActions.ConnectCall({sessionData: fakeSessionData}));
-
-      sinon.assert.calledOnce(store._websocket.promiseConnect);
-    });
-
-    describe("WebSocket connection result", function() {
-      beforeEach(function() {
-        sandbox.stub(window.console, "error");
-        store.connectCall(
-          new sharedActions.ConnectCall({sessionData: fakeSessionData}));
-
-        sandbox.stub(dispatcher, "dispatch");
-      });
-
-      it("should dispatch a connection progress action on success", function(done) {
-        resolveConnectPromise(WS_STATES.INIT);
-
-        connectPromise.then(function() {
-          checkFailures(done, function() {
-            sinon.assert.calledOnce(dispatcher.dispatch);
-            // Can't use instanceof here, as that matches any action
-            sinon.assert.calledWithMatch(dispatcher.dispatch,
-              sinon.match.hasOwn("name", "connectionProgress"));
-            sinon.assert.calledWithMatch(dispatcher.dispatch,
-              sinon.match.hasOwn("wsState", WS_STATES.INIT));
-          });
-        }, function() {
-          done(new Error("Promise should have been resolve, not rejected"));
-        });
-      });
-
-      it("should log an error when connection fails", function(done) {
-        rejectConnectPromise();
-
-        connectPromise.then(function() {
-          done(new Error("Promise not reject"));
-        }, function() {
-          checkFailures(done, function() {
-            sinon.assert.calledOnce(console.error);
-          });
-        });
-      });
-
-      it("should dispatch a connection failure action on failure", function(done) {
-        rejectConnectPromise();
-
-        connectPromise.then(function() {
-          done(new Error("Promise should have been rejected, not resolved"));
-        }, function() {
-          checkFailures(done, function() {
-            sinon.assert.calledOnce(dispatcher.dispatch);
-            // Can't use instanceof here, as that matches any action
-            sinon.assert.calledWithMatch(dispatcher.dispatch,
-              sinon.match.hasOwn("name", "connectionFailure"));
-            sinon.assert.calledWithMatch(dispatcher.dispatch,
-              sinon.match.hasOwn("reason", "websocket-setup"));
-           });
-        });
-      });
-    });
-  });
-
-  describe("#hangupCall", function() {
-    var wsMediaFailSpy, wsHangupSpy;
-    beforeEach(function() {
-      wsMediaFailSpy = sinon.spy();
-      wsHangupSpy = sinon.spy();
-
-      store._websocket = {
-        mediaFail: wsMediaFailSpy,
-        close: wsHangupSpy
-      };
-      store.setStoreState({callState: CALL_STATES.ONGOING});
-      store.setStoreState({windowId: "42"});
-    });
-
-    it("should disconnect the session", function() {
-      store.hangupCall(new sharedActions.HangupCall());
-
-      sinon.assert.calledOnce(sdkDriver.disconnectSession);
-    });
-
-    it("should send a media-fail message to the websocket if it is open", function() {
-      store.hangupCall(new sharedActions.HangupCall());
-
-      sinon.assert.calledOnce(wsMediaFailSpy);
-    });
-
-    it("should ensure the websocket is closed", function() {
-      store.hangupCall(new sharedActions.HangupCall());
-
-      sinon.assert.calledOnce(wsHangupSpy);
-    });
-
-    it("should set the callState to finished for ongoing call state", function() {
-      store.hangupCall(new sharedActions.HangupCall());
-
-      expect(store.getStoreState("callState")).eql(CALL_STATES.FINISHED);
-    });
-
-    it("should set the callState to CLOSE for non-ongoing call state", function() {
-      store.setStoreState({
-        callState: CALL_STATES.CONNECTING
-      });
-
-      store.hangupCall(new sharedActions.HangupCall());
-
-      expect(store.getStoreState("callState")).eql(CALL_STATES.CLOSE);
-    });
-
-    it("should release mozLoop callsData", function() {
-      store.hangupCall(new sharedActions.HangupCall());
-
-      sinon.assert.calledOnce(fakeMozLoop.calls.clearCallInProgress);
-      sinon.assert.calledWithExactly(
-        fakeMozLoop.calls.clearCallInProgress, "42");
-    });
-  });
-
-  describe("#remotePeerDisconnected", function() {
-    var wsMediaFailSpy, wsDisconnectSpy;
-    beforeEach(function() {
-      wsMediaFailSpy = sinon.spy();
-      wsDisconnectSpy = sinon.spy();
-
-      store._websocket = {
-        mediaFail: wsMediaFailSpy,
-        close: wsDisconnectSpy
-      };
-      store.setStoreState({callState: CALL_STATES.ONGOING});
-      store.setStoreState({windowId: "42"});
-    });
-
-    it("should disconnect the session", function() {
-      store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
-        peerHungup: true
-      }));
-
-      sinon.assert.calledOnce(sdkDriver.disconnectSession);
-    });
-
-    it("should ensure the websocket is closed", function() {
-      store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
-        peerHungup: true
-      }));
-
-      sinon.assert.calledOnce(wsDisconnectSpy);
-    });
-
-    it("should release mozLoop callsData", function() {
-      store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
-        peerHungup: true
-      }));
-
-      sinon.assert.calledOnce(fakeMozLoop.calls.clearCallInProgress);
-      sinon.assert.calledWithExactly(
-        fakeMozLoop.calls.clearCallInProgress, "42");
-    });
-
-    it("should set the callState to finished if the peer hungup", function() {
-      store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
-        peerHungup: true
-      }));
-
-      expect(store.getStoreState("callState")).eql(CALL_STATES.FINISHED);
-    });
-
-    it("should set the callState to terminated if the peer was disconnected" +
-      "for an unintentional reason", function() {
-        store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
-          peerHungup: false
-        }));
-
-        expect(store.getStoreState("callState")).eql(CALL_STATES.TERMINATED);
-      });
-
-    it("should set the reason to peerNetworkDisconnected if the peer was" +
-      "disconnected for an unintentional reason", function() {
-        store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
-          peerHungup: false
-        }));
-
-        expect(store.getStoreState("callStateReason"))
-          .eql("peerNetworkDisconnected");
-    });
-  });
-
-  describe("#cancelCall", function() {
-    beforeEach(function() {
-      store._websocket = fakeWebsocket;
-
-      store.setStoreState({callState: CALL_STATES.CONNECTING});
-      store.setStoreState({windowId: "42"});
-    });
-
-    it("should disconnect the session", function() {
-      store.cancelCall(new sharedActions.CancelCall());
-
-      sinon.assert.calledOnce(sdkDriver.disconnectSession);
-    });
-
-    it("should send a cancel message to the websocket if it is open for outgoing calls", function() {
-      store.setStoreState({outgoing: true});
-
-      store.cancelCall(new sharedActions.CancelCall());
-
-      sinon.assert.calledOnce(wsCancelSpy);
-    });
-
-    it("should ensure the websocket is closed", function() {
-      store.cancelCall(new sharedActions.CancelCall());
-
-      sinon.assert.calledOnce(wsCloseSpy);
-    });
-
-    it("should set the state to close if the call is connecting", function() {
-      store.cancelCall(new sharedActions.CancelCall());
-
-      expect(store.getStoreState("callState")).eql(CALL_STATES.CLOSE);
-    });
-
-    it("should set the state to close if the call has terminated already", function() {
-      store.setStoreState({callState: CALL_STATES.TERMINATED});
-
-      store.cancelCall(new sharedActions.CancelCall());
-
-      expect(store.getStoreState("callState")).eql(CALL_STATES.CLOSE);
-    });
-
-    it("should release mozLoop callsData", function() {
-      store.cancelCall(new sharedActions.CancelCall());
-
-      sinon.assert.calledOnce(fakeMozLoop.calls.clearCallInProgress);
-      sinon.assert.calledWithExactly(
-        fakeMozLoop.calls.clearCallInProgress, "42");
-    });
-  });
-
-  describe("#retryCall", function() {
-    it("should set the state to gather", function() {
-      store.setStoreState({callState: CALL_STATES.TERMINATED});
-
-      store.retryCall(new sharedActions.RetryCall());
-
-      expect(store.getStoreState("callState"))
-        .eql(CALL_STATES.GATHER);
-    });
-
-    it("should request the outgoing call data", function() {
-      store.setStoreState({
-        callState: CALL_STATES.TERMINATED,
-        outgoing: true,
-        callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO,
-        contact: contact
-      });
-
-      store.retryCall(new sharedActions.RetryCall());
-
-      sinon.assert.calledOnce(client.setupOutgoingCall);
-      sinon.assert.calledWith(client.setupOutgoingCall,
-        ["fakeEmail"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
-    });
-  });
-
-  describe("#mediaConnected", function() {
-    it("should send mediaUp via the websocket", function() {
-      store._websocket = fakeWebsocket;
-
-      store.mediaConnected(new sharedActions.MediaConnected());
-
-      sinon.assert.calledOnce(wsMediaUpSpy);
-    });
-
-    it("should set store.mediaConnected to true", function () {
-      store._websocket = fakeWebsocket;
-
-      store.mediaConnected(new sharedActions.MediaConnected());
-
-      expect(store.getStoreState("mediaConnected")).eql(true);
-    });
-  });
-
-  describe("#mediaStreamCreated", function() {
-    it("should add a local video object to the store", function() {
-      expect(store.getStoreState()).to.not.have.property("localSrcMediaElement");
-
-      store.mediaStreamCreated(new sharedActions.MediaStreamCreated({
-        hasVideo: false,
-        isLocal: true,
-        srcMediaElement: fakeStreamElement
-      }));
-
-      expect(store.getStoreState().localSrcMediaElement).eql(fakeStreamElement);
-      expect(store.getStoreState()).to.not.have.property("remoteSrcMediaElement");
-    });
-
-    it("should set the local video enabled", function() {
-      store.setStoreState({
-        localVideoEnabled: false,
-        remoteVideoEnabled: false
-      });
-
-      store.mediaStreamCreated(new sharedActions.MediaStreamCreated({
-        hasVideo: true,
-        isLocal: true,
-        srcMediaElement: fakeStreamElement
-      }));
-
-      expect(store.getStoreState().localVideoEnabled).eql(true);
-      expect(store.getStoreState().remoteVideoEnabled).eql(false);
-    });
-
-    it("should add a remote video object to the store", function() {
-      expect(store.getStoreState()).to.not.have.property("remoteSrcMediaElement");
-
-      store.mediaStreamCreated(new sharedActions.MediaStreamCreated({
-        hasVideo: false,
-        isLocal: false,
-        srcMediaElement: fakeStreamElement
-      }));
-
-      expect(store.getStoreState()).not.have.property("localSrcMediaElement");
-      expect(store.getStoreState().remoteSrcMediaElement).eql(fakeStreamElement);
-    });
-
-    it("should set the remote video enabled", function() {
-      store.setStoreState({
-        localVideoEnabled: false,
-        remoteVideoEnabled: false
-      });
-
-      store.mediaStreamCreated(new sharedActions.MediaStreamCreated({
-        hasVideo: true,
-        isLocal: false,
-        srcMediaElement: fakeStreamElement
-      }));
-
-      expect(store.getStoreState().localVideoEnabled).eql(false);
-      expect(store.getStoreState().remoteVideoEnabled).eql(true);
-    });
-  });
-
-  describe("#mediaStreamDestroyed", function() {
-    beforeEach(function() {
-      store.setStoreState({
-        localSrcMediaElement: fakeStreamElement,
-        remoteSrcMediaElement: fakeStreamElement
-      });
-    });
-
-    it("should clear the local video object", function() {
-      store.mediaStreamDestroyed(new sharedActions.MediaStreamDestroyed({
-        isLocal: true
-      }));
-
-      expect(store.getStoreState().localSrcMediaElement).eql(null);
-      expect(store.getStoreState().remoteSrcMediaElement).eql(fakeStreamElement);
-    });
-
-    it("should clear the remote video object", function() {
-      store.mediaStreamDestroyed(new sharedActions.MediaStreamDestroyed({
-        isLocal: false
-      }));
-
-      expect(store.getStoreState().localSrcMediaElement).eql(fakeStreamElement);
-      expect(store.getStoreState().remoteSrcMediaElement).eql(null);
-    });
-  });
-
-  describe("#remoteVideoStatus", function() {
-    it("should set remoteVideoEnabled to true", function() {
-      store.setStoreState({
-        remoteVideoEnabled: false
-      });
-
-      store.remoteVideoStatus(new sharedActions.RemoteVideoStatus({
-        videoEnabled: true
-      }));
-
-      expect(store.getStoreState().remoteVideoEnabled).eql(true);
-    });
-
-    it("should set remoteVideoEnabled to false", function() {
-      store.setStoreState({
-        remoteVideoEnabled: true
-      });
-
-      store.remoteVideoStatus(new sharedActions.RemoteVideoStatus({
-        videoEnabled: false
-      }));
-
-      expect(store.getStoreState().remoteVideoEnabled).eql(false);
-    });
-  });
-
-  describe("#setMute", function() {
-    beforeEach(function() {
-      dispatcher.dispatch(
-        // Setup store to prevent console warnings.
-        new sharedActions.SetupWindowData({
-          windowId: "123456",
-          type: "outgoing",
-          contact: contact,
-          callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO
-        }));
-    });
-
-    it("should save the mute state for the audio stream", function() {
-      store.setStoreState({"audioMuted": false});
-
-      dispatcher.dispatch(new sharedActions.SetMute({
-        type: "audio",
-        enabled: true
-      }));
-
-      expect(store.getStoreState("audioMuted")).eql(false);
-    });
-
-    it("should save the mute state for the video stream", function() {
-      store.setStoreState({"videoMuted": true});
-
-      dispatcher.dispatch(new sharedActions.SetMute({
-        type: "video",
-        enabled: false
-      }));
-
-      expect(store.getStoreState("videoMuted")).eql(true);
-    });
-  });
-
-  describe("#fetchRoomEmailLink", function() {
-    it("should request a new call url to the server", function() {
-      store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
-        roomName: "FakeRoomName"
-      }));
-
-      sinon.assert.calledOnce(fakeMozLoop.rooms.create);
-      sinon.assert.calledWithMatch(fakeMozLoop.rooms.create, {
-        decryptedContext: {
-          roomName: "FakeRoomName"
-        }
-      });
-    });
-
-    it("should update the emailLink attribute when the new room url is received",
-      function() {
-        fakeMozLoop.rooms.create = function(roomData, cb) {
-          cb(null, {roomUrl: "http://fake.invalid/"});
-        };
-        store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
-          roomName: "FakeRoomName"
-        }));
-
-        expect(store.getStoreState("emailLink")).eql("http://fake.invalid/");
-        sinon.assert.calledOnce(fakeMozLoop.telemetryAddValue);
-        sinon.assert.calledWithExactly(fakeMozLoop.telemetryAddValue, "LOOP_ROOM_CREATE", 0);
-      });
-
-    it("should trigger an error:emailLink event in case of failure",
-      function() {
-        var trigger = sandbox.stub(store, "trigger");
-        fakeMozLoop.rooms.create = function(roomData, cb) {
-          cb(new Error("error"));
-        };
-        store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
-          roomName: "FakeRoomName"
-        }));
-
-        sinon.assert.calledOnce(trigger);
-        sinon.assert.calledWithExactly(trigger, "error:emailLink");
-        sinon.assert.calledOnce(fakeMozLoop.telemetryAddValue);
-        sinon.assert.calledWithExactly(fakeMozLoop.telemetryAddValue, "LOOP_ROOM_CREATE", 1);
-      });
-  });
-
-  describe("#windowUnload", function() {
-    var fakeWs;
-
-    beforeEach(function() {
-      fakeWs = store._websocket = {
-        close: sinon.stub(),
-        decline: sinon.stub()
-      };
-
-      store.setStoreState({windowId: 42});
-    });
-
-    it("should decline the connection on the websocket for incoming calls if the state is alerting", function() {
-      store.setStoreState({
-        callState: CALL_STATES.ALERTING,
-        outgoing: false
-      });
-
-      store.windowUnload();
-
-      sinon.assert.calledOnce(fakeWs.decline);
-    });
-
-    it("should disconnect the sdk session", function() {
-      store.windowUnload();
-
-      sinon.assert.calledOnce(sdkDriver.disconnectSession);
-    });
-
-    it("should close the websocket", function() {
-      store.windowUnload();
-
-      sinon.assert.calledOnce(fakeWs.close);
-    });
-
-    it("should clear the call in progress for the backend", function() {
-      store.windowUnload();
-
-      sinon.assert.calledOnce(fakeMozLoop.calls.clearCallInProgress);
-      sinon.assert.calledWithExactly(fakeMozLoop.calls.clearCallInProgress, 42);
-    });
-  });
-
-  describe("Events", function() {
-    describe("Websocket progress", function() {
-      beforeEach(function() {
-        store.connectCall(
-          new sharedActions.ConnectCall({sessionData: fakeSessionData}));
-
-        sandbox.stub(dispatcher, "dispatch");
-      });
-
-      it("should dispatch a connection failure action on 'terminate' for incoming calls if the previous state was not 'alerting' or 'init'", function() {
-        store.setStoreState({
-          outgoing: false
-        });
-
-        store._websocket.trigger("progress", {
-          state: WS_STATES.TERMINATED,
-          reason: WEBSOCKET_REASONS.CANCEL
-        }, WS_STATES.CONNECTING);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.ConnectionFailure({
-            reason: WEBSOCKET_REASONS.CANCEL
-          }));
-      });
-
-      it("should dispatch a cancel call action on 'terminate' for incoming calls if the previous state was 'init'", function() {
-        store.setStoreState({
-          outgoing: false
-        });
-
-        store._websocket.trigger("progress", {
-          state: WS_STATES.TERMINATED,
-          reason: WEBSOCKET_REASONS.CANCEL
-        }, WS_STATES.INIT);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.CancelCall({}));
-      });
-
-      it("should dispatch a cancel call action on 'terminate' for incoming calls if the previous state was 'alerting'", function() {
-        store.setStoreState({
-          outgoing: false
-        });
-
-        store._websocket.trigger("progress", {
-          state: WS_STATES.TERMINATED,
-          reason: WEBSOCKET_REASONS.CANCEL
-        }, WS_STATES.ALERTING);
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.CancelCall({}));
-      });
-
-      it("should dispatch a connection progress action on 'alerting'", function() {
-        store._websocket.trigger("progress", {state: WS_STATES.ALERTING});
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.ConnectionProgress({
-            wsState: WS_STATES.ALERTING
-          }));
-      });
-
-      describe("Outgoing Calls, handling 'terminate'", function() {
-        beforeEach(function() {
-          store.setStoreState({
-            outgoing: true
-          });
-        });
-
-        it("should dispatch a connection failure action", function() {
-          store._websocket.trigger("progress", {
-            state: WS_STATES.TERMINATED,
-            reason: WEBSOCKET_REASONS.MEDIA_FAIL
-          });
-
-          sinon.assert.calledOnce(dispatcher.dispatch);
-          sinon.assert.calledWithExactly(dispatcher.dispatch,
-            new sharedActions.ConnectionFailure({
-              reason: WEBSOCKET_REASONS.MEDIA_FAIL
-            }));
-        });
-
-        it("should dispatch an action with user unavailable if the websocket reports busy", function() {
-          store._websocket.trigger("progress", {
-            state: WS_STATES.TERMINATED,
-            reason: WEBSOCKET_REASONS.BUSY
-          });
-
-          sinon.assert.calledOnce(dispatcher.dispatch);
-          sinon.assert.calledWithExactly(dispatcher.dispatch,
-            new sharedActions.ConnectionFailure({
-              reason: FAILURE_DETAILS.USER_UNAVAILABLE
-            }));
-        });
-
-        it("should dispatch an action with user unavailable if the websocket reports reject", function() {
-          store._websocket.trigger("progress", {
-            state: WS_STATES.TERMINATED,
-            reason: WEBSOCKET_REASONS.REJECT
-          });
-
-          sinon.assert.calledOnce(dispatcher.dispatch);
-          sinon.assert.calledWithExactly(dispatcher.dispatch,
-            new sharedActions.ConnectionFailure({
-              reason: FAILURE_DETAILS.USER_UNAVAILABLE
-            }));
-        });
-      });
-    });
-  });
-});
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -45,42 +45,38 @@
     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/crypto.js"></script>
-  <script src="../../content/shared/js/websocket.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/activeRoomStore.js"></script>
-  <script src="../../content/shared/js/conversationStore.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/urlRegExps.js"></script>
   <script src="../../content/shared/js/linkifiedTextView.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="validate_test.js"></script>
   <script src="dispatcher_test.js"></script>
   <script src="activeRoomStore_test.js"></script>
-  <script src="conversationStore_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 src="linkifiedTextView_test.js"></script>
 
   <script>
     describe("Uncaught Error Check", function() {
deleted file mode 100644
--- a/browser/components/loop/test/shared/websocket_test.js
+++ /dev/null
@@ -1,340 +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.CallConnectionWebSocket", function() {
-  "use strict";
-
-  var expect = chai.expect;
-  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
-
-  var sandbox,
-      dummySocket;
-
-  beforeEach(function() {
-    sandbox = sinon.sandbox.create();
-    sandbox.useFakeTimers();
-
-    dummySocket = {
-      close: sinon.spy(),
-      send: sinon.spy()
-    };
-    sandbox.stub(window, "WebSocket").returns(dummySocket);
-  });
-
-  afterEach(function() {
-    sandbox.restore();
-  });
-
-  describe("#constructor", function() {
-    it("should require a url option", function() {
-      expect(function() {
-        return new loop.CallConnectionWebSocket();
-      }).to.Throw(/No url/);
-    });
-
-    it("should require a callId setting", function() {
-      expect(function() {
-        return new loop.CallConnectionWebSocket({url: "wss://fake/"});
-      }).to.Throw(/No callId/);
-    });
-
-    it("should require a websocketToken setting", function() {
-      expect(function() {
-        return new loop.CallConnectionWebSocket({
-          url: "http://fake/",
-          callId: "hello"
-        });
-      }).to.Throw(/No websocketToken/);
-    });
-  });
-
-  describe("constructed", function() {
-    var callWebSocket, fakeUrl, fakeCallId, fakeWebSocketToken;
-
-    beforeEach(function() {
-      fakeUrl = "wss://fake/";
-      fakeCallId = "callId";
-      fakeWebSocketToken = "7b";
-
-      callWebSocket = new loop.CallConnectionWebSocket({
-        url: fakeUrl,
-        callId: fakeCallId,
-        websocketToken: fakeWebSocketToken
-      });
-    });
-
-    describe("#promiseConnect", function() {
-      it("should create a new websocket connection", function() {
-        callWebSocket.promiseConnect();
-
-        sinon.assert.calledOnce(window.WebSocket);
-        sinon.assert.calledWithExactly(window.WebSocket, fakeUrl);
-      });
-
-      it("should reject the promise if connection is not completed in " +
-         "5 seconds", function(done) {
-        var promise = callWebSocket.promiseConnect();
-
-        sandbox.clock.tick(5101);
-
-        promise.then(function() {}, function(error) {
-          expect(error).to.be.equal(WEBSOCKET_REASONS.TIMEOUT);
-          done();
-        });
-      });
-
-      it("should reject the promise if the connection errors", function(done) {
-        var promise = callWebSocket.promiseConnect();
-
-        dummySocket.onerror("error");
-
-        promise.then(function() {}, function(error) {
-          expect(error).to.be.equal("error");
-          done();
-        });
-      });
-
-      it("should reject the promise if the connection closes", function(done) {
-        var promise = callWebSocket.promiseConnect();
-
-        dummySocket.onclose("close");
-
-        promise.then(function() {}, function(error) {
-          expect(error).to.be.equal("close");
-          done();
-        });
-      });
-
-      it("should send hello when the socket is opened", function() {
-        callWebSocket.promiseConnect();
-
-        dummySocket.onopen();
-
-        sinon.assert.calledOnce(dummySocket.send);
-        sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
-          messageType: "hello",
-          callId: fakeCallId,
-          auth: fakeWebSocketToken
-        }));
-      });
-
-      it("should resolve the promise when the 'hello' is received",
-        function(done) {
-          var promise = callWebSocket.promiseConnect();
-
-          dummySocket.onmessage({
-            data: '{"messageType":"hello", "state":"init"}'
-          });
-
-          promise.then(function(state) {
-            expect(state).eql("init");
-            done();
-          }, function() {
-            done(new Error("shouldn't have rejected the promise"));
-          });
-        });
-    });
-
-    describe("#close", function() {
-      it("should close the socket", function() {
-        callWebSocket.promiseConnect();
-
-        callWebSocket.close();
-
-        sinon.assert.calledOnce(dummySocket.close);
-      });
-    });
-
-    describe("#decline", function() {
-      it("should send a terminate message to the server", function() {
-        callWebSocket.promiseConnect();
-
-        callWebSocket.decline();
-
-        sinon.assert.calledOnce(dummySocket.send);
-        sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
-          messageType: "action",
-          event: "terminate",
-          reason: WEBSOCKET_REASONS.REJECT
-        }));
-      });
-    });
-
-    describe("#accept", function() {
-      it("should send an accept message to the server", function() {
-        callWebSocket.promiseConnect();
-
-        callWebSocket.accept();
-
-        sinon.assert.calledOnce(dummySocket.send);
-        sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
-          messageType: "action",
-          event: "accept"
-        }));
-      });
-    });
-
-    describe("#mediaUp", function() {
-      it("should send a media-up message to the server", function() {
-        callWebSocket.promiseConnect();
-
-        callWebSocket.mediaUp();
-
-        sinon.assert.calledOnce(dummySocket.send);
-        sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
-          messageType: "action",
-          event: "media-up"
-        }));
-      });
-    });
-
-    describe("#cancel", function() {
-      it("should send a terminate message to the server with a reason of WEBSOCKET_REASONS.CANCEL",
-        function() {
-          callWebSocket.promiseConnect();
-
-          callWebSocket.cancel();
-
-          sinon.assert.calledOnce(dummySocket.send);
-          sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
-            messageType: "action",
-            event: "terminate",
-            reason: WEBSOCKET_REASONS.CANCEL
-          }));
-        });
-    });
-
-    describe("#mediaFail", function() {
-      it("should send a terminate message to the server with a reason of WEBSOCKET_REASONS.MEDIA_FAIL",
-        function() {
-          callWebSocket.promiseConnect();
-
-          callWebSocket.mediaFail();
-
-          sinon.assert.calledOnce(dummySocket.send);
-          sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
-            messageType: "action",
-            event: "terminate",
-            reason: WEBSOCKET_REASONS.MEDIA_FAIL
-          }));
-        });
-    });
-
-    describe("Events", function() {
-      beforeEach(function() {
-        sandbox.stub(callWebSocket, "trigger");
-
-        callWebSocket.promiseConnect();
-      });
-
-      describe("Progress", function() {
-        it("should trigger a progress event on the callWebSocket", function() {
-          var eventData = {
-            messageType: "progress",
-            state: "terminate",
-            reason: WEBSOCKET_REASONS.REJECT
-          };
-
-          dummySocket.onmessage({
-            data: JSON.stringify(eventData)
-          });
-
-          sinon.assert.called(callWebSocket.trigger);
-          sinon.assert.calledWithExactly(callWebSocket.trigger, "progress",
-                                         eventData, "init");
-        });
-
-        it("should trigger a progress event with the previous state", function() {
-          var previousEventData = {
-            messageType: "progress",
-            state: "alerting"
-          };
-
-          // This first call is to set the previous state of the object
-          // ready for the main test below.
-          dummySocket.onmessage({
-            data: JSON.stringify(previousEventData)
-          });
-
-          var currentEventData = {
-            messageType: "progress",
-            state: "terminate",
-            reason: WEBSOCKET_REASONS.REJECT
-          };
-
-          dummySocket.onmessage({
-            data: JSON.stringify(currentEventData)
-          });
-
-          sinon.assert.called(callWebSocket.trigger);
-          sinon.assert.calledWithExactly(callWebSocket.trigger, "progress",
-                                         currentEventData, "alerting");
-        });
-
-        it("should trigger a progress:<state> event on the callWebSocket", function() {
-          var eventData = {
-            messageType: "progress",
-            state: "terminate",
-            reason: WEBSOCKET_REASONS.REJECT
-          };
-
-          dummySocket.onmessage({
-            data: JSON.stringify(eventData)
-          });
-
-          sinon.assert.called(callWebSocket.trigger);
-          sinon.assert.calledWithExactly(callWebSocket.trigger, "progress:terminate");
-        });
-      });
-
-      describe("Error", function() {
-        // Handled in constructed -> #promiseConnect:
-        //   should reject the promise if the connection errors
-
-        it("should trigger an error if state is not completed", function() {
-          callWebSocket._clearConnectionFlags();
-
-          dummySocket.onerror("Error");
-
-          sinon.assert.calledOnce(callWebSocket.trigger);
-          sinon.assert.calledWithExactly(callWebSocket.trigger,
-                                         "error", "Error");
-        });
-
-        it("should not trigger an error if state is completed", function() {
-          callWebSocket._clearConnectionFlags();
-          callWebSocket._lastServerState = "connected";
-
-          dummySocket.onerror("Error");
-
-          sinon.assert.notCalled(callWebSocket.trigger);
-        });
-      });
-
-      describe("Close", function() {
-        // Handled in constructed -> #promiseConnect:
-        //   should reject the promise if the connection closes
-
-        it("should trigger a close event if state is not completed", function() {
-          callWebSocket._clearConnectionFlags();
-
-          dummySocket.onclose("Error");
-
-          sinon.assert.calledOnce(callWebSocket.trigger);
-          sinon.assert.calledWithExactly(callWebSocket.trigger,
-                                         "closed", "Error");
-        });
-
-        it("should not trigger an error if state is completed", function() {
-          callWebSocket._clearConnectionFlags();
-          callWebSocket._lastServerState = "terminated";
-
-          dummySocket.onclose("Error");
-
-          sinon.assert.notCalled(callWebSocket.trigger);
-        });
-      });
-    });
-  });
-});
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -41,17 +41,16 @@
   <script src="../shared/sdk_mock.js"></script>
   <script>
     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/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/store.js"></script>
   <script src="../../content/shared/js/roomStates.js"></script>
   <script src="../../content/shared/js/activeRoomStore.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/textChatStore.js"></script>
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -31,32 +31,28 @@
     <script src="fake-l10n.js"></script>
     <script src="../content/shared/libs/react-0.12.2.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/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/activeRoomStore.js"></script>
     <script src="../content/shared/js/views.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/shared/js/urlRegExps.js"></script>
     <script src="../content/shared/js/linkifiedTextView.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/webapp.js"></script>
     <script src="../standalone/content/js/standaloneRoomViews.js"></script>
     <script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>
     <script>
       if (!loop.contacts) {
         // For browsers that don't support ES6 without special flags (all but Fx
         // at the moment), we shim the contacts namespace with its most barebone
         // implementation.
--- a/browser/components/loop/ui/ui-showcase.css
+++ b/browser/components/loop/ui/ui-showcase.css
@@ -122,22 +122,16 @@ body {
 .showcase .fx-embedded .local-stream {
   position: absolute;
 }
 
 .showcase p.note > strong {
   font-weight: bold;
 }
 
-.call-action-group .btn-group-chevron,
-.call-action-group .btn-group {
-  /* Prevent box overflow due to long string */
-  max-width: 120px;
-}
-
 .room-waiting-tile {
   background-color: grey;
 }
 /* SVG icons showcase */
 
 .svg-icons h3 {
   clear: left;
 }
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -18,20 +18,16 @@
   var AvailabilityDropdown = loop.panel.AvailabilityDropdown;
   var PanelView = loop.panel.PanelView;
   var SignInRequestView = loop.panel.SignInRequestView;
   var ContactDetailsForm = loop.contacts.ContactDetailsForm;
   var ContactDropdown = loop.contacts.ContactDropdown;
   var ContactDetail = loop.contacts.ContactDetail;
   var GettingStartedView = loop.panel.GettingStartedView;
   // 1.2. Conversation Window
-  var AcceptCallView = loop.conversationViews.AcceptCallView;
-  var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
-  var OngoingConversationView = loop.conversationViews.OngoingConversationView;
-  var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
   var DesktopRoomEditContextView = loop.roomViews.DesktopRoomEditContextView;
   var RoomFailureView = loop.roomViews.RoomFailureView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
@@ -333,73 +329,16 @@
     mozLoop: navigator.mozLoop,
     activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
   });
 
   var textChatStore = new loop.store.TextChatStore(dispatcher, {
     sdkDriver: mockSDK
   });
 
-  /**
-   * Every view that uses an conversationStore needs its own; if they shared
-   * a conversation store, they'd interfere with each other.
-   *
-   * @param options
-   * @returns {loop.store.ConversationStore}
-   */
-  function makeConversationStore() {
-    var roomDispatcher = new loop.Dispatcher();
-
-    var store = new loop.store.ConversationStore(dispatcher, {
-      client: {},
-      mozLoop: navigator.mozLoop,
-      sdkDriver: mockSDK
-    });
-
-    store.forcedUpdate = function forcedUpdate(contentWindow) {
-      // Since this is called by setTimeout, we don't want to lose any
-      // exceptions if there's a problem and we need to debug, so...
-      try {
-        var newStoreState = {
-          // Override the matchMedia, this is so that the correct version is
-          // used for the frame.
-          //
-          // Currently, we use an icky hack, and the showcase conspires with
-          // react-frame-component to set iframe.contentWindow.matchMedia onto
-          // the store. Once React context matures a bit (somewhere between
-          // 0.14 and 1.0, apparently):
-          //
-          // https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
-          //
-          // we should be able to use those to clean this up.
-          matchMedia: contentWindow.matchMedia.bind(contentWindow)
-        };
-
-        store.setStoreState(newStoreState);
-      } catch (ex) {
-        console.error("exception in forcedUpdate:", ex);
-      }
-    };
-
-    return store;
-  }
-
-  var conversationStores = [];
-  for (var index = 0; index < 5; index++) {
-    conversationStores[index] = makeConversationStore();
-  }
-
-  conversationStores[0].setStoreState({
-    callStateReason: FAILURE_DETAILS.NO_MEDIA
-  });
-  conversationStores[1].setStoreState({
-    callStateReason: FAILURE_DETAILS.USER_UNAVAILABLE,
-    contact: fakeManyContacts[0]
-  });
-
   // Update the text chat store with the room info.
   textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
     roomName: "A Very Long Conversation Name",
     roomUrl: "http://showcase",
     urls: [{
       description: "A wonderful page!",
       location: "http://wonderful.invalid"
       // use the fallback thumbnail
@@ -444,17 +383,16 @@
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.shared.utils.CHAT_CONTENT_TYPES.TEXT,
     message: "8:00 PM",
     receivedTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
-    conversationStore: conversationStores[0],
     textChatStore: textChatStore
   });
 
   // Local mocks
   var mockMozLoopNoRooms = _.cloneDeep(navigator.mozLoop);
   mockMozLoopNoRooms.rooms.getAll = function(version, callback) {
     callback(null, []);
   };
@@ -540,22 +478,16 @@
       value: "smith@invalid.com"
     }]
   };
 
   var mockClient = {
     requestCallUrlInfo: noop
   };
 
-  var mockWebSocket = new loop.CallConnectionWebSocket({
-    url: "fake",
-    callId: "fakeId",
-    websocketToken: "fakeToken"
-  });
-
   var notifications = new loop.shared.models.NotificationCollection();
   var errNotifications = new loop.shared.models.NotificationCollection();
   errNotifications.add({
     level: "error",
     message: "Could Not Authenticate",
     details: "Did you change your password?",
     detailsButtonLabel: "Retry"
   });
@@ -1055,57 +987,16 @@
                                 canEdit: false, 
                                 eventPosY: 0, 
                                 getContainerCoordinates: function() { return {"top": 0, "height": 0 }; }, 
                                 handleAction: function () {}})
              )
             )
           ), 
 
-          React.createElement(Section, {name: "AcceptCallView"}, 
-            React.createElement(FramedExample, {dashed: true, 
-                           height: 272, 
-                           summary: "Default / incoming video call", 
-                           width: 332}, 
-              React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(AcceptCallView, {callType: CALL_TYPES.AUDIO_VIDEO, 
-                                callerId: "Mr Smith", 
-                                dispatcher: dispatcher, 
-                                mozLoop: mockMozLoopLoggedIn})
-              )
-            ), 
-
-            React.createElement(FramedExample, {dashed: true, 
-                           height: 272, 
-                           summary: "Default / incoming audio only call", 
-                           width: 332}, 
-              React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(AcceptCallView, {callType: CALL_TYPES.AUDIO_ONLY, 
-                                callerId: "Mr Smith", 
-                                dispatcher: dispatcher, 
-                                mozLoop: mockMozLoopLoggedIn})
-              )
-            )
-          ), 
-
-          React.createElement(Section, {name: "AcceptCallView-ActiveState"}, 
-            React.createElement(FramedExample, {dashed: true, 
-                           height: 272, 
-                           summary: "Default", 
-                           width: 332}, 
-              React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(AcceptCallView, {callType: CALL_TYPES.AUDIO_VIDEO, 
-                                callerId: "Mr Smith", 
-                                dispatcher: dispatcher, 
-                                mozLoop: mockMozLoopLoggedIn, 
-                                showMenu: true})
-              )
-            )
-          ), 
-
           React.createElement(Section, {name: "ConversationToolbar"}, 
             React.createElement("div", null, 
               React.createElement(FramedExample, {dashed: true, 
                              height: 56, 
                              summary: "Default", 
                              width: 300}, 
                 React.createElement("div", {className: "fx-embedded"}, 
                   React.createElement(ConversationToolbar, {audio: { enabled: true, visible: true}, 
@@ -1146,169 +1037,16 @@
                                        settingsMenuItems: [{ id: "feedback" }], 
                                        show: true, 
                                        video: { enabled: true, visible: true}})
                 )
               )
             )
           ), 
 
-          React.createElement(Section, {name: "PendingConversationView (Desktop)"}, 
-            React.createElement(FramedExample, {dashed: true, 
-                           height: 272, 
-                           summary: "Connecting", 
-                           width: 300}, 
-              React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(DesktopPendingConversationView, {callState: "gather", 
-                                                contact: mockContact, 
-                                                dispatcher: dispatcher})
-              )
-            )
-          ), 
-
-          React.createElement(Section, {name: "DirectCallFailureView"}, 
-            React.createElement(FramedExample, {
-              dashed: true, 
-              height: 254, 
-              summary: "Call Failed - Incoming", 
-              width: 298}, 
-              React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(DirectCallFailureView, {
-                  conversationStore: conversationStores[0], 
-                  dispatcher: dispatcher, 
-                  mozLoop: navigator.mozLoop, 
-                  outgoing: false})
-              )
-            ), 
-            React.createElement(FramedExample, {
-              dashed: true, 
-              height: 254, 
-              summary: "Call Failed - Outgoing", 
-              width: 298}, 
-              React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(DirectCallFailureView, {
-                  conversationStore: conversationStores[1], 
-                  dispatcher: dispatcher, 
-                  mozLoop: navigator.mozLoop, 
-                  outgoing: true})
-              )
-            ), 
-            React.createElement(FramedExample, {
-              dashed: true, 
-              height: 254, 
-              summary: "Call Failed — with call URL error", 
-              width: 298}, 
-              React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(DirectCallFailureView, {
-                  conversationStore: conversationStores[0], 
-                  dispatcher: dispatcher, 
-                  emailLinkError: true, 
-                  mozLoop: navigator.mozLoop, 
-                  outgoing: true})
-              )
-            )
-          ), 
-
-          React.createElement(Section, {name: "OngoingConversationView"}, 
-            React.createElement(FramedExample, {dashed: true, 
-                           height: 398, 
-                           onContentsRendered: conversationStores[0].forcedUpdate, 
-                           summary: "Desktop ongoing conversation window", 
-                           width: 348}, 
-              React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(OngoingConversationView, {
-                  audio: { enabled: true, visible: true}, 
-                  chatWindowDetached: false, 
-                  conversationStore: conversationStores[0], 
-                  dispatcher: dispatcher, 
-                  localPosterUrl: "sample-img/video-screen-local.png", 
-                  mediaConnected: true, 
-                  remotePosterUrl: "sample-img/video-screen-remote.png", 
-                  remoteVideoEnabled: true, 
-                  video: { enabled: true, visible: true}})
-              )
-            ), 
-
-            React.createElement(FramedExample, {dashed: true, 
-                           height: 400, 
-                           onContentsRendered: conversationStores[1].forcedUpdate, 
-                           summary: "Desktop ongoing conversation window (medium)", 
-                           width: 600}, 
-              React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(OngoingConversationView, {
-                  audio: { enabled: true, visible: true}, 
-                  chatWindowDetached: false, 
-                  conversationStore: conversationStores[1], 
-                  dispatcher: dispatcher, 
-                  localPosterUrl: "sample-img/video-screen-local.png", 
-                  mediaConnected: true, 
-                  remotePosterUrl: "sample-img/video-screen-remote.png", 
-                  remoteVideoEnabled: true, 
-                  video: { enabled: true, visible: true}})
-              )
-            ), 
-
-            React.createElement(FramedExample, {height: 600, 
-                           onContentsRendered: conversationStores[2].forcedUpdate, 
-                           summary: "Desktop ongoing conversation window (large)", 
-                           width: 800}, 
-              React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(OngoingConversationView, {
-                  audio: { enabled: true, visible: true}, 
-                  chatWindowDetached: false, 
-                  conversationStore: conversationStores[2], 
-                  dispatcher: dispatcher, 
-                  localPosterUrl: "sample-img/video-screen-local.png", 
-                  mediaConnected: true, 
-                  remotePosterUrl: "sample-img/video-screen-remote.png", 
-                  remoteVideoEnabled: true, 
-                  video: { enabled: true, visible: true}})
-              )
-            ), 
-
-            React.createElement(FramedExample, {dashed: true, 
-                           height: 398, 
-                           onContentsRendered: conversationStores[3].forcedUpdate, 
-                           summary: "Desktop ongoing conversation window - local face mute", 
-                           width: 348}, 
-              React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(OngoingConversationView, {
-                  audio: { enabled: true, visible: true}, 
-                  chatWindowDetached: false, 
-                  conversationStore: conversationStores[3], 
-                  dispatcher: dispatcher, 
-                  localPosterUrl: "sample-img/video-screen-local.png", 
-                  mediaConnected: true, 
-                  remotePosterUrl: "sample-img/video-screen-remote.png", 
-                  remoteVideoEnabled: true, 
-                  video: { enabled: false, visible: true}})
-              )
-            ), 
-
-            React.createElement(FramedExample, {dashed: true, 
-                           height: 398, 
-                           onContentsRendered: conversationStores[4].forcedUpdate, 
-                           summary: "Desktop ongoing conversation window - remote face mute", 
-                           width: 348}, 
-              React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(OngoingConversationView, {
-                  audio: { enabled: true, visible: true}, 
-                  chatWindowDetached: false, 
-                  conversationStore: conversationStores[4], 
-                  dispatcher: dispatcher, 
-                  localPosterUrl: "sample-img/video-screen-local.png", 
-                  mediaConnected: true, 
-                  remotePosterUrl: "sample-img/video-screen-remote.png", 
-                  remoteVideoEnabled: false, 
-                  video: { enabled: true, visible: true}})
-              )
-            )
-
-          ), 
-
           React.createElement(Section, {name: "FeedbackView"}, 
             React.createElement("p", {className: "note"}
             ), 
             React.createElement(FramedExample, {dashed: true, 
                            height: 288, 
                            summary: "Default (useable demo)", 
                            width: 348}, 
               React.createElement("div", {className: "fx-embedded"}, 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -18,20 +18,16 @@
   var AvailabilityDropdown = loop.panel.AvailabilityDropdown;
   var PanelView = loop.panel.PanelView;
   var SignInRequestView = loop.panel.SignInRequestView;
   var ContactDetailsForm = loop.contacts.ContactDetailsForm;
   var ContactDropdown = loop.contacts.ContactDropdown;
   var ContactDetail = loop.contacts.ContactDetail;
   var GettingStartedView = loop.panel.GettingStartedView;
   // 1.2. Conversation Window
-  var AcceptCallView = loop.conversationViews.AcceptCallView;
-  var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
-  var OngoingConversationView = loop.conversationViews.OngoingConversationView;
-  var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
   var DesktopRoomEditContextView = loop.roomViews.DesktopRoomEditContextView;
   var RoomFailureView = loop.roomViews.RoomFailureView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
@@ -333,73 +329,16 @@
     mozLoop: navigator.mozLoop,
     activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
   });
 
   var textChatStore = new loop.store.TextChatStore(dispatcher, {
     sdkDriver: mockSDK
   });
 
-  /**
-   * Every view that uses an conversationStore needs its own; if they shared
-   * a conversation store, they'd interfere with each other.
-   *
-   * @param options
-   * @returns {loop.store.ConversationStore}
-   */
-  function makeConversationStore() {
-    var roomDispatcher = new loop.Dispatcher();
-
-    var store = new loop.store.ConversationStore(dispatcher, {
-      client: {},
-      mozLoop: navigator.mozLoop,
-      sdkDriver: mockSDK
-    });
-
-    store.forcedUpdate = function forcedUpdate(contentWindow) {
-      // Since this is called by setTimeout, we don't want to lose any
-      // exceptions if there's a problem and we need to debug, so...
-      try {
-        var newStoreState = {
-          // Override the matchMedia, this is so that the correct version is
-          // used for the frame.
-          //
-          // Currently, we use an icky hack, and the showcase conspires with
-          // react-frame-component to set iframe.contentWindow.matchMedia onto
-          // the store. Once React context matures a bit (somewhere between
-          // 0.14 and 1.0, apparently):
-          //
-          // https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
-          //
-          // we should be able to use those to clean this up.
-          matchMedia: contentWindow.matchMedia.bind(contentWindow)
-        };
-
-        store.setStoreState(newStoreState);
-      } catch (ex) {
-        console.error("exception in forcedUpdate:", ex);
-      }
-    };
-
-    return store;
-  }
-
-  var conversationStores = [];
-  for (var index = 0; index < 5; index++) {
-    conversationStores[index] = makeConversationStore();
-  }
-
-  conversationStores[0].setStoreState({
-    callStateReason: FAILURE_DETAILS.NO_MEDIA
-  });
-  conversationStores[1].setStoreState({
-    callStateReason: FAILURE_DETAILS.USER_UNAVAILABLE,
-    contact: fakeManyContacts[0]
-  });
-
   // Update the text chat store with the room info.
   textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
     roomName: "A Very Long Conversation Name",
     roomUrl: "http://showcase",
     urls: [{
       description: "A wonderful page!",
       location: "http://wonderful.invalid"
       // use the fallback thumbnail
@@ -444,17 +383,16 @@
   dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
     contentType: loop.shared.utils.CHAT_CONTENT_TYPES.TEXT,
     message: "8:00 PM",
     receivedTimestamp: "2015-06-23T22:27:45.590Z"
   }));
 
   loop.store.StoreMixin.register({
     activeRoomStore: activeRoomStore,
-    conversationStore: conversationStores[0],
     textChatStore: textChatStore
   });
 
   // Local mocks
   var mockMozLoopNoRooms = _.cloneDeep(navigator.mozLoop);
   mockMozLoopNoRooms.rooms.getAll = function(version, callback) {
     callback(null, []);
   };
@@ -540,22 +478,16 @@
       value: "smith@invalid.com"
     }]
   };
 
   var mockClient = {
     requestCallUrlInfo: noop
   };
 
-  var mockWebSocket = new loop.CallConnectionWebSocket({
-    url: "fake",
-    callId: "fakeId",
-    websocketToken: "fakeToken"
-  });
-
   var notifications = new loop.shared.models.NotificationCollection();
   var errNotifications = new loop.shared.models.NotificationCollection();
   errNotifications.add({
     level: "error",
     message: "Could Not Authenticate",
     details: "Did you change your password?",
     detailsButtonLabel: "Retry"
   });
@@ -1055,57 +987,16 @@
                                 canEdit={false}
                                 eventPosY={0}
                                 getContainerCoordinates={function() { return {"top": 0, "height": 0 }; }}
                                 handleAction={function () {}} />
              </div>
             </FramedExample>
           </Section>
 
-          <Section name="AcceptCallView">
-            <FramedExample dashed={true}
-                           height={272}
-                           summary="Default / incoming video call"
-                           width={332}>
-              <div className="fx-embedded">
-                <AcceptCallView callType={CALL_TYPES.AUDIO_VIDEO}
-                                callerId="Mr Smith"
-                                dispatcher={dispatcher}
-                                mozLoop={mockMozLoopLoggedIn} />
-              </div>
-            </FramedExample>
-
-            <FramedExample dashed={true}
-                           height={272}
-                           summary="Default / incoming audio only call"
-                           width={332}>
-              <div className="fx-embedded">
-                <AcceptCallView callType={CALL_TYPES.AUDIO_ONLY}
-                                callerId="Mr Smith"
-                                dispatcher={dispatcher}
-                                mozLoop={mockMozLoopLoggedIn} />
-              </div>
-            </FramedExample>
-          </Section>
-
-          <Section name="AcceptCallView-ActiveState">
-            <FramedExample dashed={true}
-                           height={272}
-                           summary="Default"
-                           width={332}>
-              <div className="fx-embedded">
-                <AcceptCallView callType={CALL_TYPES.AUDIO_VIDEO}
-                                callerId="Mr Smith"
-                                dispatcher={dispatcher}
-                                mozLoop={mockMozLoopLoggedIn}
-                                showMenu={true} />
-              </div>
-            </FramedExample>
-          </Section>
-
           <Section name="ConversationToolbar">
             <div>
               <FramedExample dashed={true}
                              height={56}
                              summary="Default"
                              width={300}>
                 <div className="fx-embedded">
                   <ConversationToolbar audio={{ enabled: true, visible: true }}
@@ -1146,169 +1037,16 @@
                                        settingsMenuItems={[{ id: "feedback" }]}
                                        show={true}
                                        video={{ enabled: true, visible: true }} />
                 </div>
               </FramedExample>
             </div>
           </Section>
 
-          <Section name="PendingConversationView (Desktop)">
-            <FramedExample dashed={true}
-                           height={272}
-                           summary="Connecting"
-                           width={300}>
-              <div className="fx-embedded">
-                <DesktopPendingConversationView callState={"gather"}
-                                                contact={mockContact}
-                                                dispatcher={dispatcher} />
-              </div>
-            </FramedExample>
-          </Section>
-
-          <Section name="DirectCallFailureView">
-            <FramedExample
-              dashed={true}
-              height={254}
-              summary="Call Failed - Incoming"
-              width={298}>
-              <div className="fx-embedded">
-                <DirectCallFailureView
-                  conversationStore={conversationStores[0]}
-                  dispatcher={dispatcher}
-                  mozLoop={navigator.mozLoop}
-                  outgoing={false} />
-              </div>
-            </FramedExample>
-            <FramedExample
-              dashed={true}
-              height={254}
-              summary="Call Failed - Outgoing"
-              width={298}>
-              <div className="fx-embedded">
-                <DirectCallFailureView
-                  conversationStore={conversationStores[1]}
-                  dispatcher={dispatcher}
-                  mozLoop={navigator.mozLoop}
-                  outgoing={true} />
-              </div>
-            </FramedExample>
-            <FramedExample
-              dashed={true}
-              height={254}
-              summary="Call Failed — with call URL error"
-              width={298}>
-              <div className="fx-embedded">
-                <DirectCallFailureView
-                  conversationStore={conversationStores[0]}
-                  dispatcher={dispatcher}
-                  emailLinkError={true}
-                  mozLoop={navigator.mozLoop}
-                  outgoing={true} />
-              </div>
-            </FramedExample>
-          </Section>
-
-          <Section name="OngoingConversationView">
-            <FramedExample dashed={true}
-                           height={398}
-                           onContentsRendered={conversationStores[0].forcedUpdate}
-                           summary="Desktop ongoing conversation window"
-                           width={348}>
-              <div className="fx-embedded">
-                <OngoingConversationView
-                  audio={{ enabled: true, visible: true }}
-                  chatWindowDetached={false}
-                  conversationStore={conversationStores[0]}
-                  dispatcher={dispatcher}
-                  localPosterUrl="sample-img/video-screen-local.png"
-                  mediaConnected={true}
-                  remotePosterUrl="sample-img/video-screen-remote.png"
-                  remoteVideoEnabled={true}
-                  video={{ enabled: true, visible: true }} />
-              </div>
-            </FramedExample>
-
-            <FramedExample dashed={true}
-                           height={400}
-                           onContentsRendered={conversationStores[1].forcedUpdate}
-                           summary="Desktop ongoing conversation window (medium)"
-                           width={600}>
-              <div className="fx-embedded">
-                <OngoingConversationView
-                  audio={{ enabled: true, visible: true }}
-                  chatWindowDetached={false}
-                  conversationStore={conversationStores[1]}
-                  dispatcher={dispatcher}
-                  localPosterUrl="sample-img/video-screen-local.png"
-                  mediaConnected={true}
-                  remotePosterUrl="sample-img/video-screen-remote.png"
-                  remoteVideoEnabled={true}
-                  video={{ enabled: true, visible: true }} />
-              </div>
-            </FramedExample>
-
-            <FramedExample height={600}
-                           onContentsRendered={conversationStores[2].forcedUpdate}
-                           summary="Desktop ongoing conversation window (large)"
-                           width={800}>
-              <div className="fx-embedded">
-                <OngoingConversationView
-                  audio={{ enabled: true, visible: true }}
-                  chatWindowDetached={false}
-                  conversationStore={conversationStores[2]}
-                  dispatcher={dispatcher}
-                  localPosterUrl="sample-img/video-screen-local.png"
-                  mediaConnected={true}
-                  remotePosterUrl="sample-img/video-screen-remote.png"
-                  remoteVideoEnabled={true}
-                  video={{ enabled: true, visible: true }} />
-              </div>
-            </FramedExample>
-
-            <FramedExample dashed={true}
-                           height={398}
-                           onContentsRendered={conversationStores[3].forcedUpdate}
-                           summary="Desktop ongoing conversation window - local face mute"
-                           width={348}>
-              <div className="fx-embedded">
-                <OngoingConversationView
-                  audio={{ enabled: true, visible: true }}
-                  chatWindowDetached={false}
-                  conversationStore={conversationStores[3]}
-                  dispatcher={dispatcher}
-                  localPosterUrl="sample-img/video-screen-local.png"
-                  mediaConnected={true}
-                  remotePosterUrl="sample-img/video-screen-remote.png"
-                  remoteVideoEnabled={true}
-                  video={{ enabled: false, visible: true }} />
-              </div>
-            </FramedExample>
-
-            <FramedExample dashed={true}
-                           height={398}
-                           onContentsRendered={conversationStores[4].forcedUpdate}
-                           summary="Desktop ongoing conversation window - remote face mute"
-                           width={348} >
-              <div className="fx-embedded">
-                <OngoingConversationView
-                  audio={{ enabled: true, visible: true }}
-                  chatWindowDetached={false}
-                  conversationStore={conversationStores[4]}
-                  dispatcher={dispatcher}
-                  localPosterUrl="sample-img/video-screen-local.png"
-                  mediaConnected={true}
-                  remotePosterUrl="sample-img/video-screen-remote.png"
-                  remoteVideoEnabled={false}
-                  video={{ enabled: true, visible: true }} />
-              </div>
-            </FramedExample>
-
-          </Section>
-
           <Section name="FeedbackView">
             <p className="note">
             </p>
             <FramedExample dashed={true}
                            height={288}
                            summary="Default (useable demo)"
                            width={348}>
               <div className="fx-embedded">
--- a/browser/locales/en-US/chrome/browser/devtools/animationinspector.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/animationinspector.properties
@@ -82,9 +82,23 @@ timeline.cssanimation.nameLabel=%S - CSS
 # %S will be replaced by the name of the transition at run-time.
 timeline.csstransition.nameLabel=%S - CSS Transition
 
 # LOCALIZATION NOTE (timeline.unknown.nameLabel):
 # This string is displayed in a tooltip of the animation panel that is shown
 # when hovering over the name of an unknown animation type in the timeline UI.
 # This can happen if devtools couldn't figure out the type of the animation.
 # %S will be replaced by the name of the transition at run-time.
-timeline.unknown.nameLabel=%S
\ No newline at end of file
+timeline.unknown.nameLabel=%S
+
+# LOCALIZATION NOTE (node.selectNodeLabel):
+# This string is displayed in a tooltip of the animation panel that is shown
+# when hovering over an animated node (e.g. something like div.animated).
+# The tooltip invites the user to click on the node in order to select it in the
+# inspector panel.
+node.selectNodeLabel=Click to select this node in the Inspector
+
+# LOCALIZATION NOTE (node.highlightNodeLabel):
+# This string is displayed in a tooltip of the animation panel that is shown
+# when hovering over the inspector icon displayed next to animated nodes.
+# The tooltip invites the user to click on the icon in order to show the node
+# highlighter.
+node.highlightNodeLabel=Click to highlight this node in the page
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -37,19 +37,18 @@ invite_email_link_button=Email Link
 invite_facebook_button2=Share on Facebook
 
 # Status text
 display_name_guest=Guest
 display_name_dnd_status=Do Not Disturb
 display_name_available_status=Available
 
 # Error bars
-## LOCALIZATION NOTE(unable_retrieve_url,session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
+## LOCALIZATION NOTE(session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
 ## These may be displayed at the top of the panel.
-unable_retrieve_url=Sorry, we were unable to retrieve a call URL.
 session_expired_error_description=Session expired. All URLs you have previously created and shared will no longer work.
 could_not_authenticate=Could Not Authenticate
 password_changed_question=Did you change your password?
 try_again_later=Please try again later
 could_not_connect=Could Not Connect To The Server
 check_internet_connection=Please check your internet connection
 login_expired=Your Login Has Expired
 service_not_available=Service Unavailable At This Time
@@ -68,17 +67,16 @@ share_email_body6=Click the Firefox Hell
 share_email_body_context2=Join me for a video conversation. Click the Firefox Hello link to connect now: {{callUrl}}\n\nLet’s talk about this during our conversation: {{title}}
 ## LOCALIZATION NOTE (share_email_footer): Common footer content for both email types
 share_email_footer=\n\n________\nJoin and create video conversations free with Firefox Hello. Connect easily over video with anyone, anywhere. No downloads or registration. Learn more at http://www.firefox.com/hello
 ## LOCALIZATION NOTE (share_tweeet): In this item, don't translate the part
 ## between {{..}}. Please keep the text below 117 characters to make sure it fits
 ## in a tweet.
 share_tweet=Join me for a video conversation on {{clientShortname2}}!
 
-share_button3=Share Link
 share_add_service_button=Add a Service
 
 ## LOCALIZATION NOTE (copy_link_menuitem, email_link_menuitem, delete_conversation_menuitem):
 ## These menu items are displayed from a panel's context menu for a conversation.
 copy_link_menuitem=Copy Link
 email_link_menuitem=Email Link
 delete_conversation_menuitem=Delete conversation
 
@@ -217,21 +215,16 @@ gravatars_promo_message=You can automati
 gravatars_promo_message_learnmore=Learn more
 gravatars_promo_button_nothanks2=No, thanks
 gravatars_promo_button_use2=Use profile icons
 
 # Conversation Window Strings
 
 initiate_call_button_label2=Ready to start your conversation?
 incoming_call_title2=Conversation Request
-incoming_call_accept_button=Accept
-incoming_call_accept_audio_only_tooltip=Accept with voice
-incoming_call_accept_audio_video_tooltip=Accept with video
-incoming_call_cancel_button=Cancel
-incoming_call_cancel_and_block_button=Cancel and Block
 incoming_call_block_button=Block
 hangup_button_title=Hang up
 hangup_button_caption2=Exit
 mute_local_audio_button_title=Mute your audio
 unmute_local_audio_button_title=Unmute your audio
 mute_local_video_button_title2=Disable video
 unmute_local_video_button_title2=Enable video
 active_screenshare_button_title=Stop sharing
@@ -247,37 +240,22 @@ self_view_hidden_message=Self-view hidde
 call_with_contact_title=Conversation with {{contactName}}
 
 # Outgoing conversation
 
 outgoing_call_title=Start conversation?
 initiate_audio_video_call_button2=Start
 initiate_audio_video_call_tooltip2=Start a video conversation
 initiate_audio_call_button2=Voice conversation
-initiate_call_cancel_button=Cancel
-
-## LOCALIZATION NOTE (call_progress_connecting_description): This is displayed
-## whilst the client is contacting the client at the other end of the connection.
-call_progress_connecting_description=Connecting…
-## LOCALIZATION NOTE (call_progress_ringing_description): This is displayed
-## when the other client is actually ringing.
-call_progress_ringing_description=Ringing…
 
 peer_ended_conversation2=The person you were calling has ended the conversation.
 conversation_has_ended=Your conversation has ended.
 restart_call=Rejoin
 
-## LOCALIZATION NOTE (contact_unavailable_title): The title displayed
-## when a contact is unavailable. Don't translate the part between {{..}}
-## because this will be replaced by the contact's name.
-contact_unavailable_title={{contactName}} is unavailable.
-generic_contact_unavailable_title=This person is unavailable.
-
 generic_failure_message=We're having technical difficulties…
-generic_failure_with_reason2=You can try again or email a link to be reached at later.
 generic_failure_no_reason2=Would you like to try again?
 ## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname2}}
 ## as this will be replaced by the shortname.
 tos_failure_message={{clientShortname}} is not available in your country.
 
 ## LOCALIZATION NOTE (contact_offline_title): Title which is displayed when the
 ## contact is offline.
 contact_offline_title=This person is not online
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -14,17 +14,17 @@
 %include linuxShared.inc
 %filter substitution
 
 %define forwardTransitionLength 150ms
 %define conditionalForwardWithUrlbar window:not([chromehidden~="toolbar"]) #urlbar-wrapper
 %define conditionalForwardWithUrlbarWidth 30
 
 :root {
-  --backbutton-urlbar-overlap: 5px;
+  --backbutton-urlbar-overlap: 6px;
 
   --toolbarbutton-hover-background: hsla(0,0%,100%,.3) linear-gradient(hsla(0,0%,100%,.7), hsla(0,0%,100%,.2));
   --toolbarbutton-hover-boxshadow: 0 1px 0 hsla(0,0%,100%,.3) inset, 0 0 0 1px hsla(0,0%,100%,.2) inset, 0 1px 0 hsla(0,0%,0%,.03);
   --toolbarbutton-hover-bordercolor: rgb(154,154,154);
 
   --toolbarbutton-active-boxshadow: 0 1px 1px hsla(0,0%,0%,.1) inset, 0 0 1px hsla(0,0%,0%,.3) inset;
   --toolbarbutton-active-bordercolor: rgb(154,154,154);
   --toolbarbutton-active-background: rgba(154,154,154,.5) linear-gradient(hsla(0,0%,100%,.7), hsla(0,0%,100%,.4));
@@ -692,87 +692,60 @@ toolbarbutton[constrain-size="true"][cui
 }
 
 :-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   -moz-margin-start: -4px;
   margin-top: 3px;
   margin-bottom: 3px;
 }
 
+:-moz-any(#back-button, #forward-button) > .toolbarbutton-icon {
+  border-color: ThreeDShadow !important /* bug 561154 */;
+}
+
+:-moz-any(#back-button, #forward-button):not(:hover):not(:active):not([open=true]) > .toolbarbutton-icon,
+:-moz-any(#back-button, #forward-button)[disabled=true] > .toolbarbutton-icon {
+  background-color: rgba(255,255,255,.15) !important /* bug 561154 */;
+}
+
 #back-button {
-  padding-top: 3px;
-  padding-bottom: 3px;
-  -moz-padding-start: 5px;
+  padding-top: 2px;
+  padding-bottom: 2px;
+  -moz-padding-start: 4px;
   -moz-padding-end: 0;
   position: relative;
   z-index: 1;
   border-radius: 0 10000px 10000px 0;
 }
 
 #back-button:-moz-locale-dir(rtl) {
   border-radius: 10000px 0 0 10000px;
 }
 
 #back-button > menupopup {
   margin-top: -1px;
 }
 
 #back-button > .toolbarbutton-icon {
   border-radius: 10000px;
-  background-clip: padding-box;
   padding: 6px;
-  border: none;
-  box-shadow: 0 1px 0 hsla(0,0%,100%,.3) inset,
-              0 0 0 1px hsla(0,0%,100%,.3) inset,
-              0 0 0 1px hsla(210,54%,20%,.25),
-              0 1px 0 hsla(210,54%,20%,.35);
-  background-image: linear-gradient(hsla(0,0%,100%,.6), hsla(0,0%,100%,.1));
-  transition-property: background-color, box-shadow;
-  transition-duration: 250ms;
-}
-
-#back-button:not([disabled="true"]):not([open="true"]):not(:active):hover > .toolbarbutton-icon {
-  background-color: hsla(210,48%,96%,.75);
-  box-shadow: 0 1px 0 hsla(0,0%,100%,.3) inset,
-              0 0 0 1px hsla(0,0%,100%,.3) inset,
-              0 0 0 1px hsla(210,54%,20%,.3),
-              0 1px 0 hsla(210,54%,20%,.4),
-              0 0 4px hsla(210,54%,20%,.2);
-}
-
-#back-button:not([disabled="true"]):hover:active > .toolbarbutton-icon,
-#back-button[open="true"] > .toolbarbutton-icon {
-  background-color: hsla(210,54%,20%,.15);
-  box-shadow: 0 1px 1px hsla(210,54%,20%,.1) inset,
-              0 0 1px hsla(210,54%,20%,.2) inset,
-              0 0 0 1px hsla(210,54%,20%,.4),
-              0 1px 0 hsla(210,54%,20%,.2);
-  transition: none;
-}
-
-#main-window:not([customizing]) #back-button[disabled] > .toolbarbutton-icon {
-  box-shadow: 0 0 0 1px hsla(210,54%,20%,.55),
-              0 1px 0 hsla(210,54%,20%,.65) !important;
-  transition: none;
 }
 
 #back-button:-moz-locale-dir(rtl) > .toolbarbutton-icon {
   transform: scaleX(-1);
 }
 
 #forward-button {
   -moz-box-align: stretch; /* let the button shape grow vertically with the location bar */
   padding: 0;
 }
 
 #forward-button > .toolbarbutton-icon {
-  background-clip: padding-box;
-  padding-left: calc(var(--backbutton-urlbar-overlap) + 4px);
+  padding-left: calc(var(--backbutton-urlbar-overlap) + 3px);
   padding-right: 3px;
-  border: 1px solid #9a9a9a;
   border-left-style: none;
   border-radius: 0;
 }
 
 @conditionalForwardWithUrlbar@:not([switchingtabs]) > #forward-button {
   transition: margin-left @forwardTransitionLength@ ease-out;
 }
 
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -186,16 +186,21 @@
   background: url(chrome://browser/skin/controlcenter/warning-gray.svg) no-repeat 0 50%;
 }
 
 .identity-popup-warning-yellow {
   -moz-padding-start: 24px;
   background: url(chrome://browser/skin/controlcenter/warning-yellow.svg) no-repeat 0 50%;
 }
 
+.identity-popup-warning-gray:-moz-locale-dir(rtl),
+.identity-popup-warning-yellow:-moz-locale-dir(rtl) {
+  background-position: 100% 50%;
+}
+
 /* SECURITY */
 
 .identity-popup-connection-secure {
   color: #418220;
 }
 
 .identity-popup-connection-not-secure {
   color: #d74345;
--- a/devtools/client/animationinspector/animation-panel.js
+++ b/devtools/client/animationinspector/animation-panel.js
@@ -73,18 +73,16 @@ var AnimationsPanel = {
     }
     this.destroyed = promise.defer();
 
     this.stopListeners();
 
     this.animationsTimelineComponent.destroy();
     this.animationsTimelineComponent = null;
 
-    yield this.destroyPlayerWidgets();
-
     this.playersEl = this.errorMessageEl = null;
     this.toggleAllButtonEl = this.pickerButtonEl = null;
     this.playTimelineButtonEl = null;
 
     this.destroyed.resolve();
   }),
 
   startListeners: function() {
@@ -182,17 +180,16 @@ var AnimationsPanel = {
     }
   },
 
   refreshAnimations: Task.async(function*() {
     let done = gInspector.updating("animationspanel");
 
     // Empty the whole panel first.
     this.togglePlayers(true);
-    yield this.destroyPlayerWidgets();
 
     // Re-render the timeline component.
     this.animationsTimelineComponent.render(
       AnimationsController.animationPlayers,
       AnimationsController.documentCurrentTime);
 
     // If there are no players to show, show the error message instead and
     // return.
@@ -200,23 +197,12 @@ var AnimationsPanel = {
       this.togglePlayers(false);
       this.emit(this.UI_UPDATED_EVENT);
       done();
       return;
     }
 
     this.emit(this.UI_UPDATED_EVENT);
     done();
-  }),
-
-  destroyPlayerWidgets: Task.async(function*() {
-    if (!this.playerWidgets) {
-      return;
-    }
-
-    let destroyers = this.playerWidgets.map(widget => widget.destroy());
-    yield promise.all(destroyers);
-    this.playerWidgets = null;
-    this.playersEl.innerHTML = "";
   })
 };
 
 EventEmitter.decorate(AnimationsPanel);
--- a/devtools/client/animationinspector/components.js
+++ b/devtools/client/animationinspector/components.js
@@ -20,17 +20,18 @@
 //    c.destroy();
 
 const {Cu} = require("chrome");
 Cu.import("resource:///modules/devtools/client/shared/widgets/ViewHelpers.jsm");
 const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 const {
   createNode,
   drawGraphElementBackground,
-  findOptimalTimeInterval
+  findOptimalTimeInterval,
+  TargetNodeHighlighter
 } = require("devtools/client/animationinspector/utils");
 
 const STRINGS_URI = "chrome://browser/locale/devtools/animationinspector.properties";
 const L10N = new ViewHelpers.L10N(STRINGS_URI);
 const MILLIS_TIME_FORMAT_MAX_DURATION = 4000;
 // The minimum spacing between 2 time graduation headers in the timeline (px).
 const TIME_GRADUATION_MIN_SPACING = 40;
 
@@ -47,16 +48,18 @@ const TIME_GRADUATION_MIN_SPACING = 40;
 function AnimationTargetNode(inspector, options={}) {
   this.inspector = inspector;
   this.options = options;
 
   this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this);
   this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this);
   this.onSelectNodeClick = this.onSelectNodeClick.bind(this);
   this.onMarkupMutations = this.onMarkupMutations.bind(this);
+  this.onHighlightNodeClick = this.onHighlightNodeClick.bind(this);
+  this.onTargetHighlighterLocked = this.onTargetHighlighterLocked.bind(this);
 
   EventEmitter.decorate(this);
 }
 
 exports.AnimationTargetNode = AnimationTargetNode;
 
 AnimationTargetNode.prototype = {
   init: function(containerEl) {
@@ -66,28 +69,32 @@ AnimationTargetNode.prototype = {
     this.el = createNode({
       parent: containerEl,
       attributes: {
         "class": "animation-target"
       }
     });
 
     // Icon to select the node in the inspector.
-    this.selectNodeEl = createNode({
+    this.highlightNodeEl = createNode({
       parent: this.el,
       nodeType: "span",
       attributes: {
-        "class": "node-selector"
+        "class": "node-highlighter",
+        "title": L10N.getStr("node.highlightNodeLabel")
       }
     });
 
     // Wrapper used for mouseover/out event handling.
     this.previewEl = createNode({
       parent: this.el,
-      nodeType: "span"
+      nodeType: "span",
+      attributes: {
+        "title": L10N.getStr("node.selectNodeLabel")
+      }
     });
 
     if (!this.options.compact) {
       this.previewEl.appendChild(document.createTextNode("<"));
     }
 
     // Tag name.
     this.tagNameEl = createNode({
@@ -175,51 +182,90 @@ AnimationTargetNode.prototype = {
     if (!this.options.compact) {
       this.classEl.appendChild(document.createTextNode("\""));
       this.previewEl.appendChild(document.createTextNode(">"));
     }
 
     // Init events for highlighting and selecting the node.
     this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver);
     this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut);
-    this.selectNodeEl.addEventListener("click", this.onSelectNodeClick);
+    this.previewEl.addEventListener("click", this.onSelectNodeClick);
+    this.highlightNodeEl.addEventListener("click", this.onHighlightNodeClick);
 
     // Start to listen for markupmutation events.
     this.inspector.on("markupmutation", this.onMarkupMutations);
+
+    // Listen to the target node highlighter.
+    TargetNodeHighlighter.on("highlighted", this.onTargetHighlighterLocked);
   },
 
   destroy: function() {
+    TargetNodeHighlighter.unhighlight().catch(e => console.error(e));
+
+    TargetNodeHighlighter.off("highlighted", this.onTargetHighlighterLocked);
     this.inspector.off("markupmutation", this.onMarkupMutations);
     this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver);
     this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut);
-    this.selectNodeEl.removeEventListener("click", this.onSelectNodeClick);
+    this.previewEl.removeEventListener("click", this.onSelectNodeClick);
+    this.highlightNodeEl.removeEventListener("click", this.onHighlightNodeClick);
+
     this.el.remove();
     this.el = this.tagNameEl = this.idEl = this.classEl = null;
-    this.selectNodeEl = this.previewEl = null;
+    this.highlightNodeEl = this.previewEl = null;
     this.nodeFront = this.inspector = this.playerFront = null;
   },
 
+  get highlighterUtils() {
+    return this.inspector.toolbox.highlighterUtils;
+  },
+
   onPreviewMouseOver: function() {
     if (!this.nodeFront) {
       return;
     }
-    this.inspector.toolbox.highlighterUtils.highlightNodeFront(this.nodeFront);
+    this.highlighterUtils.highlightNodeFront(this.nodeFront);
   },
 
   onPreviewMouseOut: function() {
-    this.inspector.toolbox.highlighterUtils.unhighlight();
+    if (!this.nodeFront) {
+      return;
+    }
+    this.highlighterUtils.unhighlight();
   },
 
   onSelectNodeClick: function() {
     if (!this.nodeFront) {
       return;
     }
     this.inspector.selection.setNodeFront(this.nodeFront, "animationinspector");
   },
 
+  onHighlightNodeClick: function() {
+    let classList = this.highlightNodeEl.classList;
+
+    let isHighlighted = classList.contains("selected");
+    if (isHighlighted) {
+      classList.remove("selected");
+      TargetNodeHighlighter.unhighlight().then(() => {
+        this.emit("target-highlighter-unlocked");
+      }, e => console.error(e));
+    } else {
+      classList.add("selected");
+      TargetNodeHighlighter.highlight(this).then(() => {
+        this.emit("target-highlighter-locked");
+      }, e => console.error(e));
+    }
+  },
+
+  onTargetHighlighterLocked: function(e, animationTargetNode) {
+    if (animationTargetNode !== this) {
+      this.highlightNodeEl.classList.remove("selected");
+    }
+  },
+
   onMarkupMutations: function(e, mutations) {
     if (!this.nodeFront || !this.playerFront) {
       return;
     }
 
     for (let {target} of mutations) {
       if (target === this.nodeFront) {
         // Re-render with the same nodeFront to update the output.
@@ -232,23 +278,24 @@ AnimationTargetNode.prototype = {
   render: Task.async(function*(playerFront) {
     this.playerFront = playerFront;
     this.nodeFront = undefined;
 
     try {
       this.nodeFront = yield this.inspector.walker.getNodeFromActor(
                              playerFront.actorID, ["node"]);
     } catch (e) {
-      // We might have been destroyed in the meantime, or the node might not be
-      // found.
       if (!this.el) {
+        // The panel was destroyed in the meantime. Just log a warning.
         console.warn("Cound't retrieve the animation target node, widget " +
                      "destroyed");
+      } else {
+        // This was an unexpected error, log it.
+        console.error(e);
       }
-      console.error(e);
       return;
     }
 
     if (!this.nodeFront || !this.el) {
       return;
     }
 
     let {tagName, attributes} = this.nodeFront;
--- a/devtools/client/animationinspector/test/browser.ini
+++ b/devtools/client/animationinspector/test/browser.ini
@@ -18,16 +18,17 @@ support-files =
 [browser_animation_playerWidgets_appear_on_panel_init.js]
 [browser_animation_playerWidgets_target_nodes.js]
 [browser_animation_refresh_on_added_animation.js]
 [browser_animation_refresh_on_removed_animation.js]
 [browser_animation_refresh_when_active.js]
 [browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
 [browser_animation_shows_player_on_valid_node.js]
 [browser_animation_target_highlight_select.js]
+[browser_animation_target_highlighter_lock.js]
 [browser_animation_timeline_header.js]
 [browser_animation_timeline_pause_button.js]
 [browser_animation_timeline_scrubber_exists.js]
 [browser_animation_timeline_scrubber_movable.js]
 [browser_animation_timeline_scrubber_moves.js]
 [browser_animation_timeline_shows_delay.js]
 [browser_animation_timeline_shows_iterations.js]
 [browser_animation_timeline_shows_time_info.js]
--- a/devtools/client/animationinspector/test/browser_animation_participate_in_inspector_update.js
+++ b/devtools/client/animationinspector/test/browser_animation_participate_in_inspector_update.js
@@ -5,39 +5,39 @@
 "use strict";
 
 // Test that the update of the animation panel participate in the
 // inspector-updated event. This means that the test verifies that the
 // inspector-updated event is emitted *after* the animation panel is ready.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel, controller} = yield openAnimationInspector();
 
-  let ui = yield openAnimationInspector();
-  yield testEventsOrder(ui);
-});
-
-function* testEventsOrder({inspector, panel, controller}) {
   info("Listen for the players-updated, ui-updated and inspector-updated events");
   let receivedEvents = [];
   controller.once(controller.PLAYERS_UPDATED_EVENT, () => {
     receivedEvents.push(controller.PLAYERS_UPDATED_EVENT);
   });
   panel.once(panel.UI_UPDATED_EVENT, () => {
     receivedEvents.push(panel.UI_UPDATED_EVENT);
   });
   inspector.once("inspector-updated", () => {
     receivedEvents.push("inspector-updated");
   });
 
   info("Selecting an animated node");
   let node = yield getNodeFront(".animated", inspector);
   yield selectNode(node, inspector);
 
-  info("Check that all events were received, and in the right order");
+  info("Check that all events were received");
+  // Only assert that the inspector-updated event is last, the order of the
+  // first 2 events is irrelevant.
+
   is(receivedEvents.length, 3, "3 events were received");
-  is(receivedEvents[0], controller.PLAYERS_UPDATED_EVENT,
-    "The first event received was the players-updated event");
-  is(receivedEvents[1], panel.UI_UPDATED_EVENT,
-    "The second event received was the ui-updated event");
   is(receivedEvents[2], "inspector-updated",
-    "The third event received was the inspector-updated event");
-}
+     "The third event received was the inspector-updated event");
+
+  ok(receivedEvents.indexOf(controller.PLAYERS_UPDATED_EVENT) !== -1,
+     "The players-updated event was received");
+  ok(receivedEvents.indexOf(panel.UI_UPDATED_EVENT) !== -1,
+     "The ui-updated event was received");
+});
--- a/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js
+++ b/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js
@@ -18,12 +18,12 @@ add_task(function*() {
   // yet been retrieved by the TargetNodeComponent.
   if (!targetNodeComponent.nodeFront) {
     yield targetNodeComponent.once("target-retrieved");
   }
 
   is(targetNodeComponent.el.textContent, "div#.ball.animated",
     "The target element's content is correct");
 
-  let selectorEl = targetNodeComponent.el.querySelector(".node-selector");
-  ok(selectorEl,
-    "The icon to select the target element in the inspector exists");
+  let highlighterEl = targetNodeComponent.el.querySelector(".node-highlighter");
+  ok(highlighterEl,
+    "The icon to highlight the target element in the page exists");
 });
--- a/devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js
+++ b/devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js
@@ -3,43 +3,43 @@
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test that the panel content refreshes when new animations are added.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel} = yield openAnimationInspector();
 
-  let {inspector, panel} = yield openAnimationInspector();
-  yield testRefreshOnNewAnimation(inspector, panel);
-});
-
-function* testRefreshOnNewAnimation(inspector, panel) {
   info("Select a non animated node");
   yield selectNode(".still", inspector);
 
   assertAnimationsDisplayed(panel, 0);
 
-  info("Listen to the next UI update event");
-  let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
-
   info("Start an animation on the node");
-  yield executeInContent("devtools:test:setAttribute", {
+  yield changeElementAndWait({
     selector: ".still",
     attributeName: "class",
     attributeValue: "ball animated"
-  });
-
-  yield onPanelUpdated;
-  ok(true, "The panel update event was fired");
+  }, panel, inspector);
 
   assertAnimationsDisplayed(panel, 1);
 
   info("Remove the animation class on the node");
-  onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
-  yield executeInContent("devtools:test:setAttribute", {
+  yield changeElementAndWait({
     selector: ".ball.animated",
     attributeName: "class",
     attributeValue: "ball still"
-  });
-  yield onPanelUpdated;
-}
+  }, panel, inspector);
+
+  assertAnimationsDisplayed(panel, 0);
+});
+
+function* changeElementAndWait(options, panel, inspector) {
+  let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
+  let onInspectorUpdated = inspector.once("inspector-updated");
+
+  yield executeInContent("devtools:test:setAttribute", options);
+
+  yield promise.all([
+    onInspectorUpdated, onPanelUpdated, waitForAllAnimationTargets(panel)]);
+}
\ No newline at end of file
--- a/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js
+++ b/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js
@@ -5,23 +5,19 @@
 "use strict";
 
 // Test that the DOM element targets displayed in animation player widgets can
 // be used to highlight elements in the DOM and select them in the inspector.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
 
-  let ui = yield openAnimationInspector();
-  yield testTargetNode(ui);
-});
+  let {toolbox, inspector, panel} = yield openAnimationInspector();
 
-function* testTargetNode({toolbox, inspector, panel}) {
   info("Select the simple animated node");
-
   let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
   yield selectNode(".animated", inspector);
   yield onPanelUpdated;
 
   let targets = yield waitForAllAnimationTargets(panel);
   // Arbitrary select the first one
   let targetNodeComponent = targets[0];
 
@@ -31,17 +27,17 @@ function* testTargetNode({toolbox, inspe
   info("Listen to node-highlight event and mouse over the widget");
   let onHighlight = toolbox.once("node-highlight");
   EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseover"},
                              highlightingEl.ownerDocument.defaultView);
   let nodeFront = yield onHighlight;
 
   // Do not forget to mouseout, otherwise we get random mouseover event
   // when selecting another node, which triggers some requests in animation
-  // inspector
+  // inspector.
   EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseout"},
                              highlightingEl.ownerDocument.defaultView);
 
   ok(true, "The node-highlight event was fired");
   is(targetNodeComponent.nodeFront, nodeFront,
     "The highlighted node is the one stored on the animation widget");
   is(nodeFront.tagName, "DIV",
     "The highlighted node has the correct tagName");
@@ -53,24 +49,23 @@ function* testTargetNode({toolbox, inspe
   info("Select the body node in order to have the list of all animations");
   onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
   yield selectNode("body", inspector);
   yield onPanelUpdated;
 
   targets = yield waitForAllAnimationTargets(panel);
   targetNodeComponent = targets[0];
 
-  info("Click on the first animation widget's selector icon and wait for the " +
-    "selection to change");
+  info("Click on the first animated node component and wait for the " +
+       "selection to change");
   let onSelection = inspector.selection.once("new-node-front");
   onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
-  let selectIconEl = targetNodeComponent.selectNodeEl;
-  EventUtils.sendMouseEvent({type: "click"}, selectIconEl,
-                            selectIconEl.ownerDocument.defaultView);
+  let nodeEl = targetNodeComponent.previewEl;
+  EventUtils.sendMouseEvent({type: "click"}, nodeEl,
+                            nodeEl.ownerDocument.defaultView);
   yield onSelection;
 
   is(inspector.selection.nodeFront, targetNodeComponent.nodeFront,
     "The selected node is the one stored on the animation widget");
 
   yield onPanelUpdated;
-
   yield waitForAllAnimationTargets(panel);
-}
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js
@@ -0,0 +1,50 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the DOM element targets displayed in animation player widgets can
+// be used to highlight elements in the DOM and select them in the inspector.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {toolbox, inspector, panel} = yield openAnimationInspector();
+
+  let targets = panel.animationsTimelineComponent.targetNodes;
+
+  info("Click on the highlighter icon for the first animated node");
+  yield lockHighlighterOn(targets[0]);
+  ok(targets[0].highlightNodeEl.classList.contains("selected"),
+     "The highlighter icon is selected");
+
+  info("Click on the highlighter icon for the second animated node");
+  yield lockHighlighterOn(targets[1]);
+  ok(targets[1].highlightNodeEl.classList.contains("selected"),
+     "The highlighter icon is selected");
+  ok(!targets[0].highlightNodeEl.classList.contains("selected"),
+     "The highlighter icon for the first node is unselected");
+
+  info("Click again to unhighlight");
+  yield unlockHighlighterOn(targets[1]);
+  ok(!targets[1].highlightNodeEl.classList.contains("selected"),
+     "The highlighter icon for the second node is unselected");
+});
+
+function* lockHighlighterOn(targetComponent) {
+  let onLocked = targetComponent.once("target-highlighter-locked");
+  clickOnHighlighterIcon(targetComponent);
+  yield onLocked;
+}
+
+function* unlockHighlighterOn(targetComponent) {
+  let onUnlocked = targetComponent.once("target-highlighter-unlocked");
+  clickOnHighlighterIcon(targetComponent);
+  yield onUnlocked;
+}
+
+function clickOnHighlighterIcon(targetComponent) {
+  let lockEl = targetComponent.highlightNodeEl;
+  EventUtils.sendMouseEvent({type: "click"}, lockEl,
+                            lockEl.ownerDocument.defaultView);
+}
--- a/devtools/client/animationinspector/utils.js
+++ b/devtools/client/animationinspector/utils.js
@@ -1,16 +1,22 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const {Cu} = require("chrome");
+const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
+var {loader} = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm");
+loader.lazyRequireGetter(this, "EventEmitter",
+                               "devtools/shared/event-emitter");
+
 // How many times, maximum, can we loop before we find the optimal time
 // interval in the timeline graph.
 const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100;
 // Background time graduations should be multiple of this number of millis.
 const TIME_INTERVAL_MULTIPLE = 25;
 const TIME_INTERVAL_SCALES = 3;
 // The default minimum spacing between time graduations in px.
 const TIME_GRADUATION_MIN_SPACING = 10;
@@ -129,8 +135,45 @@ function findOptimalTimeInterval(timeSca
       timingStep *= 2;
       continue;
     }
     return scaledStep;
   }
 }
 
 exports.findOptimalTimeInterval = findOptimalTimeInterval;
+
+/**
+ * The TargetNodeHighlighter util is a helper for AnimationTargetNode components
+ * that is used to lock the highlighter on animated nodes in the page.
+ * It instantiates a new highlighter that is then shared amongst all instances
+ * of AnimationTargetNode. This is useful because that means showing the
+ * highlighter on one animated node will unhighlight the previously highlighted
+ * one, but will not interfere with the default inspector highlighter.
+ */
+var TargetNodeHighlighter = {
+  highlighter: null,
+  isShown: false,
+
+  highlight: Task.async(function*(animationTargetNode) {
+    if (!this.highlighter) {
+      let hUtils = animationTargetNode.inspector.toolbox.highlighterUtils;
+      this.highlighter = yield hUtils.getHighlighterByType("BoxModelHighlighter");
+    }
+
+    yield this.highlighter.show(animationTargetNode.nodeFront);
+    this.isShown = true;
+    this.emit("highlighted", animationTargetNode);
+  }),
+
+  unhighlight: Task.async(function*() {
+    if (!this.highlighter || !this.isShown) {
+      return;
+    }
+
+    yield this.highlighter.hide();
+    this.isShown = false;
+    this.emit("unhighlighted");
+  })
+};
+
+EventEmitter.decorate(TargetNodeHighlighter);
+exports.TargetNodeHighlighter = TargetNodeHighlighter;
--- a/devtools/client/shared/test/browser_css_color.js
+++ b/devtools/client/shared/test/browser_css_color.js
@@ -125,170 +125,170 @@ function testSetAlpha() {
     ok(true, "Fails when setAlpha receives an invalid color.");
   }
 
   is(colorUtils.setAlpha("#fff"), "rgba(255, 255, 255, 1)", "sets alpha to 1 if invalid.");
 }
 
 function getTestData() {
   return [
-    {authored: "aliceblue", name: "aliceblue", hex: "#F0F8FF", hsl: "hsl(208, 100%, 97%)", rgb: "rgb(240, 248, 255)"},
-    {authored: "antiquewhite", name: "antiquewhite", hex: "#FAEBD7", hsl: "hsl(34, 78%, 91%)", rgb: "rgb(250, 235, 215)"},
-    {authored: "aqua", name: "aqua", hex: "#0FF", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)"},
-    {authored: "aquamarine", name: "aquamarine", hex: "#7FFFD4", hsl: "hsl(160, 100%, 75%)", rgb: "rgb(127, 255, 212)"},
-    {authored: "azure", name: "azure", hex: "#F0FFFF", hsl: "hsl(180, 100%, 97%)", rgb: "rgb(240, 255, 255)"},
-    {authored: "beige", name: "beige", hex: "#F5F5DC", hsl: "hsl(60, 56%, 91%)", rgb: "rgb(245, 245, 220)"},
-    {authored: "bisque", name: "bisque", hex: "#FFE4C4", hsl: "hsl(33, 100%, 88%)", rgb: "rgb(255, 228, 196)"},
+    {authored: "aliceblue", name: "aliceblue", hex: "#f0f8ff", hsl: "hsl(208, 100%, 97%)", rgb: "rgb(240, 248, 255)"},
+    {authored: "antiquewhite", name: "antiquewhite", hex: "#faebd7", hsl: "hsl(34, 78%, 91%)", rgb: "rgb(250, 235, 215)"},
+    {authored: "aqua", name: "aqua", hex: "#0ff", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)"},
+    {authored: "aquamarine", name: "aquamarine", hex: "#7fffd4", hsl: "hsl(160, 100%, 75%)", rgb: "rgb(127, 255, 212)"},
+    {authored: "azure", name: "azure", hex: "#f0ffff", hsl: "hsl(180, 100%, 97%)", rgb: "rgb(240, 255, 255)"},
+    {authored: "beige", name: "beige", hex: "#f5f5dc", hsl: "hsl(60, 56%, 91%)", rgb: "rgb(245, 245, 220)"},
+    {authored: "bisque", name: "bisque", hex: "#ffe4c4", hsl: "hsl(33, 100%, 88%)", rgb: "rgb(255, 228, 196)"},
     {authored: "black", name: "black", hex: "#000", hsl: "hsl(0, 0%, 0%)", rgb: "rgb(0, 0, 0)"},
-    {authored: "blanchedalmond", name: "blanchedalmond", hex: "#FFEBCD", hsl: "hsl(36, 100%, 90%)", rgb: "rgb(255, 235, 205)"},
-    {authored: "blue", name: "blue", hex: "#00F", hsl: "hsl(240, 100%, 50%)", rgb: "rgb(0, 0, 255)"},
-    {authored: "blueviolet", name: "blueviolet", hex: "#8A2BE2", hsl: "hsl(271, 76%, 53%)", rgb: "rgb(138, 43, 226)"},
-    {authored: "brown", name: "brown", hex: "#A52A2A", hsl: "hsl(0, 59%, 41%)", rgb: "rgb(165, 42, 42)"},
-    {authored: "burlywood", name: "burlywood", hex: "#DEB887", hsl: "hsl(34, 57%, 70%)", rgb: "rgb(222, 184, 135)"},
-    {authored: "cadetblue", name: "cadetblue", hex: "#5F9EA0", hsl: "hsl(182, 25%, 50%)", rgb: "rgb(95, 158, 160)"},
-    {authored: "chartreuse", name: "chartreuse", hex: "#7FFF00", hsl: "hsl(90, 100%, 50%)", rgb: "rgb(127, 255, 0)"},
-    {authored: "chocolate", name: "chocolate", hex: "#D2691E", hsl: "hsl(25, 75%, 47%)", rgb: "rgb(210, 105, 30)"},
-    {authored: "coral", name: "coral", hex: "#FF7F50", hsl: "hsl(16, 100%, 66%)", rgb: "rgb(255, 127, 80)"},
-    {authored: "cornflowerblue", name: "cornflowerblue", hex: "#6495ED", hsl: "hsl(219, 79%, 66%)", rgb: "rgb(100, 149, 237)"},
-    {authored: "cornsilk", name: "cornsilk", hex: "#FFF8DC", hsl: "hsl(48, 100%, 93%)", rgb: "rgb(255, 248, 220)"},
-    {authored: "crimson", name: "crimson", hex: "#DC143C", hsl: "hsl(348, 83%, 47%)", rgb: "rgb(220, 20, 60)"},
-    {authored: "cyan", name: "aqua", hex: "#0FF", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)"},
-    {authored: "darkblue", name: "darkblue", hex: "#00008B", hsl: "hsl(240, 100%, 27%)", rgb: "rgb(0, 0, 139)"},
-    {authored: "darkcyan", name: "darkcyan", hex: "#008B8B", hsl: "hsl(180, 100%, 27%)", rgb: "rgb(0, 139, 139)"},
-    {authored: "darkgoldenrod", name: "darkgoldenrod", hex: "#B8860B", hsl: "hsl(43, 89%, 38%)", rgb: "rgb(184, 134, 11)"},
-    {authored: "darkgray", name: "darkgray", hex: "#A9A9A9", hsl: "hsl(0, 0%, 66%)", rgb: "rgb(169, 169, 169)"},
+    {authored: "blanchedalmond", name: "blanchedalmond", hex: "#ffebcd", hsl: "hsl(36, 100%, 90%)", rgb: "rgb(255, 235, 205)"},
+    {authored: "blue", name: "blue", hex: "#00f", hsl: "hsl(240, 100%, 50%)", rgb: "rgb(0, 0, 255)"},
+    {authored: "blueviolet", name: "blueviolet", hex: "#8a2be2", hsl: "hsl(271, 76%, 53%)", rgb: "rgb(138, 43, 226)"},
+    {authored: "brown", name: "brown", hex: "#a52a2a", hsl: "hsl(0, 59%, 41%)", rgb: "rgb(165, 42, 42)"},
+    {authored: "burlywood", name: "burlywood", hex: "#deb887", hsl: "hsl(34, 57%, 70%)", rgb: "rgb(222, 184, 135)"},
+    {authored: "cadetblue", name: "cadetblue", hex: "#5f9ea0", hsl: "hsl(182, 25%, 50%)", rgb: "rgb(95, 158, 160)"},
+    {authored: "chartreuse", name: "chartreuse", hex: "#7fff00", hsl: "hsl(90, 100%, 50%)", rgb: "rgb(127, 255, 0)"},
+    {authored: "chocolate", name: "chocolate", hex: "#d2691e", hsl: "hsl(25, 75%, 47%)", rgb: "rgb(210, 105, 30)"},
+    {authored: "coral", name: "coral", hex: "#ff7f50", hsl: "hsl(16, 100%, 66%)", rgb: "rgb(255, 127, 80)"},
+    {authored: "cornflowerblue", name: "cornflowerblue", hex: "#6495ed", hsl: "hsl(219, 79%, 66%)", rgb: "rgb(100, 149, 237)"},
+    {authored: "cornsilk", name: "cornsilk", hex: "#fff8dc", hsl: "hsl(48, 100%, 93%)", rgb: "rgb(255, 248, 220)"},
+    {authored: "crimson", name: "crimson", hex: "#dc143c", hsl: "hsl(348, 83%, 47%)", rgb: "rgb(220, 20, 60)"},
+    {authored: "cyan", name: "aqua", hex: "#0ff", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)"},
+    {authored: "darkblue", name: "darkblue", hex: "#00008b", hsl: "hsl(240, 100%, 27%)", rgb: "rgb(0, 0, 139)"},
+    {authored: "darkcyan", name: "darkcyan", hex: "#008b8b", hsl: "hsl(180, 100%, 27%)", rgb: "rgb(0, 139, 139)"},
+    {authored: "darkgoldenrod", name: "darkgoldenrod", hex: "#b8860b", hsl: "hsl(43, 89%, 38%)", rgb: "rgb(184, 134, 11)"},
+    {authored: "darkgray", name: "darkgray", hex: "#a9a9a9", hsl: "hsl(0, 0%, 66%)", rgb: "rgb(169, 169, 169)"},
     {authored: "darkgreen", name: "darkgreen", hex: "#006400", hsl: "hsl(120, 100%, 20%)", rgb: "rgb(0, 100, 0)"},
-    {authored: "darkgrey", name: "darkgray", hex: "#A9A9A9", hsl: "hsl(0, 0%, 66%)", rgb: "rgb(169, 169, 169)"},
-    {authored: "darkkhaki", name: "darkkhaki", hex: "#BDB76B", hsl: "hsl(56, 38%, 58%)", rgb: "rgb(189, 183, 107)"},
-    {authored: "darkmagenta", name: "darkmagenta", hex: "#8B008B", hsl: "hsl(300, 100%, 27%)", rgb: "rgb(139, 0, 139)"},
-    {authored: "darkolivegreen", name: "darkolivegreen", hex: "#556B2F", hsl: "hsl(82, 39%, 30%)", rgb: "rgb(85, 107, 47)"},
-    {authored: "darkorange", name: "darkorange", hex: "#FF8C00", hsl: "hsl(33, 100%, 50%)", rgb: "rgb(255, 140, 0)"},
-    {authored: "darkorchid", name: "darkorchid", hex: "#9932CC", hsl: "hsl(280, 61%, 50%)", rgb: "rgb(153, 50, 204)"},
-    {authored: "darkred", name: "darkred", hex: "#8B0000", hsl: "hsl(0, 100%, 27%)", rgb: "rgb(139, 0, 0)"},
-    {authored: "darksalmon", name: "darksalmon", hex: "#E9967A", hsl: "hsl(15, 72%, 70%)", rgb: "rgb(233, 150, 122)"},
-    {authored: "darkseagreen", name: "darkseagreen", hex: "#8FBC8F", hsl: "hsl(120, 25%, 65%)", rgb: "rgb(143, 188, 143)"},
-    {authored: "darkslateblue", name: "darkslateblue", hex: "#483D8B", hsl: "hsl(248, 39%, 39%)", rgb: "rgb(72, 61, 139)"},
-    {authored: "darkslategray", name: "darkslategray", hex: "#2F4F4F", hsl: "hsl(180, 25%, 25%)", rgb: "rgb(47, 79, 79)"},
-    {authored: "darkslategrey", name: "darkslategray", hex: "#2F4F4F", hsl: "hsl(180, 25%, 25%)", rgb: "rgb(47, 79, 79)"},
-    {authored: "darkturquoise", name: "darkturquoise", hex: "#00CED1", hsl: "hsl(181, 100%, 41%)", rgb: "rgb(0, 206, 209)"},
-    {authored: "darkviolet", name: "darkviolet", hex: "#9400D3", hsl: "hsl(282, 100%, 41%)", rgb: "rgb(148, 0, 211)"},
-    {authored: "deeppink", name: "deeppink", hex: "#FF1493", hsl: "hsl(328, 100%, 54%)", rgb: "rgb(255, 20, 147)"},
-    {authored: "deepskyblue", name: "deepskyblue", hex: "#00BFFF", hsl: "hsl(195, 100%, 50%)", rgb: "rgb(0, 191, 255)"},
+    {authored: "darkgrey", name: "darkgray", hex: "#a9a9a9", hsl: "hsl(0, 0%, 66%)", rgb: "rgb(169, 169, 169)"},
+    {authored: "darkkhaki", name: "darkkhaki", hex: "#bdb76b", hsl: "hsl(56, 38%, 58%)", rgb: "rgb(189, 183, 107)"},
+    {authored: "darkmagenta", name: "darkmagenta", hex: "#8b008b", hsl: "hsl(300, 100%, 27%)", rgb: "rgb(139, 0, 139)"},
+    {authored: "darkolivegreen", name: "darkolivegreen", hex: "#556b2f", hsl: "hsl(82, 39%, 30%)", rgb: "rgb(85, 107, 47)"},
+    {authored: "darkorange", name: "darkorange", hex: "#ff8c00", hsl: "hsl(33, 100%, 50%)", rgb: "rgb(255, 140, 0)"},
+    {authored: "darkorchid", name: "darkorchid", hex: "#9932cc", hsl: "hsl(280, 61%, 50%)", rgb: "rgb(153, 50, 204)"},
+    {authored: "darkred", name: "darkred", hex: "#8b0000", hsl: "hsl(0, 100%, 27%)", rgb: "rgb(139, 0, 0)"},
+    {authored: "darksalmon", name: "darksalmon", hex: "#e9967a", hsl: "hsl(15, 72%, 70%)", rgb: "rgb(233, 150, 122)"},
+    {authored: "darkseagreen", name: "darkseagreen", hex: "#8fbc8f", hsl: "hsl(120, 25%, 65%)", rgb: "rgb(143, 188, 143)"},
+    {authored: "darkslateblue", name: "darkslateblue", hex: "#483d8b", hsl: "hsl(248, 39%, 39%)", rgb: "rgb(72, 61, 139)"},
+    {authored: "darkslategray", name: "darkslategray", hex: "#2f4f4f", hsl: "hsl(180, 25%, 25%)", rgb: "rgb(47, 79, 79)"},
+    {authored: "darkslategrey", name: "darkslategray", hex: "#2f4f4f", hsl: "hsl(180, 25%, 25%)", rgb: "rgb(47, 79, 79)"},
+    {authored: "darkturquoise", name: "darkturquoise", hex: "#00ced1", hsl: "hsl(181, 100%, 41%)", rgb: "rgb(0, 206, 209)"},
+    {authored: "darkviolet", name: "darkviolet", hex: "#9400d3", hsl: "hsl(282, 100%, 41%)", rgb: "rgb(148, 0, 211)"},
+    {authored: "deeppink", name: "deeppink", hex: "#ff1493", hsl: "hsl(328, 100%, 54%)", rgb: "rgb(255, 20, 147)"},
+    {authored: "deepskyblue", name: "deepskyblue", hex: "#00bfff", hsl: "hsl(195, 100%, 50%)", rgb: "rgb(0, 191, 255)"},
     {authored: "dimgray", name: "dimgray", hex: "#696969", hsl: "hsl(0, 0%, 41%)", rgb: "rgb(105, 105, 105)"},
-    {authored: "dodgerblue", name: "dodgerblue", hex: "#1E90FF", hsl: "hsl(210, 100%, 56%)", rgb: "rgb(30, 144, 255)"},
-    {authored: "firebrick", name: "firebrick", hex: "#B22222", hsl: "hsl(0, 68%, 42%)", rgb: "rgb(178, 34, 34)"},
-    {authored: "floralwhite", name: "floralwhite", hex: "#FFFAF0", hsl: "hsl(40, 100%, 97%)", rgb: "rgb(255, 250, 240)"},
-    {authored: "forestgreen", name: "forestgreen", hex: "#228B22", hsl: "hsl(120, 61%, 34%)", rgb: "rgb(34, 139, 34)"},
-    {authored: "fuchsia", name: "fuchsia", hex: "#F0F", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)"},
-    {authored: "gainsboro", name: "gainsboro", hex: "#DCDCDC", hsl: "hsl(0, 0%, 86%)", rgb: "rgb(220, 220, 220)"},
-    {authored: "ghostwhite", name: "ghostwhite", hex: "#F8F8FF", hsl: "hsl(240, 100%, 99%)", rgb: "rgb(248, 248, 255)"},
-    {authored: "gold", name: "gold", hex: "#FFD700", hsl: "hsl(51, 100%, 50%)", rgb: "rgb(255, 215, 0)"},
-    {authored: "goldenrod", name: "goldenrod", hex: "#DAA520", hsl: "hsl(43, 74%, 49%)", rgb: "rgb(218, 165, 32)"},
+    {authored: "dodgerblue", name: "dodgerblue", hex: "#1e90ff", hsl: "hsl(210, 100%, 56%)", rgb: "rgb(30, 144, 255)"},
+    {authored: "firebrick", name: "firebrick", hex: "#b22222", hsl: "hsl(0, 68%, 42%)", rgb: "rgb(178, 34, 34)"},
+    {authored: "floralwhite", name: "floralwhite", hex: "#fffaf0", hsl: "hsl(40, 100%, 97%)", rgb: "rgb(255, 250, 240)"},
+    {authored: "forestgreen", name: "forestgreen", hex: "#228b22", hsl: "hsl(120, 61%, 34%)", rgb: "rgb(34, 139, 34)"},
+    {authored: "fuchsia", name: "fuchsia", hex: "#f0f", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)"},
+    {authored: "gainsboro", name: "gainsboro", hex: "#dcdcdc", hsl: "hsl(0, 0%, 86%)", rgb: "rgb(220, 220, 220)"},
+    {authored: "ghostwhite", name: "ghostwhite", hex: "#f8f8ff", hsl: "hsl(240, 100%, 99%)", rgb: "rgb(248, 248, 255)"},
+    {authored: "gold", name: "gold", hex: "#ffd700", hsl: "hsl(51, 100%, 50%)", rgb: "rgb(255, 215, 0)"},
+    {authored: "goldenrod", name: "goldenrod", hex: "#daa520", hsl: "hsl(43, 74%, 49%)", rgb: "rgb(218, 165, 32)"},
     {authored: "gray", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50%)", rgb: "rgb(128, 128, 128)"},
     {authored: "green", name: "green", hex: "#008000", hsl: "hsl(120, 100%, 25%)", rgb: "rgb(0, 128, 0)"},
-    {authored: "greenyellow", name: "greenyellow", hex: "#ADFF2F", hsl: "hsl(84, 100%, 59%)", rgb: "rgb(173, 255, 47)"},
+    {authored: "greenyellow", name: "greenyellow", hex: "#adff2f", hsl: "hsl(84, 100%, 59%)", rgb: "rgb(173, 255, 47)"},
     {authored: "grey", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50%)", rgb: "rgb(128, 128, 128)"},
-    {authored: "honeydew", name: "honeydew", hex: "#F0FFF0", hsl: "hsl(120, 100%, 97%)", rgb: "rgb(240, 255, 240)"},
-    {authored: "hotpink", name: "hotpink", hex: "#FF69B4", hsl: "hsl(330, 100%, 71%)", rgb: "rgb(255, 105, 180)"},
-    {authored: "indianred", name: "indianred", hex: "#CD5C5C", hsl: "hsl(0, 53%, 58%)", rgb: "rgb(205, 92, 92)"},
-    {authored: "indigo", name: "indigo", hex: "#4B0082", hsl: "hsl(275, 100%, 25%)", rgb: "rgb(75, 0, 130)"},
-    {authored: "ivory", name: "ivory", hex: "#FFFFF0", hsl: "hsl(60, 100%, 97%)", rgb: "rgb(255, 255, 240)"},
-    {authored: "khaki", name: "khaki", hex: "#F0E68C", hsl: "hsl(54, 77%, 75%)", rgb: "rgb(240, 230, 140)"},
-    {authored: "lavender", name: "lavender", hex: "#E6E6FA", hsl: "hsl(240, 67%, 94%)", rgb: "rgb(230, 230, 250)"},
-    {authored: "lavenderblush", name: "lavenderblush", hex: "#FFF0F5", hsl: "hsl(340, 100%, 97%)", rgb: "rgb(255, 240, 245)"},
-    {authored: "lawngreen", name: "lawngreen", hex: "#7CFC00", hsl: "hsl(90, 100%, 49%)", rgb: "rgb(124, 252, 0)"},
-    {authored: "lemonchiffon", name: "lemonchiffon", hex: "#FFFACD", hsl: "hsl(54, 100%, 90%)", rgb: "rgb(255, 250, 205)"},
-    {authored: "lightblue", name: "lightblue", hex: "#ADD8E6", hsl: "hsl(195, 53%, 79%)", rgb: "rgb(173, 216, 230)"},
-    {authored: "lightcoral", name: "lightcoral", hex: "#F08080", hsl: "hsl(0, 79%, 72%)", rgb: "rgb(240, 128, 128)"},
-    {authored: "lightcyan", name: "lightcyan", hex: "#E0FFFF", hsl: "hsl(180, 100%, 94%)", rgb: "rgb(224, 255, 255)"},
-    {authored: "lightgoldenrodyellow", name: "lightgoldenrodyellow", hex: "#FAFAD2", hsl: "hsl(60, 80%, 90%)", rgb: "rgb(250, 250, 210)"},
-    {authored: "lightgray", name: "lightgray", hex: "#D3D3D3", hsl: "hsl(0, 0%, 83%)", rgb: "rgb(211, 211, 211)"},
-    {authored: "lightgreen", name: "lightgreen", hex: "#90EE90", hsl: "hsl(120, 73%, 75%)", rgb: "rgb(144, 238, 144)"},
-    {authored: "lightgrey", name: "lightgray", hex: "#D3D3D3", hsl: "hsl(0, 0%, 83%)", rgb: "rgb(211, 211, 211)"},
-    {authored: "lightpink", name: "lightpink", hex: "#FFB6C1", hsl: "hsl(351, 100%, 86%)", rgb: "rgb(255, 182, 193)"},
-    {authored: "lightsalmon", name: "lightsalmon", hex: "#FFA07A", hsl: "hsl(17, 100%, 74%)", rgb: "rgb(255, 160, 122)"},
-    {authored: "lightseagreen", name: "lightseagreen", hex: "#20B2AA", hsl: "hsl(177, 70%, 41%)", rgb: "rgb(32, 178, 170)"},
-    {authored: "lightskyblue", name: "lightskyblue", hex: "#87CEFA", hsl: "hsl(203, 92%, 75%)", rgb: "rgb(135, 206, 250)"},
+    {authored: "honeydew", name: "honeydew", hex: "#f0fff0", hsl: "hsl(120, 100%, 97%)", rgb: "rgb(240, 255, 240)"},
+    {authored: "hotpink", name: "hotpink", hex: "#ff69b4", hsl: "hsl(330, 100%, 71%)", rgb: "rgb(255, 105, 180)"},
+    {authored: "indianred", name: "indianred", hex: "#cd5c5c", hsl: "hsl(0, 53%, 58%)", rgb: "rgb(205, 92, 92)"},
+    {authored: "indigo", name: "indigo", hex: "#4b0082", hsl: "hsl(275, 100%, 25%)", rgb: "rgb(75, 0, 130)"},
+    {authored: "ivory", name: "ivory", hex: "#fffff0", hsl: "hsl(60, 100%, 97%)", rgb: "rgb(255, 255, 240)"},
+    {authored: "khaki", name: "khaki", hex: "#f0e68c", hsl: "hsl(54, 77%, 75%)", rgb: "rgb(240, 230, 140)"},
+    {authored: "lavender", name: "lavender", hex: "#e6e6fa", hsl: "hsl(240, 67%, 94%)", rgb: "rgb(230, 230, 250)"},
+    {authored: "lavenderblush", name: "lavenderblush", hex: "#fff0f5", hsl: "hsl(340, 100%, 97%)", rgb: "rgb(255, 240, 245)"},
+    {authored: "lawngreen", name: "lawngreen", hex: "#7cfc00", hsl: "hsl(90, 100%, 49%)", rgb: "rgb(124, 252, 0)"},
+    {authored: "lemonchiffon", name: "lemonchiffon", hex: "#fffacd", hsl: "hsl(54, 100%, 90%)", rgb: "rgb(255, 250, 205)"},
+    {authored: "lightblue", name: "lightblue", hex: "#add8e6", hsl: "hsl(195, 53%, 79%)", rgb: "rgb(173, 216, 230)"},
+    {authored: "lightcoral", name: "lightcoral", hex: "#f08080", hsl: "hsl(0, 79%, 72%)", rgb: "rgb(240, 128, 128)"},
+    {authored: "lightcyan", name: "lightcyan", hex: "#e0ffff", hsl: "hsl(180, 100%, 94%)", rgb: "rgb(224, 255, 255)"},
+    {authored: "lightgoldenrodyellow", name: "lightgoldenrodyellow", hex: "#fafad2", hsl: "hsl(60, 80%, 90%)", rgb: "rgb(250, 250, 210)"},
+    {authored: "lightgray", name: "lightgray", hex: "#d3d3d3", hsl: "hsl(0, 0%, 83%)", rgb: "rgb(211, 211, 211)"},
+    {authored: "lightgreen", name: "lightgreen", hex: "#90ee90", hsl: "hsl(120, 73%, 75%)", rgb: "rgb(144, 238, 144)"},
+    {authored: "lightgrey", name: "lightgray", hex: "#d3d3d3", hsl: "hsl(0, 0%, 83%)", rgb: "rgb(211, 211, 211)"},
+    {authored: "lightpink", name: "lightpink", hex: "#ffb6c1", hsl: "hsl(351, 100%, 86%)", rgb: "rgb(255, 182, 193)"},
+    {authored: "lightsalmon", name: "lightsalmon", hex: "#ffa07a", hsl: "hsl(17, 100%, 74%)", rgb: "rgb(255, 160, 122)"},
+    {authored: "lightseagreen", name: "lightseagreen", hex: "#20b2aa", hsl: "hsl(177, 70%, 41%)", rgb: "rgb(32, 178, 170)"},
+    {authored: "lightskyblue", name: "lightskyblue", hex: "#87cefa", hsl: "hsl(203, 92%, 75%)", rgb: "rgb(135, 206, 250)"},
     {authored: "lightslategray", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14%, 53%)", rgb: "rgb(119, 136, 153)"},
     {authored: "lightslategrey", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14%, 53%)", rgb: "rgb(119, 136, 153)"},
-    {authored: "lightsteelblue", name: "lightsteelblue", hex: "#B0C4DE", hsl: "hsl(214, 41%, 78%)", rgb: "rgb(176, 196, 222)"},
-    {authored: "lightyellow", name: "lightyellow", hex: "#FFFFE0", hsl: "hsl(60, 100%, 94%)", rgb: "rgb(255, 255, 224)"},
-    {authored: "lime", name: "lime", hex: "#0F0", hsl: "hsl(120, 100%, 50%)", rgb: "rgb(0, 255, 0)"},
-    {authored: "limegreen", name: "limegreen", hex: "#32CD32", hsl: "hsl(120, 61%, 50%)", rgb: "rgb(50, 205, 50)"},
-    {authored: "linen", name: "linen", hex: "#FAF0E6", hsl: "hsl(30, 67%, 94%)", rgb: "rgb(250, 240, 230)"},
-    {authored: "magenta", name: "fuchsia", hex: "#F0F", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)"},
+    {authored: "lightsteelblue", name: "lightsteelblue", hex: "#b0c4de", hsl: "hsl(214, 41%, 78%)", rgb: "rgb(176, 196, 222)"},
+    {authored: "lightyellow", name: "lightyellow", hex: "#ffffe0", hsl: "hsl(60, 100%, 94%)", rgb: "rgb(255, 255, 224)"},
+    {authored: "lime", name: "lime", hex: "#0f0", hsl: "hsl(120, 100%, 50%)", rgb: "rgb(0, 255, 0)"},
+    {authored: "limegreen", name: "limegreen", hex: "#32cd32", hsl: "hsl(120, 61%, 50%)", rgb: "rgb(50, 205, 50)"},
+    {authored: "linen", name: "linen", hex: "#faf0e6", hsl: "hsl(30, 67%, 94%)", rgb: "rgb(250, 240, 230)"},
+    {authored: "magenta", name: "fuchsia", hex: "#f0f", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)"},
     {authored: "maroon", name: "maroon", hex: "#800000", hsl: "hsl(0, 100%, 25%)", rgb: "rgb(128, 0, 0)"},
-    {authored: "mediumaquamarine", name: "mediumaquamarine", hex: "#66CDAA", hsl: "hsl(160, 51%, 60%)", rgb: "rgb(102, 205, 170)"},
-    {authored: "mediumblue", name: "mediumblue", hex: "#0000CD", hsl: "hsl(240, 100%, 40%)", rgb: "rgb(0, 0, 205)"},
-    {authored: "mediumorchid", name: "mediumorchid", hex: "#BA55D3", hsl: "hsl(288, 59%, 58%)", rgb: "rgb(186, 85, 211)"},
-    {authored: "mediumpurple", name: "mediumpurple", hex: "#9370DB", hsl: "hsl(260, 60%, 65%)", rgb: "rgb(147, 112, 219)"},
-    {authored: "mediumseagreen", name: "mediumseagreen", hex: "#3CB371", hsl: "hsl(147, 50%, 47%)", rgb: "rgb(60, 179, 113)"},
-    {authored: "mediumslateblue", name: "mediumslateblue", hex: "#7B68EE", hsl: "hsl(249, 80%, 67%)", rgb: "rgb(123, 104, 238)"},
-    {authored: "mediumspringgreen", name: "mediumspringgreen", hex: "#00FA9A", hsl: "hsl(157, 100%, 49%)", rgb: "rgb(0, 250, 154)"},
-    {authored: "mediumturquoise", name: "mediumturquoise", hex: "#48D1CC", hsl: "hsl(178, 60%, 55%)", rgb: "rgb(72, 209, 204)"},
-    {authored: "mediumvioletred", name: "mediumvioletred", hex: "#C71585", hsl: "hsl(322, 81%, 43%)", rgb: "rgb(199, 21, 133)"},
+    {authored: "mediumaquamarine", name: "mediumaquamarine", hex: "#66cdaa", hsl: "hsl(160, 51%, 60%)", rgb: "rgb(102, 205, 170)"},
+    {authored: "mediumblue", name: "mediumblue", hex: "#0000cd", hsl: "hsl(240, 100%, 40%)", rgb: "rgb(0, 0, 205)"},
+    {authored: "mediumorchid", name: "mediumorchid", hex: "#ba55d3", hsl: "hsl(288, 59%, 58%)", rgb: "rgb(186, 85, 211)"},
+    {authored: "mediumpurple", name: "mediumpurple", hex: "#9370db", hsl: "hsl(260, 60%, 65%)", rgb: "rgb(147, 112, 219)"},
+    {authored: "mediumseagreen", name: "mediumseagreen", hex: "#3cb371", hsl: "hsl(147, 50%, 47%)", rgb: "rgb(60, 179, 113)"},
+    {authored: "mediumslateblue", name: "mediumslateblue", hex: "#7b68ee", hsl: "hsl(249, 80%, 67%)", rgb: "rgb(123, 104, 238)"},
+    {authored: "mediumspringgreen", name: "mediumspringgreen", hex: "#00fa9a", hsl: "hsl(157, 100%, 49%)", rgb: "rgb(0, 250, 154)"},
+    {authored: "mediumturquoise", name: "mediumturquoise", hex: "#48d1cc", hsl: "hsl(178, 60%, 55%)", rgb: "rgb(72, 209, 204)"},
+    {authored: "mediumvioletred", name: "mediumvioletred", hex: "#c71585", hsl: "hsl(322, 81%, 43%)", rgb: "rgb(199, 21, 133)"},
     {authored: "midnightblue", name: "midnightblue", hex: "#191970", hsl: "hsl(240, 64%, 27%)", rgb: "rgb(25, 25, 112)"},
-    {authored: "mintcream", name: "mintcream", hex: "#F5FFFA", hsl: "hsl(150, 100%, 98%)", rgb: "rgb(245, 255, 250)"},
-    {authored: "mistyrose", name: "mistyrose", hex: "#FFE4E1", hsl: "hsl(6, 100%, 94%)", rgb: "rgb(255, 228, 225)"},
-    {authored: "moccasin", name: "moccasin", hex: "#FFE4B5", hsl: "hsl(38, 100%, 85%)", rgb: "rgb(255, 228, 181)"},
-    {authored: "navajowhite", name: "navajowhite", hex: "#FFDEAD", hsl: "hsl(36, 100%, 84%)", rgb: "rgb(255, 222, 173)"},
+    {authored: "mintcream", name: "mintcream", hex: "#f5fffa", hsl: "hsl(150, 100%, 98%)", rgb: "rgb(245, 255, 250)"},
+    {authored: "mistyrose", name: "mistyrose", hex: "#ffe4e1", hsl: "hsl(6, 100%, 94%)", rgb: "rgb(255, 228, 225)"},
+    {authored: "moccasin", name: "moccasin", hex: "#ffe4b5", hsl: "hsl(38, 100%, 85%)", rgb: "rgb(255, 228, 181)"},
+    {authored: "navajowhite", name: "navajowhite", hex: "#ffdead", hsl: "hsl(36, 100%, 84%)", rgb: "rgb(255, 222, 173)"},
     {authored: "navy", name: "navy", hex: "#000080", hsl: "hsl(240, 100%, 25%)", rgb: "rgb(0, 0, 128)"},
-    {authored: "oldlace", name: "oldlace", hex: "#FDF5E6", hsl: "hsl(39, 85%, 95%)", rgb: "rgb(253, 245, 230)"},
+    {authored: "oldlace", name: "oldlace", hex: "#fdf5e6", hsl: "hsl(39, 85%, 95%)", rgb: "rgb(253, 245, 230)"},
     {authored: "olive", name: "olive", hex: "#808000", hsl: "hsl(60, 100%, 25%)", rgb: "rgb(128, 128, 0)"},
-    {authored: "olivedrab", name: "olivedrab", hex: "#6B8E23", hsl: "hsl(80, 60%, 35%)", rgb: "rgb(107, 142, 35)"},
-    {authored: "orange", name: "orange", hex: "#FFA500", hsl: "hsl(39, 100%, 50%)", rgb: "rgb(255, 165, 0)"},
-    {authored: "orangered", name: "orangered", hex: "#FF4500", hsl: "hsl(16, 100%, 50%)", rgb: "rgb(255, 69, 0)"},
-    {authored: "orchid", name: "orchid", hex: "#DA70D6", hsl: "hsl(302, 59%, 65%)", rgb: "rgb(218, 112, 214)"},
-    {authored: "palegoldenrod", name: "palegoldenrod", hex: "#EEE8AA", hsl: "hsl(55, 67%, 80%)", rgb: "rgb(238, 232, 170)"},
-    {authored: "palegreen", name: "palegreen", hex: "#98FB98", hsl: "hsl(120, 93%, 79%)", rgb: "rgb(152, 251, 152)"},
-    {authored: "paleturquoise", name: "paleturquoise", hex: "#AFEEEE", hsl: "hsl(180, 65%, 81%)", rgb: "rgb(175, 238, 238)"},
-    {authored: "palevioletred", name: "palevioletred", hex: "#DB7093", hsl: "hsl(340, 60%, 65%)", rgb: "rgb(219, 112, 147)"},
-    {authored: "papayawhip", name: "papayawhip", hex: "#FFEFD5", hsl: "hsl(37, 100%, 92%)", rgb: "rgb(255, 239, 213)"},
-    {authored: "peachpuff", name: "peachpuff", hex: "#FFDAB9", hsl: "hsl(28, 100%, 86%)", rgb: "rgb(255, 218, 185)"},
-    {authored: "peru", name: "peru", hex: "#CD853F", hsl: "hsl(30, 59%, 53%)", rgb: "rgb(205, 133, 63)"},
-    {authored: "pink", name: "pink", hex: "#FFC0CB", hsl: "hsl(350, 100%, 88%)", rgb: "rgb(255, 192, 203)"},
-    {authored: "plum", name: "plum", hex: "#DDA0DD", hsl: "hsl(300, 47%, 75%)", rgb: "rgb(221, 160, 221)"},
-    {authored: "powderblue", name: "powderblue", hex: "#B0E0E6", hsl: "hsl(187, 52%, 80%)", rgb: "rgb(176, 224, 230)"},
+    {authored: "olivedrab", name: "olivedrab", hex: "#6b8e23", hsl: "hsl(80, 60%, 35%)", rgb: "rgb(107, 142, 35)"},
+    {authored: "orange", name: "orange", hex: "#ffa500", hsl: "hsl(39, 100%, 50%)", rgb: "rgb(255, 165, 0)"},
+    {authored: "orangered", name: "orangered", hex: "#ff4500", hsl: "hsl(16, 100%, 50%)", rgb: "rgb(255, 69, 0)"},
+    {authored: "orchid", name: "orchid", hex: "#da70d6", hsl: "hsl(302, 59%, 65%)", rgb: "rgb(218, 112, 214)"},
+    {authored: "palegoldenrod", name: "palegoldenrod", hex: "#eee8aa", hsl: "hsl(55, 67%, 80%)", rgb: "rgb(238, 232, 170)"},
+    {authored: "palegreen", name: "palegreen", hex: "#98fb98", hsl: "hsl(120, 93%, 79%)", rgb: "rgb(152, 251, 152)"},
+    {authored: "paleturquoise", name: "paleturquoise", hex: "#afeeee", hsl: "hsl(180, 65%, 81%)", rgb: "rgb(175, 238, 238)"},
+    {authored: "palevioletred", name: "palevioletred", hex: "#db7093", hsl: "hsl(340, 60%, 65%)", rgb: "rgb(219, 112, 147)"},
+    {authored: "papayawhip", name: "papayawhip", hex: "#ffefd5", hsl: "hsl(37, 100%, 92%)", rgb: "rgb(255, 239, 213)"},
+    {authored: "peachpuff", name: "peachpuff", hex: "#ffdab9", hsl: "hsl(28, 100%, 86%)", rgb: "rgb(255, 218, 185)"},
+    {authored: "peru", name: "peru", hex: "#cd853f", hsl: "hsl(30, 59%, 53%)", rgb: "rgb(205, 133, 63)"},
+    {authored: "pink", name: "pink", hex: "#ffc0cb", hsl: "hsl(350, 100%, 88%)", rgb: "rgb(255, 192, 203)"},
+    {authored: "plum", name: "plum", hex: "#dda0dd", hsl: "hsl(300, 47%, 75%)", rgb: "rgb(221, 160, 221)"},
+    {authored: "powderblue", name: "powderblue", hex: "#b0e0e6", hsl: "hsl(187, 52%, 80%)", rgb: "rgb(176, 224, 230)"},
     {authored: "purple", name: "purple", hex: "#800080", hsl: "hsl(300, 100%, 25%)", rgb: "rgb(128, 0, 128)"},
     {authored: "rebeccapurple", name: "rebeccapurple", hex: "#639", hsl: "hsl(270, 50%, 40%)", rgb: "rgb(102, 51, 153)"},
-    {authored: "red", name: "red", hex: "#F00", hsl: "hsl(0, 100%, 50%)", rgb: "rgb(255, 0, 0)"},
-    {authored: "rosybrown", name: "rosybrown", hex: "#BC8F8F", hsl: "hsl(0, 25%, 65%)", rgb: "rgb(188, 143, 143)"},
-    {authored: "royalblue", name: "royalblue", hex: "#4169E1", hsl: "hsl(225, 73%, 57%)", rgb: "rgb(65, 105, 225)"},
-    {authored: "saddlebrown", name: "saddlebrown", hex: "#8B4513", hsl: "hsl(25, 76%, 31%)", rgb: "rgb(139, 69, 19)"},
-    {authored: "salmon", name: "salmon", hex: "#FA8072", hsl: "hsl(6, 93%, 71%)", rgb: "rgb(250, 128, 114)"},
-    {authored: "sandybrown", name: "sandybrown", hex: "#F4A460", hsl: "hsl(28, 87%, 67%)", rgb: "rgb(244, 164, 96)"},
-    {authored: "seagreen", name: "seagreen", hex: "#2E8B57", hsl: "hsl(146, 50%, 36%)", rgb: "rgb(46, 139, 87)"},
-    {authored: "seashell", name: "seashell", hex: "#FFF5EE", hsl: "hsl(25, 100%, 97%)", rgb: "rgb(255, 245, 238)"},
-    {authored: "sienna", name: "sienna", hex: "#A0522D", hsl: "hsl(19, 56%, 40%)", rgb: "rgb(160, 82, 45)"},
-    {authored: "silver", name: "silver", hex: "#C0C0C0", hsl: "hsl(0, 0%, 75%)", rgb: "rgb(192, 192, 192)"},
-    {authored: "skyblue", name: "skyblue", hex: "#87CEEB", hsl: "hsl(197, 71%, 73%)", rgb: "rgb(135, 206, 235)"},
-    {authored: "slateblue", name: "slateblue", hex: "#6A5ACD", hsl: "hsl(248, 53%, 58%)", rgb: "rgb(106, 90, 205)"},
+    {authored: "red", name: "red", hex: "#f00", hsl: "hsl(0, 100%, 50%)", rgb: "rgb(255, 0, 0)"},
+    {authored: "rosybrown", name: "rosybrown", hex: "#bc8f8f", hsl: "hsl(0, 25%, 65%)", rgb: "rgb(188, 143, 143)"},
+    {authored: "royalblue", name: "royalblue", hex: "#4169e1", hsl: "hsl(225, 73%, 57%)", rgb: "rgb(65, 105, 225)"},
+    {authored: "saddlebrown", name: "saddlebrown", hex: "#8b4513", hsl: "hsl(25, 76%, 31%)", rgb: "rgb(139, 69, 19)"},
+    {authored: "salmon", name: "salmon", hex: "#fa8072", hsl: "hsl(6, 93%, 71%)", rgb: "rgb(250, 128, 114)"},
+    {authored: "sandybrown", name: "sandybrown", hex: "#f4a460", hsl: "hsl(28, 87%, 67%)", rgb: "rgb(244, 164, 96)"},
+    {authored: "seagreen", name: "seagreen", hex: "#2e8b57", hsl: "hsl(146, 50%, 36%)", rgb: "rgb(46, 139, 87)"},
+    {authored: "seashell", name: "seashell", hex: "#fff5ee", hsl: "hsl(25, 100%, 97%)", rgb: "rgb(255, 245, 238)"},
+    {authored: "sienna", name: "sienna", hex: "#a0522d", hsl: "hsl(19, 56%, 40%)", rgb: "rgb(160, 82, 45)"},
+    {authored: "silver", name: "silver", hex: "#c0c0c0", hsl: "hsl(0, 0%, 75%)", rgb: "rgb(192, 192, 192)"},
+    {authored: "skyblue", name: "skyblue", hex: "#87ceeb", hsl: "hsl(197, 71%, 73%)", rgb: "rgb(135, 206, 235)"},
+    {authored: "slateblue", name: "slateblue", hex: "#6a5acd", hsl: "hsl(248, 53%, 58%)", rgb: "rgb(106, 90, 205)"},
     {authored: "slategray", name: "slategray", hex: "#708090", hsl: "hsl(210, 13%, 50%)", rgb: "rgb(112, 128, 144)"},
     {authored: "slategrey", name: "slategray", hex: "#708090", hsl: "hsl(210, 13%, 50%)", rgb: "rgb(112, 128, 144)"},
-    {authored: "snow", name: "snow", hex: "#FFFAFA", hsl: "hsl(0, 100%, 99%)", rgb: "rgb(255, 250, 250)"},
-    {authored: "springgreen", name: "springgreen", hex: "#00FF7F", hsl: "hsl(150, 100%, 50%)", rgb: "rgb(0, 255, 127)"},
-    {authored: "steelblue", name: "steelblue", hex: "#4682B4", hsl: "hsl(207, 44%, 49%)", rgb: "rgb(70, 130, 180)"},
-    {authored: "tan", name: "tan", hex: "#D2B48C", hsl: "hsl(34, 44%, 69%)", rgb: "rgb(210, 180, 140)"},
+    {authored: "snow", name: "snow", hex: "#fffafa", hsl: "hsl(0, 100%, 99%)", rgb: "rgb(255, 250, 250)"},
+    {authored: "springgreen", name: "springgreen", hex: "#00ff7f", hsl: "hsl(150, 100%, 50%)", rgb: "rgb(0, 255, 127)"},
+    {authored: "steelblue", name: "steelblue", hex: "#4682b4", hsl: "hsl(207, 44%, 49%)", rgb: "rgb(70, 130, 180)"},
+    {authored: "tan", name: "tan", hex: "#d2b48c", hsl: "hsl(34, 44%, 69%)", rgb: "rgb(210, 180, 140)"},
     {authored: "teal", name: "teal", hex: "#008080", hsl: "hsl(180, 100%, 25%)", rgb: "rgb(0, 128, 128)"},
-    {authored: "thistle", name: "thistle", hex: "#D8BFD8", hsl: "hsl(300, 24%, 80%)", rgb: "rgb(216, 191, 216)"},
-    {authored: "tomato", name: "tomato", hex: "#FF6347", hsl: "hsl(9, 100%, 64%)", rgb: "rgb(255, 99, 71)"},
-    {authored: "turquoise", name: "turquoise", hex: "#40E0D0", hsl: "hsl(174, 72%, 56%)", rgb: "rgb(64, 224, 208)"},
-    {authored: "violet", name: "violet", hex: "#EE82EE", hsl: "hsl(300, 76%, 72%)", rgb: "rgb(238, 130, 238)"},
-    {authored: "wheat", name: "wheat", hex: "#F5DEB3", hsl: "hsl(39, 77%, 83%)", rgb: "rgb(245, 222, 179)"},
-    {authored: "white", name: "white", hex: "#FFF", hsl: "hsl(0, 0%, 100%)", rgb: "rgb(255, 255, 255)"},
-    {authored: "whitesmoke", name: "whitesmoke", hex: "#F5F5F5", hsl: "hsl(0, 0%, 96%)", rgb: "rgb(245, 245, 245)"},
-    {authored: "yellow", name: "yellow", hex: "#FF0", hsl: "hsl(60, 100%, 50%)", rgb: "rgb(255, 255, 0)"},
-    {authored: "yellowgreen", name: "yellowgreen", hex: "#9ACD32", hsl: "hsl(80, 61%, 50%)", rgb: "rgb(154, 205, 50)"},
+    {authored: "thistle", name: "thistle", hex: "#d8bfd8", hsl: "hsl(300, 24%, 80%)", rgb: "rgb(216, 191, 216)"},
+    {authored: "tomato", name: "tomato", hex: "#ff6347", hsl: "hsl(9, 100%, 64%)", rgb: "rgb(255, 99, 71)"},
+    {authored: "turquoise", name: "turquoise", hex: "#40e0d0", hsl: "hsl(174, 72%, 56%)", rgb: "rgb(64, 224, 208)"},
+    {authored: "violet", name: "violet", hex: "#ee82ee", hsl: "hsl(300, 76%, 72%)", rgb: "rgb(238, 130, 238)"},
+    {authored: "wheat", name: "wheat", hex: "#f5deb3", hsl: "hsl(39, 77%, 83%)", rgb: "rgb(245, 222, 179)"},
+    {authored: "white", name: "white", hex: "#fff", hsl: "hsl(0, 0%, 100%)", rgb: "rgb(255, 255, 255)"},
+    {authored: "whitesmoke", name: "whitesmoke", hex: "#f5f5f5", hsl: "hsl(0, 0%, 96%)", rgb: "rgb(245, 245, 245)"},
+    {authored: "yellow", name: "yellow", hex: "#ff0", hsl: "hsl(60, 100%, 50%)", rgb: "rgb(255, 255, 0)"},
+    {authored: "yellowgreen", name: "yellowgreen", hex: "#9acd32", hsl: "hsl(80, 61%, 50%)", rgb: "rgb(154, 205, 50)"},
     {authored: "rgba(0, 0, 0, 0)", name: "rgba(0, 0, 0, 0)", hex: "rgba(0, 0, 0, 0)", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)"},
     {authored: "hsla(0, 0%, 0%, 0)", name: "rgba(0, 0, 0, 0)", hex: "rgba(0, 0, 0, 0)", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)"},
     {authored: "rgba(50, 60, 70, 0.5)", name: "rgba(50, 60, 70, 0.5)", hex: "rgba(50, 60, 70, 0.5)", hsl: "hsla(210, 17%, 24%, 0.5)", rgb: "rgba(50, 60, 70, 0.5)"},
     {authored: "rgba(0, 0, 0, 0.3)", name: "rgba(0, 0, 0, 0.3)", hex: "rgba(0, 0, 0, 0.3)", hsl: "hsla(0, 0%, 0%, 0.3)", rgb: "rgba(0, 0, 0, 0.3)"},
     {authored: "rgba(255, 255, 255, 0.6)", name: "rgba(255, 255, 255, 0.6)", hex: "rgba(255, 255, 255, 0.6)", hsl: "hsla(0, 0%, 100%, 0.6)", rgb: "rgba(255, 255, 255, 0.6)"},
-    {authored: "rgba(127, 89, 45, 1)", name: "#7F592D", hex: "#7F592D", hsl: "hsl(32, 48%, 34%)", rgb: "rgb(127, 89, 45)"},
-    {authored: "hsla(19.304, 56%, 40%, 1)", name: "#9F512C", hex: "#9F512C", hsl: "hsl(19, 57%, 40%)", rgb: "rgb(159, 81, 44)"},
+    {authored: "rgba(127, 89, 45, 1)", name: "#7f592d", hex: "#7f592d", hsl: "hsl(32, 48%, 34%)", rgb: "rgb(127, 89, 45)"},
+    {authored: "hsla(19.304, 56%, 40%, 1)", name: "#9f512c", hex: "#9f512c", hsl: "hsl(19, 57%, 40%)", rgb: "rgb(159, 81, 44)"},
     {authored: "currentcolor", name: "currentcolor", hex: "currentcolor", hsl: "currentcolor", rgb: "currentcolor"},
     {authored: "inherit", name: "inherit", hex: "inherit", hsl: "inherit", rgb: "inherit"},
     {authored: "initial", name: "initial", hex: "initial", hsl: "initial", rgb: "initial"},
     {authored: "invalidColor", name: "", hex: "", hsl: "", rgb: ""},
     {authored: "transparent", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent"},
     {authored: "unset", name: "unset", hex: "unset", hsl: "unset", rgb: "unset"}
   ];
 }
--- a/devtools/client/styleinspector/test/browser_computedview_cycle_color.js
+++ b/devtools/client/styleinspector/test/browser_computedview_cycle_color.js
@@ -4,17 +4,17 @@
 
 "use strict";
 
 // Computed view color cycling test.
 
 const TEST_URI = `
   <style type="text/css">
     .matches {
-      color: #F00;
+      color: #f00;
     }
   </style>
   <span id="matches" class="matches">Some styled text</span>
 `;
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openComputedView();
@@ -36,17 +36,17 @@ function checkColorCycling(container, in
 
   // "Authored" (default; currently the computed value)
   is(valueNode.textContent, "rgb(255, 0, 0)",
                             "Color displayed as an RGB value.");
 
   // Hex
   EventUtils.synthesizeMouseAtCenter(swatch,
                                      {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "#F00", "Color displayed as a hex value.");
+  is(valueNode.textContent, "#f00", "Color displayed as a hex value.");
 
   // HSL
   EventUtils.synthesizeMouseAtCenter(swatch,
                                      {type: "mousedown", shiftKey: true}, win);
   is(valueNode.textContent, "hsl(0, 100%, 50%)",
                             "Color displayed as an HSL value.");
 
   // RGB
--- a/devtools/client/styleinspector/test/browser_ruleview_authored.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_authored.js
@@ -86,18 +86,20 @@ function* overrideTest() {
     let prop = rule.textProps[i];
     is(prop.overridden || !prop.enabled, i !== 0,
        "post-change check overridden for " + i);
   }
 }
 
 function* colorEditingTest() {
   let colors = [
-    {name: "hex", text: "#f0c", result: "#0F0"},
-    {name: "rgb", text: "rgb(0,128,250)", result: "rgb(0, 255, 0)"}
+    {name: "hex", text: "#f0c", result: "#0f0"},
+    {name: "rgb", text: "rgb(0,128,250)", result: "rgb(0, 255, 0)"},
+    // Test case preservation.
+    {name: "hex", text: "#F0C", result: "#0F0"},
   ];
 
   Services.prefs.setCharPref("devtools.defaultColorUnit", "authored");
 
   for (let color of colors) {
     let view = yield createTestContent("#testid {" +
                                        "  color: " + color.text + ";" +
                                        "} ");
--- a/devtools/client/styleinspector/test/browser_ruleview_colorUnit.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_colorUnit.js
@@ -12,17 +12,17 @@ const TEST_URI = `
       color: blue;
     }
   </style>
   <div id='testid' class='testclass'>Styled Node</div>
 `;
 
 add_task(function*() {
   let TESTS = [
-    {name: "hex", result: "#0F0"},
+    {name: "hex", result: "#0f0"},
     {name: "rgb", result: "rgb(0, 255, 0)"}
   ];
 
   for (let {name, result} of TESTS) {
     info("starting test for " + name);
     Services.prefs.setCharPref("devtools.defaultColorUnit", name);
 
     yield addTab("data:text/html;charset=utf-8," +
--- a/devtools/client/styleinspector/test/browser_ruleview_cycle-color.js
+++ b/devtools/client/styleinspector/test/browser_ruleview_cycle-color.js
@@ -4,17 +4,17 @@
 
 "use strict";
 
 // Test cycling color types in the rule view.
 
 const TEST_URI = `
   <style type="text/css">
     body {
-      color: #F00;
+      color: #f00;
     }
   </style>
   Test cycling color types in the rule view!
 `;
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
@@ -23,17 +23,17 @@ add_task(function*() {
 });
 
 function checkColorCycling(container, inspector) {
   let swatch = container.querySelector(".ruleview-colorswatch");
   let valueNode = container.querySelector(".ruleview-color");
   let win = inspector.sidebar.getWindowForTab("ruleview");
 
   // Hex
-  is(valueNode.textContent, "#F00", "Color displayed as a hex value.");
+  is(valueNode.textContent, "#f00", "Color displayed as a hex value.");
 
   // HSL
   EventUtils.synthesizeMouseAtCenter(swatch,
                                      {type: "mousedown", shiftKey: true}, win);
   is(valueNode.textContent, "hsl(0, 100%, 50%)",
                             "Color displayed as an HSL value.");
 
   // RGB
@@ -46,17 +46,17 @@ function checkColorCycling(container, in
   EventUtils.synthesizeMouseAtCenter(swatch,
                                      {type: "mousedown", shiftKey: true}, win);
   is(valueNode.textContent, "red",
                             "Color displayed as a color name.");
 
   // "Authored"
   EventUtils.synthesizeMouseAtCenter(swatch,
                                      {type: "mousedown", shiftKey: true}, win);
-  is(valueNode.textContent, "#F00",
+  is(valueNode.textContent, "#f00",
                             "Color displayed as an authored value.");
 
   // One more click skips hex, because it is the same as authored, and
   // instead goes back to HSL.
   EventUtils.synthesizeMouseAtCenter(swatch,
                                      {type: "mousedown", shiftKey: true}, win);
   is(valueNode.textContent, "hsl(0, 100%, 50%)",
                             "Color displayed as an HSL value again.");
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -344,34 +344,36 @@ body {
 
 .animation-target {
   background-color: var(--theme-toolbar-background);
   padding: 1px 4px;
   box-sizing: border-box;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
+  cursor: pointer;
 }
 
 .animation-target .attribute-name {
   padding-left: 4px;
 }
 
-.animation-target .node-selector {
+.animation-target .node-highlighter {
   background: url("chrome://devtools/skin/themes/images/vview-open-inspector.png") no-repeat 0 0;
   padding-left: 16px;
   margin-right: 5px;
   cursor: pointer;
 }
 
-.animation-target .node-selector:hover {
+.animation-target .node-highlighter:hover {
   background-position: -32px 0;
 }
 
-.animation-target .node-selector:active {
+.animation-target .node-highlighter:active,
+.animation-target .node-highlighter.selected {
   background-position: -16px 0;
 }
 
 /* Animation title gutter, contains the name, duration, iteration */
 
 .animation-title {
   background-color: var(--theme-toolbar-background);
   border-bottom: 1px solid var(--theme-splitter-color);