Bug 1090209 - Part 1 Drop the window type from the url that opens a Loop conversation window, and pass it in the call data instead. r=nperriault a=loop-only
authorMark Banner <standard8@mozilla.com>
Mon, 03 Nov 2014 16:34:02 +0000
changeset 235109 6fd86797e66d1290c1cc2dde276225018959c48b
parent 235108 ed92dd8406328b4ba6671756075580ac100870e5
child 235110 fa172bfb0bfc1b5bc73996637f18e027c40c0ae9
push id611
push userraliiev@mozilla.com
push dateMon, 05 Jan 2015 23:23:16 +0000
treeherdermozilla-release@345cd3b9c445 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnperriault, loop-only
bugs1090209
milestone35.0a2
Bug 1090209 - Part 1 Drop the window type from the url that opens a Loop conversation window, and pass it in the call data instead. r=nperriault a=loop-only Also creates a ConversationAppStore for managing the overall window data and selection of the type of window it is for the views.
browser/components/loop/LoopCalls.jsm
browser/components/loop/content/conversation.html
browser/components/loop/content/js/conversation.js
browser/components/loop/content/js/conversation.jsx
browser/components/loop/content/js/conversationAppStore.js
browser/components/loop/content/js/conversationViews.js
browser/components/loop/content/js/conversationViews.jsx
browser/components/loop/content/shared/js/actions.js
browser/components/loop/content/shared/js/conversationStore.js
browser/components/loop/content/shared/js/localRoomStore.js
browser/components/loop/jar.mn
browser/components/loop/test/desktop-local/conversationAppStore_test.js
browser/components/loop/test/desktop-local/conversationViews_test.js
browser/components/loop/test/desktop-local/conversation_test.js
browser/components/loop/test/desktop-local/index.html
browser/components/loop/test/shared/conversationStore_test.js
browser/components/loop/test/shared/dispatcher_test.js
browser/components/loop/test/shared/localRoomStore_test.js
browser/components/loop/test/xpcshell/test_loopservice_directcall.js
--- a/browser/components/loop/LoopCalls.jsm
+++ b/browser/components/loop/LoopCalls.jsm
@@ -247,64 +247,66 @@ let LoopCallsInternal = {
     try {
       let respData = JSON.parse(response.body);
       if (respData.calls && Array.isArray(respData.calls)) {
         respData.calls.forEach((callData) => {
           if (!this.callsData.inUse) {
             callData.sessionType = sessionType;
             // XXX Bug 1090209 will transiton into a better window id.
             callData.windowId = callData.callId;
-            this._startCall(callData, "incoming");
+            callData.type = "incoming";
+            this._startCall(callData);
           } else {
             this._returnBusy(callData);
           }
         });
       } else {
         MozLoopService.logwarn("Error: missing calls[] in response");
       }
     } catch (err) {
       MozLoopService.logwarn("Error parsing calls info", err);
     }
   },
 
   /**
    * Starts a call, saves the call data, and opens a chat window.
    *
    * @param {Object} callData The data associated with the call including an id.
-   * @param {Boolean} conversationType Whether or not the call is "incoming"
-   *                                   or "outgoing"
+   *                          The data should include the type - "incoming" or
+   *                          "outgoing".
    */
-  _startCall: function(callData, conversationType) {
+  _startCall: function(callData) {
     this.callsData.inUse = true;
     this.callsData.data = callData;
     MozLoopService.openChatWindow(
       null,
       // No title, let the page set that, to avoid flickering.
       "",
-      "about:loopconversation#" + conversationType + "/" + callData.windowId);
+      "about:loopconversation#" + callData.windowId);
   },
 
   /**
    * Starts a direct call to the contact addresses.
    *
    * @param {Object} contact The contact to call
    * @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
    * @return true if the call is opened, false if it is not opened (i.e. busy)
    */
   startDirectCall: function(contact, callType) {
     if (this.callsData.inUse)
       return false;
 
     var callData = {
       contact: contact,
       callType: callType,
+      type: "outgoing",
       windowId: Math.floor((Math.random() * 100000000))
     };
 
-    this._startCall(callData, "outgoing");
+    this._startCall(callData);
     return true;
   },
 
    /**
    * Open call progress websocket and terminate with a reason of busy
    * the server.
    *
    * @param {callData} Must contain the progressURL, callId and websocketToken
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -33,14 +33,15 @@
     <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/conversationStore.js"></script>
     <script type="text/javascript" src="loop/shared/js/localRoomStore.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/roomViews.js"></script>
     <script type="text/javascript" src="loop/js/conversation.js"></script>
   </body>
 </html>
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -174,22 +174,22 @@ loop.conversation = (function(mozL10n) {
           )
         )
         /* jshint ignore:end */
       );
     }
   });
 
   /**
-   * Incoming Call failed view. Displayed when a call fails.
+   * Something went wrong view. Displayed when there's a big problem.
    *
    * XXX Based on CallFailedView, but built specially until we flux-ify the
    * incoming call views (bug 1088672).
    */
-  var IncomingCallFailedView = React.createClass({displayName: 'IncomingCallFailedView',
+  var GenericFailureView = React.createClass({displayName: 'GenericFailureView',
     propTypes: {
       cancelCall: React.PropTypes.func.isRequired
     },
 
     render: function() {
       document.title = mozL10n.get("generic_failure_title");
 
       return (
@@ -278,17 +278,17 @@ loop.conversation = (function(mozL10n) {
               model: this.props.conversation, 
               video: {enabled: callType !== "audio"}}
             )
           );
         }
         case "end": {
           // XXX To be handled with the "failed" view state when bug 1047410 lands
           if (this.state.callFailed) {
-            return IncomingCallFailedView({
+            return GenericFailureView({
               cancelCall: this.closeWindow.bind(this)}
             )
           }
 
           document.title = mozL10n.get("conversation_has_ended");
 
           var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
             "feedback.baseUrl");
@@ -520,68 +520,84 @@ loop.conversation = (function(mozL10n) {
     },
   });
 
   /**
    * 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],
+
     propTypes: {
       // XXX Old types required for incoming call view.
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       sdk: React.PropTypes.object.isRequired,
 
-      // XXX New types for OutgoingConversationView
-      store: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
+      // XXX New types for flux style
+      conversationAppStore: React.PropTypes.instanceOf(
+        loop.store.ConversationAppStore).isRequired,
+      conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
+                              .isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-
-      // if not passed, this is not a room view
       localRoomStore: React.PropTypes.instanceOf(loop.store.LocalRoomStore)
     },
 
     getInitialState: function() {
-      return this.props.store.attributes;
+      return this.props.conversationAppStore.getStoreState();
     },
 
     componentWillMount: function() {
-      this.props.store.on("change:outgoing", function() {
-        this.setState(this.props.store.attributes);
+      this.listenTo(this.props.conversationAppStore, "change", function() {
+        this.setState(this.props.conversationAppStore.getStoreState());
       }, this);
     },
 
+    componentWillUnmount: function() {
+      this.stopListening(this.props.conversationAppStore);
+    },
+
+    closeWindow: function() {
+      window.close();
+    },
+
     render: function() {
-      if (this.props.localRoomStore) {
-        return (
-          EmptyRoomView({
+      switch(this.state.windowType) {
+        case "incoming": {
+          return (IncomingConversationView({
+            client: this.props.client, 
+            conversation: this.props.conversation, 
+            sdk: this.props.sdk}
+          ));
+        }
+        case "outgoing": {
+          return (OutgoingConversationView({
+            store: this.props.conversationStore, 
+            dispatcher: this.props.dispatcher}
+          ));
+        }
+        case "room": {
+          return (EmptyRoomView({
             mozLoop: navigator.mozLoop, 
             localRoomStore: this.props.localRoomStore}
-          )
-        );
-      }
-
-      // Don't display anything, until we know what type of call we are.
-      if (this.state.outgoing === undefined) {
-        return null;
+          ));
+        }
+        case "failed": {
+          return (GenericFailureView({
+            cancelCall: this.closeWindow.bind(this)}
+          ));
+        }
+        default: {
+          // If we don't have a windowType, we don't know what we are yet,
+          // so don't display anything.
+          return null;
+        }
       }
-
-      if (this.state.outgoing) {
-        return (OutgoingConversationView({
-          store: this.props.store, 
-          dispatcher: this.props.dispatcher}
-        ));
-      }
-
-      return (IncomingConversationView({
-        client: this.props.client, 
-        conversation: this.props.conversation, 
-        sdk: this.props.sdk}
-      ));
     }
   });
 
   /**
    * Conversation initialisation.
    */
   function init() {
     // Do the initial L10n setup, we do this before anything
@@ -602,89 +618,72 @@ loop.conversation = (function(mozL10n) {
 
     var dispatcher = new loop.Dispatcher();
     var client = new loop.Client();
     var sdkDriver = new loop.OTSdkDriver({
       dispatcher: dispatcher,
       sdk: OT
     });
 
+    // Create the stores.
+    var conversationAppStore = new loop.store.ConversationAppStore({
+      dispatcher: dispatcher,
+      mozLoop: navigator.mozLoop
+    });
     var conversationStore = new loop.store.ConversationStore({}, {
       client: client,
       dispatcher: dispatcher,
       sdkDriver: sdkDriver
     });
+    var localRoomStore = new loop.store.LocalRoomStore({
+      dispatcher: dispatcher,
+      mozLoop: navigator.mozLoop
+    });;
 
     // XXX Old class creation for the incoming conversation view, whilst
     // we transition across (bug 1072323).
     var conversation = new sharedModels.ConversationModel(
       {},                // Model attributes
       {sdk: window.OT}   // Model dependencies
     );
 
     // Obtain the windowId and pass it through
     var helper = new loop.shared.utils.Helper();
     var locationHash = helper.locationData().hash;
     var windowId;
-    var outgoing;
-    var localRoomStore;
 
-    // XXX removeMe, along with noisy comment at the beginning of
-    // conversation_test.js "when locationHash begins with #room".
-    if (navigator.mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
-      locationHash = "#room/32";
-    }
-
-    var hash = locationHash.match(/#incoming\/(.*)/);
+    var hash = locationHash.match(/#(.*)/);
     if (hash) {
       windowId = hash[1];
-      outgoing = false;
-    } else if (hash = locationHash.match(/#room\/(.*)/)) {
-      localRoomStore = new loop.store.LocalRoomStore({
-        dispatcher: dispatcher,
-        mozLoop: navigator.mozLoop
-      });
-    } else {
-      hash = locationHash.match(/#outgoing\/(.*)/);
-      if (hash) {
-        windowId = hash[1];
-        outgoing = true;
-      }
     }
 
     conversation.set({windowId: windowId});
 
     window.addEventListener("unload", function(event) {
       // Handle direct close of dialog box via [x] control.
       navigator.mozLoop.releaseCallData(windowId);
     });
 
     React.renderComponent(AppControllerView({
+      conversationAppStore: conversationAppStore, 
       localRoomStore: localRoomStore, 
-      store: conversationStore, 
+      conversationStore: conversationStore, 
       client: client, 
       conversation: conversation, 
       dispatcher: dispatcher, 
       sdk: window.OT}
     ), document.querySelector('#main'));
 
-   if (localRoomStore) {
-      dispatcher.dispatch(
-        new sharedActions.SetupEmptyRoom({localRoomId: hash[1]}));
-      return;
-    }
-
-    dispatcher.dispatch(new loop.shared.actions.GatherCallData({
-      windowId: windowId,
-      outgoing: outgoing
+    dispatcher.dispatch(new sharedActions.GetWindowData({
+      windowId: windowId
     }));
   }
 
   return {
     AppControllerView: AppControllerView,
     IncomingConversationView: IncomingConversationView,
     IncomingCallView: IncomingCallView,
-    IncomingCallFailedView: IncomingCallFailedView,
+    GenericFailureView: GenericFailureView,
     init: init
   };
 })(document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.conversation.init);
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -174,22 +174,22 @@ loop.conversation = (function(mozL10n) {
           </div>
         </div>
         /* jshint ignore:end */
       );
     }
   });
 
   /**
-   * Incoming Call failed view. Displayed when a call fails.
+   * Something went wrong view. Displayed when there's a big problem.
    *
    * XXX Based on CallFailedView, but built specially until we flux-ify the
    * incoming call views (bug 1088672).
    */
-  var IncomingCallFailedView = React.createClass({
+  var GenericFailureView = React.createClass({
     propTypes: {
       cancelCall: React.PropTypes.func.isRequired
     },
 
     render: function() {
       document.title = mozL10n.get("generic_failure_title");
 
       return (
@@ -278,17 +278,17 @@ loop.conversation = (function(mozL10n) {
               model={this.props.conversation}
               video={{enabled: callType !== "audio"}}
             />
           );
         }
         case "end": {
           // XXX To be handled with the "failed" view state when bug 1047410 lands
           if (this.state.callFailed) {
-            return <IncomingCallFailedView
+            return <GenericFailureView
               cancelCall={this.closeWindow.bind(this)}
             />
           }
 
           document.title = mozL10n.get("conversation_has_ended");
 
           var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
             "feedback.baseUrl");
@@ -520,68 +520,84 @@ loop.conversation = (function(mozL10n) {
     },
   });
 
   /**
    * 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],
+
     propTypes: {
       // XXX Old types required for incoming call view.
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       sdk: React.PropTypes.object.isRequired,
 
-      // XXX New types for OutgoingConversationView
-      store: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
+      // XXX New types for flux style
+      conversationAppStore: React.PropTypes.instanceOf(
+        loop.store.ConversationAppStore).isRequired,
+      conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
+                              .isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-
-      // if not passed, this is not a room view
       localRoomStore: React.PropTypes.instanceOf(loop.store.LocalRoomStore)
     },
 
     getInitialState: function() {
-      return this.props.store.attributes;
+      return this.props.conversationAppStore.getStoreState();
     },
 
     componentWillMount: function() {
-      this.props.store.on("change:outgoing", function() {
-        this.setState(this.props.store.attributes);
+      this.listenTo(this.props.conversationAppStore, "change", function() {
+        this.setState(this.props.conversationAppStore.getStoreState());
       }, this);
     },
 
+    componentWillUnmount: function() {
+      this.stopListening(this.props.conversationAppStore);
+    },
+
+    closeWindow: function() {
+      window.close();
+    },
+
     render: function() {
-      if (this.props.localRoomStore) {
-        return (
-          <EmptyRoomView
+      switch(this.state.windowType) {
+        case "incoming": {
+          return (<IncomingConversationView
+            client={this.props.client}
+            conversation={this.props.conversation}
+            sdk={this.props.sdk}
+          />);
+        }
+        case "outgoing": {
+          return (<OutgoingConversationView
+            store={this.props.conversationStore}
+            dispatcher={this.props.dispatcher}
+          />);
+        }
+        case "room": {
+          return (<EmptyRoomView
             mozLoop={navigator.mozLoop}
             localRoomStore={this.props.localRoomStore}
-          />
-        );
-      }
-
-      // Don't display anything, until we know what type of call we are.
-      if (this.state.outgoing === undefined) {
-        return null;
+          />);
+        }
+        case "failed": {
+          return (<GenericFailureView
+            cancelCall={this.closeWindow.bind(this)}
+          />);
+        }
+        default: {
+          // If we don't have a windowType, we don't know what we are yet,
+          // so don't display anything.
+          return null;
+        }
       }
-
-      if (this.state.outgoing) {
-        return (<OutgoingConversationView
-          store={this.props.store}
-          dispatcher={this.props.dispatcher}
-        />);
-      }
-
-      return (<IncomingConversationView
-        client={this.props.client}
-        conversation={this.props.conversation}
-        sdk={this.props.sdk}
-      />);
     }
   });
 
   /**
    * Conversation initialisation.
    */
   function init() {
     // Do the initial L10n setup, we do this before anything
@@ -602,89 +618,72 @@ loop.conversation = (function(mozL10n) {
 
     var dispatcher = new loop.Dispatcher();
     var client = new loop.Client();
     var sdkDriver = new loop.OTSdkDriver({
       dispatcher: dispatcher,
       sdk: OT
     });
 
+    // Create the stores.
+    var conversationAppStore = new loop.store.ConversationAppStore({
+      dispatcher: dispatcher,
+      mozLoop: navigator.mozLoop
+    });
     var conversationStore = new loop.store.ConversationStore({}, {
       client: client,
       dispatcher: dispatcher,
       sdkDriver: sdkDriver
     });
+    var localRoomStore = new loop.store.LocalRoomStore({
+      dispatcher: dispatcher,
+      mozLoop: navigator.mozLoop
+    });;
 
     // XXX Old class creation for the incoming conversation view, whilst
     // we transition across (bug 1072323).
     var conversation = new sharedModels.ConversationModel(
       {},                // Model attributes
       {sdk: window.OT}   // Model dependencies
     );
 
     // Obtain the windowId and pass it through
     var helper = new loop.shared.utils.Helper();
     var locationHash = helper.locationData().hash;
     var windowId;
-    var outgoing;
-    var localRoomStore;
 
-    // XXX removeMe, along with noisy comment at the beginning of
-    // conversation_test.js "when locationHash begins with #room".
-    if (navigator.mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
-      locationHash = "#room/32";
-    }
-
-    var hash = locationHash.match(/#incoming\/(.*)/);
+    var hash = locationHash.match(/#(.*)/);
     if (hash) {
       windowId = hash[1];
-      outgoing = false;
-    } else if (hash = locationHash.match(/#room\/(.*)/)) {
-      localRoomStore = new loop.store.LocalRoomStore({
-        dispatcher: dispatcher,
-        mozLoop: navigator.mozLoop
-      });
-    } else {
-      hash = locationHash.match(/#outgoing\/(.*)/);
-      if (hash) {
-        windowId = hash[1];
-        outgoing = true;
-      }
     }
 
     conversation.set({windowId: windowId});
 
     window.addEventListener("unload", function(event) {
       // Handle direct close of dialog box via [x] control.
       navigator.mozLoop.releaseCallData(windowId);
     });
 
     React.renderComponent(<AppControllerView
+      conversationAppStore={conversationAppStore}
       localRoomStore={localRoomStore}
-      store={conversationStore}
+      conversationStore={conversationStore}
       client={client}
       conversation={conversation}
       dispatcher={dispatcher}
       sdk={window.OT}
     />, document.querySelector('#main'));
 
-   if (localRoomStore) {
-      dispatcher.dispatch(
-        new sharedActions.SetupEmptyRoom({localRoomId: hash[1]}));
-      return;
-    }
-
-    dispatcher.dispatch(new loop.shared.actions.GatherCallData({
-      windowId: windowId,
-      outgoing: outgoing
+    dispatcher.dispatch(new sharedActions.GetWindowData({
+      windowId: windowId
     }));
   }
 
   return {
     AppControllerView: AppControllerView,
     IncomingConversationView: IncomingConversationView,
     IncomingCallView: IncomingCallView,
-    IncomingCallFailedView: IncomingCallFailedView,
+    GenericFailureView: GenericFailureView,
     init: init
   };
 })(document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.conversation.init);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/js/conversationAppStore.js
@@ -0,0 +1,88 @@
+/* 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/. */
+
+/* global loop:true */
+
+var loop = loop || {};
+loop.store = loop.store || {};
+
+/**
+ * Manages the conversation window app controller view. Used to get
+ * the window data and store the window type.
+ */
+loop.store.ConversationAppStore = (function() {
+  /**
+   * Constructor
+   *
+   * @param {Object} options Options for the store. Should contain the dispatcher.
+   */
+  var ConversationAppStore = function(options) {
+    if (!options.dispatcher) {
+      throw new Error("Missing option dispatcher");
+    }
+    if (!options.mozLoop) {
+      throw new Error("Missing option mozLoop");
+    }
+
+    this._dispatcher = options.dispatcher;
+    this._mozLoop = options.mozLoop;
+    this._storeState = {};
+
+    this._dispatcher.register(this, [
+      "getWindowData"
+    ]);
+  };
+
+  ConversationAppStore.prototype = _.extend({
+    /**
+     * Retrieves current store state.
+     *
+     * @return {Object}
+     */
+    getStoreState: function() {
+      return this._storeState;
+    },
+
+    /**
+     * Updates store states and trigger a "change" event.
+     *
+     * @param {Object} state The new store state.
+     */
+    setStoreState: function(state) {
+      this._storeState = state;
+      this.trigger("change");
+    },
+
+    /**
+     * Handles the get window data action - obtains the window data,
+     * updates the store and notifies interested components.
+     *
+     * @param {sharedActions.GetWindowData} actionData The action data
+     */
+    getWindowData: function(actionData) {
+      var windowData;
+      // XXX Remove me in bug 1074678
+      if (this._mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
+        windowData = {type: "room", localRoomId: "42"};
+      } else {
+        windowData = this._mozLoop.getCallData(actionData.windowId);
+      }
+
+      if (!windowData) {
+        console.error("Failed to get the window data");
+        this.setStoreState({windowType: "failed"});
+        return;
+      }
+
+      this.setStoreState({windowType: windowData.type});
+
+      this._dispatcher.dispatch(new loop.shared.actions.SetupWindowData({
+        windowData: windowData
+      }));
+    }
+  }, Backbone.Events);
+
+  return ConversationAppStore;
+
+})();
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -494,16 +494,20 @@ loop.conversationViews = (function(mozL1
             video: {enabled: !this.state.videoMuted}, 
             audio: {enabled: !this.state.audioMuted}}
             )
           );
         }
         case CALL_STATES.FINISHED: {
           return this._renderFeedbackView();
         }
+        case CALL_STATES.INIT: {
+          // We know what we are, but we haven't got the data yet.
+          return null;
+        }
         default: {
           return (PendingConversationView({
             dispatcher: this.props.dispatcher, 
             callState: this.state.callState, 
             contact: this.state.contact, 
             enableCancelButton: this._isCancellable()}
           ));
         }
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -494,16 +494,20 @@ loop.conversationViews = (function(mozL1
             video={{enabled: !this.state.videoMuted}}
             audio={{enabled: !this.state.audioMuted}}
             />
           );
         }
         case CALL_STATES.FINISHED: {
           return this._renderFeedbackView();
         }
+        case CALL_STATES.INIT: {
+          // We know what we are, but we haven't got the data yet.
+          return null;
+        }
         default: {
           return (<PendingConversationView
             dispatcher={this.props.dispatcher}
             callState={this.state.callState}
             contact={this.state.contact}
             enableCancelButton={this._isCancellable()}
           />);
         }
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -26,32 +26,38 @@ loop.shared.actions = (function() {
   }
 
   Action.define = function(name, schema) {
     return Action.bind(null, name, schema);
   };
 
   return {
     /**
+     * Get the window data for the provided window id
+     */
+    GetWindowData: Action.define("getWindowData", {
+      windowId: String
+    }),
+
+    /**
+     * Used to pass round the window data so that stores can
+     * record the appropriate data.
+     */
+    SetupWindowData: Action.define("setupWindowData", {
+      windowData: Object
+    }),
+
+    /**
      * Fetch a new call url from the server, intended to be sent over email when
      * a contact can't be reached.
      */
     FetchEmailLink: Action.define("fetchEmailLink", {
     }),
 
     /**
-     * Used to trigger gathering of initial call data.
-     */
-    GatherCallData: Action.define("gatherCallData", {
-      // Specify the callId for an incoming call.
-      windowId: [String, null],
-      outgoing: Boolean
-    }),
-
-    /**
      * Used to cancel call setup.
      */
     CancelCall: Action.define("cancelCall", {
     }),
 
     /**
      * Used to retry a failed call.
      */
@@ -162,21 +168,11 @@ loop.shared.actions = (function() {
     }),
 
     /**
      * Updates room list.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     UpdateRoomList: Action.define("updateRoomList", {
       roomList: Array
-    }),
-
-    /**
-     * Primes localRoomStore with roomLocalId, which triggers the EmptyRoomView
-     * to do any necessary setup.
-     *
-     * XXX should move to localRoomActions module
-     */
-    SetupEmptyRoom: Action.define("setupEmptyRoom", {
-      localRoomId: String
     })
   };
 })();
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -116,17 +116,17 @@ loop.store.ConversationStore = (function
 
       this.client = options.client;
       this.dispatcher = options.dispatcher;
       this.sdkDriver = options.sdkDriver;
 
       this.dispatcher.register(this, [
         "connectionFailure",
         "connectionProgress",
-        "gatherCallData",
+        "setupWindowData",
         "connectCall",
         "hangupCall",
         "peerHungupCall",
         "cancelCall",
         "retryCall",
         "mediaConnected",
         "setMute",
         "fetchEmailLink"
@@ -183,47 +183,34 @@ loop.store.ConversationStore = (function
         }
         default: {
           console.error("Unexpected websocket state passed to connectionProgress:",
             actionData.wsState);
         }
       }
     },
 
-    /**
-     * Handles the gather call data action, setting the state
-     * and starting to get the appropriate data for the type of call.
-     *
-     * @param {sharedActions.GatherCallData} actionData The action data.
-     */
-    gatherCallData: function(actionData) {
-      if (!actionData.outgoing) {
-        // XXX Other types aren't supported yet, but set the state for the
-        // view selection.
-        this.set({outgoing: false});
-        return;
-      }
-
-      var callData = navigator.mozLoop.getCallData(actionData.windowId);
-      if (!callData) {
-        console.error("Failed to get the call data");
-        this.set({callState: CALL_STATES.TERMINATED});
+    setupWindowData: function(actionData) {
+      var windowData = actionData.windowData;
+      var windowType = windowData.type;
+      if (windowType !== "outgoing" &&
+          windowType !== "incoming") {
+        // Not for this store, don't do anything.
         return;
       }
 
       this.set({
-        contact: callData.contact,
-        outgoing: actionData.outgoing,
-        windowId: actionData.windowId,
-        callType: callData.callType,
-        callState: CALL_STATES.GATHER
+        contact: windowData.contact,
+        outgoing: windowType === "outgoing",
+        windowId: windowData.windowId,
+        callType: windowData.callType,
+        callState: CALL_STATES.GATHER,
+        videoMuted: windowData.callType === CALL_TYPES.AUDIO_ONLY
       });
 
-      this.set({videoMuted: this.get("callType") === CALL_TYPES.AUDIO_ONLY});
-
       if (this.get("outgoing")) {
         this._setupOutgoingCall();
       } // XXX Else, other types aren't supported yet.
     },
 
     /**
      * Handles the connect call action, this saves the appropriate
      * data and starts the connection for the websocket to notify the
--- a/browser/components/loop/content/shared/js/localRoomStore.js
+++ b/browser/components/loop/content/shared/js/localRoomStore.js
@@ -31,17 +31,19 @@ loop.store.LocalRoomStore = (function() 
     }
     this.dispatcher = options.dispatcher;
 
     if (!options.mozLoop) {
       throw new Error("Missing option mozLoop");
     }
     this.mozLoop = options.mozLoop;
 
-    this.dispatcher.register(this, ["setupEmptyRoom"]);
+    this.dispatcher.register(this, [
+      "setupWindowData"
+    ]);
   }
 
   LocalRoomStore.prototype = _.extend({
 
     /**
      * Stored data reflecting the local state of a given room, used to drive
      * the room's views.
      *
@@ -68,46 +70,53 @@ loop.store.LocalRoomStore = (function() 
       this.trigger("change");
     },
 
     /**
      * Proxy to mozLoop.rooms.getRoomData for setupEmptyRoom action.
      *
      * XXXremoveMe Can probably be removed when bug 1074664 lands.
      *
-     * @param {sharedActions.setupEmptyRoom} actionData
+     * @param {Integer} roomId The id of the room.
      * @param {Function} cb Callback(error, roomData)
      */
-    _fetchRoomData: function(actionData, cb) {
-      if (this.mozLoop.rooms && this.mozLoop.rooms.getRoomData) {
-        this.mozLoop.rooms.getRoomData(actionData.localRoomId, cb);
+    _fetchRoomData: function(roomId, cb) {
+      // XXX Remove me in bug 1074678
+      if (!this.mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
+        this.mozLoop.rooms.getRoomData(roomId, cb);
       } else {
         cb(null, {roomName: "Donkeys"});
       }
     },
 
     /**
      * Execute setupEmptyRoom event action from the dispatcher.  This primes
      * the store with the localRoomId, and calls MozLoop.getRoomData on that
      * ID.  This will return either a reflection of state on the server, or,
      * if the createRoom call hasn't yet returned, it will have at least the
      * roomName as specified to the createRoom method.
      *
      * When the room name gets set, that will trigger the view to display
      * that name.
      *
-     * @param {sharedActions.setupEmptyRoom} actionData
+     * @param {sharedActions.SetupWindowData} actionData
      */
-    setupEmptyRoom: function(actionData) {
-      this._fetchRoomData(actionData, function(error, roomData) {
-        this.setStoreState({
-          error: error,
-          localRoomId: actionData.localRoomId,
-          serverData: roomData
-        });
-      }.bind(this));
+    setupWindowData: function(actionData) {
+      if (actionData.windowData.type !== "room") {
+        // Nothing for us to do here, leave it to other stores.
+        return;
+      }
+
+      this._fetchRoomData(actionData.windowData.localRoomId,
+        function(error, roomData) {
+          this.setStoreState({
+            error: error,
+            localRoomId: actionData.windowData.localRoomId,
+            serverData: roomData
+          });
+        }.bind(this));
     }
 
   }, Backbone.Events);
 
   return LocalRoomStore;
 
 })();
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -8,16 +8,17 @@ browser.jar:
   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/roomViews.js              (content/js/roomViews.js)
 
   # Shared styles
   content/browser/loop/shared/css/reset.css         (content/shared/css/reset.css)
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/desktop-local/conversationAppStore_test.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var expect = chai.expect;
+
+describe("loop.store.ConversationAppStore", function () {
+
+  var sharedActions = loop.shared.actions;
+  var sandbox, dispatcher;
+
+  beforeEach(function() {
+    sandbox = sinon.sandbox.create();
+    dispatcher = new loop.Dispatcher();
+  });
+
+  afterEach(function() {
+    sandbox.restore();
+  });
+
+  describe("#constructor", function() {
+    it("should throw an error if the dispatcher is missing", function() {
+      expect(function() {
+        new loop.store.ConversationAppStore({mozLoop: {}});
+      }).to.Throw(/dispatcher/);
+    });
+
+    it("should throw an error if mozLoop is missing", function() {
+      expect(function() {
+        new loop.store.ConversationAppStore({dispatcher: dispatcher});
+      }).to.Throw(/mozLoop/);
+    });
+  });
+
+  describe("#getWindowData", function() {
+    var fakeCallData, fakeGetWindowData, fakeMozLoop, store;
+
+    beforeEach(function() {
+      fakeCallData = {
+        type: "incoming",
+        callId: "123456"
+      };
+
+      fakeGetWindowData = {
+        windowId: "42"
+      };
+
+      fakeMozLoop = {
+        // XXX Remove me in bug 1074678
+        getLoopBoolPref: function() { return false; },
+        getCallData: function(windowId) {
+          if (windowId === "42") {
+            return fakeCallData;
+          }
+          return null;
+        }
+      };
+
+      store = new loop.store.ConversationAppStore({
+        dispatcher: dispatcher,
+        mozLoop: fakeMozLoop
+      });
+    });
+
+    it("should fetch the window type from the mozLoop API", function() {
+      dispatcher.dispatch(new sharedActions.GetWindowData(fakeGetWindowData));
+
+      expect(store.getStoreState()).eql({windowType: "incoming"});
+    });
+
+    it("should dispatch a SetupWindowData action with the data from the mozLoop API",
+      function() {
+        sandbox.stub(dispatcher, "dispatch");
+
+        store.getWindowData(new sharedActions.GetWindowData(fakeGetWindowData));
+
+        sinon.assert.calledOnce(dispatcher.dispatch);
+        sinon.assert.calledWithExactly(dispatcher.dispatch,
+          new sharedActions.SetupWindowData({
+            windowData: fakeCallData
+          }));
+      });
+  });
+
+});
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -433,20 +433,20 @@ describe("loop.conversationViews", funct
         store.set({callState: CALL_STATES.TERMINATED});
 
         view = mountTestComponent();
 
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.CallFailedView);
     });
 
-    it("should render the PendingConversationView when the call state is 'init'",
+    it("should render the PendingConversationView when the call state is 'gather'",
       function() {
         store.set({
-          callState: CALL_STATES.INIT,
+          callState: CALL_STATES.GATHER,
           contact: contact
         });
 
         view = mountTestComponent();
 
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.PendingConversationView);
     });
@@ -469,17 +469,17 @@ describe("loop.conversationViews", funct
 
         TestUtils.findRenderedComponentWithType(view,
           loop.shared.views.FeedbackView);
     });
 
     it("should update the rendered views when the state is changed.",
       function() {
         store.set({
-          callState: CALL_STATES.INIT,
+          callState: CALL_STATES.GATHER,
           contact: contact
         });
 
         view = mountTestComponent();
 
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.PendingConversationView);
 
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -75,17 +75,17 @@ describe("loop.conversation", function()
 
       sandbox.stub(loop.shared.models.ConversationModel.prototype,
         "initialize");
 
       sandbox.stub(loop.Dispatcher.prototype, "dispatch");
 
       sandbox.stub(loop.shared.utils.Helper.prototype,
         "locationData").returns({
-          hash: "#incoming/42",
+          hash: "#42",
           pathname: "/"
         });
 
       window.OT = {
         overrideGuidStorage: sinon.stub()
       };
     });
 
@@ -107,160 +107,114 @@ describe("loop.conversation", function()
       sinon.assert.calledOnce(React.renderComponent);
       sinon.assert.calledWith(React.renderComponent,
         sinon.match(function(value) {
           return TestUtils.isDescriptorOfType(value,
             loop.conversation.AppControllerView);
       }));
     });
 
-    describe("when locationHash begins with #room", function () {
-      // XXX must stay in sync with "test.alwaysUseRooms" pref check
-      // in conversation.jsx:init until we remove that code, which should
-      // happen in the second patch in bug 1074686, at which time this comment
-      // can go away as well.
-      var fakeRoomID = "32";
-
-      beforeEach(function() {
-        loop.shared.utils.Helper.prototype.locationData
-          .returns({
-            hash: "#room/" + fakeRoomID,
-            pathname: ""
-          });
-
-        sandbox.stub(loop.store, "LocalRoomStore");
-      });
-
-      it("should create a localRoomStore", function() {
-        loop.conversation.init();
-
-        sinon.assert.calledOnce(loop.store.LocalRoomStore);
-        sinon.assert.calledWithNew(loop.store.LocalRoomStore);
-        sinon.assert.calledWithExactly(loop.store.LocalRoomStore,
-          sinon.match({
-            dispatcher: sinon.match.instanceOf(loop.Dispatcher),
-            mozLoop: sinon.match.same(navigator.mozLoop)
-          }));
-      });
-
-      it("should dispatch SetupEmptyRoom with localRoomId from locationHash",
-        function() {
-
-          loop.conversation.init();
-
-          sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
-          sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
-            new loop.shared.actions.SetupEmptyRoom({localRoomId: fakeRoomID}));
-        });
-    });
-
-    it("should trigger a gatherCallData action", function() {
+    it("should trigger a getWindowData action", function() {
       loop.conversation.init();
 
       sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
       sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
-        new loop.shared.actions.GatherCallData({
-          windowId: "42",
-          outgoing: false
+        new loop.shared.actions.GetWindowData({
+          windowId: "42"
         }));
     });
-
-    it("should trigger an outgoing gatherCallData action for outgoing calls",
-      function() {
-        loop.shared.utils.Helper.prototype.locationData.returns({
-          hash: "#outgoing/24",
-          pathname: "/"
-        });
-
-        loop.conversation.init();
-
-        sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
-        sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
-          new loop.shared.actions.GatherCallData({
-            windowId: "24",
-            outgoing: true
-          }));
-      });
   });
 
-  describe("ConversationControllerView", function() {
-    var store, conversation, client, ccView, oldTitle, dispatcher;
+  describe("AppControllerView", function() {
+    var conversationStore, conversation, client, ccView, oldTitle, dispatcher;
+    var conversationAppStore, localRoomStore;
 
-    function mountTestComponent(localRoomStore) {
+    function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         loop.conversation.AppControllerView({
           client: client,
           conversation: conversation,
           localRoomStore: localRoomStore,
           sdk: {},
-          store: store
+          conversationStore: conversationStore,
+          conversationAppStore: conversationAppStore
         }));
     }
 
     beforeEach(function() {
       oldTitle = document.title;
       client = new loop.Client();
       conversation = new loop.shared.models.ConversationModel({}, {
         sdk: {}
       });
       dispatcher = new loop.Dispatcher();
-      store = new loop.store.ConversationStore({
+      conversationStore = new loop.store.ConversationStore({
         contact: {
           name: [ "Mr Smith" ],
           email: [{
             type: "home",
             value: "fakeEmail",
             pref: true
           }]
         }
       }, {
         client: client,
         dispatcher: dispatcher,
         sdkDriver: {}
       });
+      localRoomStore = new loop.store.LocalRoomStore({
+        mozLoop: navigator.mozLoop,
+        dispatcher: dispatcher
+      });
+      conversationAppStore = new loop.store.ConversationAppStore({
+        dispatcher: dispatcher,
+        mozLoop: navigator.mozLoop
+      });
     });
 
     afterEach(function() {
       ccView = undefined;
       document.title = oldTitle;
     });
 
     it("should display the OutgoingConversationView for outgoing calls", function() {
-      store.set({outgoing: true});
+      conversationAppStore.setStoreState({windowType: "outgoing"});
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
         loop.conversationViews.OutgoingConversationView);
     });
 
     it("should display the IncomingConversationView for incoming calls", function() {
-      store.set({outgoing: false});
+      conversationAppStore.setStoreState({windowType: "incoming"});
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
         loop.conversation.IncomingConversationView);
     });
 
     it("should display the EmptyRoomView for rooms", function() {
-      navigator.mozLoop.rooms = {
-        addCallback: function() {},
-        removeCallback: function() {}
-      };
-      var localRoomStore = new loop.store.LocalRoomStore({
-        mozLoop: navigator.mozLoop,
-        dispatcher: dispatcher
-      });
+      conversationAppStore.setStoreState({windowType: "room"});
 
-      ccView = mountTestComponent(localRoomStore);
+      ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
         loop.roomViews.EmptyRoomView);
     });
+
+    it("should display the GenericFailureView for failures", function() {
+      conversationAppStore.setStoreState({windowType: "failed"});
+
+      ccView = mountTestComponent();
+
+      TestUtils.findRenderedComponentWithType(ccView,
+        loop.conversation.GenericFailureView);
+    });
   });
 
   describe("IncomingConversationView", function() {
     var conversation, client, icView, oldTitle;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         loop.conversation.IncomingConversationView({
@@ -707,17 +661,17 @@ describe("loop.conversation", function()
       });
 
       describe("session:network-disconnected", function() {
         it("should navigate to call failed when network disconnects",
           function() {
             conversation.trigger("session:network-disconnected");
 
               TestUtils.findRenderedComponentWithType(icView,
-                loop.conversation.IncomingCallFailedView);
+                loop.conversation.GenericFailureView);
           });
 
         it("should update the conversation window toolbar title",
           function() {
             conversation.trigger("session:network-disconnected");
 
             expect(document.title).eql("generic_failure_title");
           });
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -41,23 +41,25 @@
   <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/roomListStore.js"></script>
   <script src="../../content/js/client.js"></script>
   <script src="../../content/shared/js/localRoomStore.js"></script>
+  <script src="../../content/js/conversationAppStore.js"></script>
   <script src="../../content/js/roomViews.js"></script>
   <script src="../../content/js/conversationViews.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="panel_test.js"></script>
   <script src="roomViews_test.js"></script>
   <script src="conversationViews_test.js"></script>
   <script>
     // Stop the default init functions running to avoid conflicts in tests
     document.removeEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -216,128 +216,107 @@ describe("loop.store.ConversationStore",
           apiKey: "fakeKey",
           sessionId: "321456",
           sessionToken: "341256"
         });
       });
     });
   });
 
-  describe("#gatherCallData", function() {
+  describe("#setupWindowData", function() {
+    var fakeSetupWindowData;
+
     beforeEach(function() {
       store.set({callState: CALL_STATES.INIT});
-
-      navigator.mozLoop = {
-        getCallData: function() {
-          return {
-            contact: contact,
-            callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO
-          };
+      fakeSetupWindowData = {
+        windowData: {
+          type: "outgoing",
+          contact: contact,
+          windowId: "123456",
+          callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO
         }
       };
     });
 
-    afterEach(function() {
-      delete navigator.mozLoop;
-    });
-
     it("should set the state to 'gather'", function() {
       dispatcher.dispatch(
-        new sharedActions.GatherCallData({
-          windowId: "76543218",
-          outgoing: true
-        }));
+        new sharedActions.SetupWindowData(fakeSetupWindowData));
 
       expect(store.get("callState")).eql(CALL_STATES.GATHER);
     });
 
     it("should save the basic call information", function() {
       dispatcher.dispatch(
-        new sharedActions.GatherCallData({
-          windowId: "123456",
-          outgoing: true
-        }));
+        new sharedActions.SetupWindowData(fakeSetupWindowData));
 
       expect(store.get("windowId")).eql("123456");
       expect(store.get("outgoing")).eql(true);
     });
 
     it("should save the basic information from the mozLoop api", function() {
       dispatcher.dispatch(
-        new sharedActions.GatherCallData({
-          windowId: "123456",
-          outgoing: true
-        }));
+        new sharedActions.SetupWindowData(fakeSetupWindowData));
 
       expect(store.get("contact")).eql(contact);
       expect(store.get("callType")).eql(sharedUtils.CALL_TYPES.AUDIO_VIDEO);
     });
 
     describe("outgoing calls", function() {
-      var outgoingCallData;
-
-      beforeEach(function() {
-        outgoingCallData = {
-          windowId: "123456",
-          outgoing: true
-        };
-      });
-
       it("should request the outgoing call data", function() {
         dispatcher.dispatch(
-          new sharedActions.GatherCallData(outgoingCallData));
+          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() {
-        contact = {
+        fakeSetupWindowData.windowData.contact = {
           name: [ "Mr Smith" ],
           email: [{
             type: "home",
             value: "fakeEmail",
             pref: true
           },
           {
             type: "work",
             value: "emailFake",
             pref: false
           }]
         };
 
         dispatcher.dispatch(
-          new sharedActions.GatherCallData(outgoingCallData));
+          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() {
-        contact = {
+        fakeSetupWindowData.windowData.contact = {
           name: [ "Mr Smith" ],
           tel: [{
             type: "home",
             value: "+44-5667+345 496(2335)45+ 456+",
             pref: true
           }]
         };
 
         dispatcher.dispatch(
-          new sharedActions.GatherCallData(outgoingCallData));
+          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() {
-        contact = {
+        fakeSetupWindowData.windowData.contact = {
           name: [ "Mr Smith" ],
           email: [{
             type: "home",
             value: "fakeEmail",
             pref: true
           }, {
             type: "work",
             value: "emailFake",
@@ -350,17 +329,17 @@ describe("loop.store.ConversationStore",
           }, {
             type: "home",
             value: "09876543210",
             pref: false
           }]
         };
 
         dispatcher.dispatch(
-          new sharedActions.GatherCallData(outgoingCallData));
+          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() {
@@ -370,32 +349,32 @@ describe("loop.store.ConversationStore",
 
         it("should dispatch a connect call action on success", function() {
           var callData = {
             apiKey: "fakeKey"
           };
 
           client.setupOutgoingCall.callsArgWith(2, null, callData);
 
-          store.gatherCallData(
-            new sharedActions.GatherCallData(outgoingCallData));
+          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.gatherCallData(
-            new sharedActions.GatherCallData(outgoingCallData));
+          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", "setup"));
         });
--- a/browser/components/loop/test/shared/dispatcher_test.js
+++ b/browser/components/loop/test/shared/dispatcher_test.js
@@ -17,108 +17,107 @@ describe("loop.Dispatcher", function () 
   afterEach(function() {
     sandbox.restore();
   });
 
   describe("#register", function() {
     it("should register a store against an action name", function() {
       var object = { fake: true };
 
-      dispatcher.register(object, ["gatherCallData"]);
+      dispatcher.register(object, ["getWindowData"]);
 
-      expect(dispatcher._eventData["gatherCallData"][0]).eql(object);
+      expect(dispatcher._eventData["getWindowData"][0]).eql(object);
     });
 
     it("should register multiple store against an action name", function() {
       var object1 = { fake: true };
       var object2 = { fake2: true };
 
-      dispatcher.register(object1, ["gatherCallData"]);
-      dispatcher.register(object2, ["gatherCallData"]);
+      dispatcher.register(object1, ["getWindowData"]);
+      dispatcher.register(object2, ["getWindowData"]);
 
-      expect(dispatcher._eventData["gatherCallData"][0]).eql(object1);
-      expect(dispatcher._eventData["gatherCallData"][1]).eql(object2);
+      expect(dispatcher._eventData["getWindowData"][0]).eql(object1);
+      expect(dispatcher._eventData["getWindowData"][1]).eql(object2);
     });
   });
 
   describe("#dispatch", function() {
-    var gatherStore1, gatherStore2, cancelStore1, connectStore1;
-    var gatherAction, cancelAction, connectAction, resolveCancelStore1;
+    var getDataStore1, getDataStore2, cancelStore1, connectStore1;
+    var getDataAction, cancelAction, connectAction, resolveCancelStore1;
 
     beforeEach(function() {
-      gatherAction = new sharedActions.GatherCallData({
-        windowId: "42",
-        outgoing: false
+      getDataAction = new sharedActions.GetWindowData({
+        windowId: "42"
       });
 
       cancelAction = new sharedActions.CancelCall();
       connectAction = new sharedActions.ConnectCall({
         sessionData: {}
       });
 
-      gatherStore1 = {
-        gatherCallData: sinon.stub()
+      getDataStore1 = {
+        getWindowData: sinon.stub()
       };
-      gatherStore2 = {
-        gatherCallData: sinon.stub()
+      getDataStore2 = {
+        getWindowData: sinon.stub()
       };
       cancelStore1 = {
         cancelCall: sinon.stub()
       };
       connectStore1 = {
         connectCall: function() {}
       };
 
-      dispatcher.register(gatherStore1, ["gatherCallData"]);
-      dispatcher.register(gatherStore2, ["gatherCallData"]);
+      dispatcher.register(getDataStore1, ["getWindowData"]);
+      dispatcher.register(getDataStore2, ["getWindowData"]);
       dispatcher.register(cancelStore1, ["cancelCall"]);
       dispatcher.register(connectStore1, ["connectCall"]);
     });
 
     it("should dispatch an action to the required object", function() {
       dispatcher.dispatch(cancelAction);
 
-      sinon.assert.notCalled(gatherStore1.gatherCallData);
+      sinon.assert.notCalled(getDataStore1.getWindowData);
 
       sinon.assert.calledOnce(cancelStore1.cancelCall);
       sinon.assert.calledWithExactly(cancelStore1.cancelCall, cancelAction);
 
-      sinon.assert.notCalled(gatherStore2.gatherCallData);
+      sinon.assert.notCalled(getDataStore2.getWindowData);
     });
 
     it("should dispatch actions to multiple objects", function() {
-      dispatcher.dispatch(gatherAction);
+      dispatcher.dispatch(getDataAction);
 
-      sinon.assert.calledOnce(gatherStore1.gatherCallData);
-      sinon.assert.calledWithExactly(gatherStore1.gatherCallData, gatherAction);
+      sinon.assert.calledOnce(getDataStore1.getWindowData);
+      sinon.assert.calledWithExactly(getDataStore1.getWindowData, getDataAction);
 
       sinon.assert.notCalled(cancelStore1.cancelCall);
 
-      sinon.assert.calledOnce(gatherStore2.gatherCallData);
-      sinon.assert.calledWithExactly(gatherStore2.gatherCallData, gatherAction);
+      sinon.assert.calledOnce(getDataStore2.getWindowData);
+      sinon.assert.calledWithExactly(getDataStore2.getWindowData, getDataAction);
     });
 
     it("should dispatch multiple actions", function() {
       dispatcher.dispatch(cancelAction);
-      dispatcher.dispatch(gatherAction);
+      dispatcher.dispatch(getDataAction);
 
       sinon.assert.calledOnce(cancelStore1.cancelCall);
-      sinon.assert.calledOnce(gatherStore1.gatherCallData);
-      sinon.assert.calledOnce(gatherStore2.gatherCallData);
+      sinon.assert.calledOnce(getDataStore1.getWindowData);
+      sinon.assert.calledOnce(getDataStore2.getWindowData);
     });
 
     describe("Queued actions", function() {
       beforeEach(function() {
         // Restore the stub, so that we can easily add a function to be
         // returned. Unfortunately, sinon doesn't make this easy.
         sandbox.stub(connectStore1, "connectCall", function() {
-          dispatcher.dispatch(gatherAction);
+          dispatcher.dispatch(getDataAction);
 
-          sinon.assert.notCalled(gatherStore1.gatherCallData);
-          sinon.assert.notCalled(gatherStore2.gatherCallData);
+          sinon.assert.notCalled(getDataStore1.getWindowData);
+          sinon.assert.notCalled(getDataStore2.getWindowData);
         });
       });
 
       it("should not dispatch an action if the previous action hasn't finished", function() {
         // Dispatch the first action. The action handler dispatches the second
         // action - see the beforeEach above.
         dispatcher.dispatch(connectAction);
 
@@ -127,14 +126,14 @@ describe("loop.Dispatcher", function () 
 
       it("should dispatch an action when the previous action finishes", function() {
         // Dispatch the first action. The action handler dispatches the second
         // action - see the beforeEach above.
         dispatcher.dispatch(connectAction);
 
         sinon.assert.calledOnce(connectStore1.connectCall);
         // These should be called, because the dispatcher synchronously queues actions.
-        sinon.assert.calledOnce(gatherStore1.gatherCallData);
-        sinon.assert.calledOnce(gatherStore2.gatherCallData);
+        sinon.assert.calledOnce(getDataStore1.getWindowData);
+        sinon.assert.calledOnce(getDataStore2.getWindowData);
       });
     });
   });
 });
--- a/browser/components/loop/test/shared/localRoomStore_test.js
+++ b/browser/components/loop/test/shared/localRoomStore_test.js
@@ -26,24 +26,25 @@ describe("loop.store.LocalRoomStore", fu
 
     it("should throw an error if mozLoop is missing", function() {
       expect(function() {
         new loop.store.LocalRoomStore({dispatcher: dispatcher});
       }).to.Throw(/mozLoop/);
     });
   });
 
-  describe("#setupEmptyRoom", function() {
+  describe("#setupWindowData", function() {
     var store, fakeMozLoop, fakeRoomId, fakeRoomName;
 
     beforeEach(function() {
       fakeRoomId = "337-ff-54";
       fakeRoomName = "Monkeys";
       fakeMozLoop = {
-        rooms: { getRoomData: sandbox.stub() }
+        rooms: { getRoomData: sandbox.stub() },
+        getLoopBoolPref: function () { return false; }
       };
 
       store = new loop.store.LocalRoomStore(
         {mozLoop: fakeMozLoop, dispatcher: dispatcher});
       fakeMozLoop.rooms.getRoomData.
         withArgs(fakeRoomId).
         callsArgOnWith(1, // index of callback argument
         store, // |this| to call it on
@@ -52,44 +53,56 @@ describe("loop.store.LocalRoomStore", fu
       );
     });
 
     it("should trigger a change event", function(done) {
       store.on("change", function() {
         done();
       });
 
-      dispatcher.dispatch(new sharedActions.SetupEmptyRoom(
-        {localRoomId: fakeRoomId}));
+      dispatcher.dispatch(new sharedActions.SetupWindowData({
+        windowData: {
+          type: "room",
+          localRoomId: fakeRoomId
+        }
+      }));
     });
 
     it("should set localRoomId on the store from the action data",
       function(done) {
 
         store.once("change", function () {
           expect(store.getStoreState()).
             to.have.property('localRoomId', fakeRoomId);
           done();
         });
 
-        dispatcher.dispatch(
-          new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
-    });
+        dispatcher.dispatch(new sharedActions.SetupWindowData({
+          windowData: {
+            type: "room",
+            localRoomId: fakeRoomId
+          }
+        }));
+      });
 
     it("should set serverData.roomName from the getRoomData callback",
       function(done) {
 
         store.once("change", function () {
           expect(store.getStoreState()).to.have.deep.property(
             'serverData.roomName', fakeRoomName);
           done();
         });
 
-        dispatcher.dispatch(
-          new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
+        dispatcher.dispatch(new sharedActions.SetupWindowData({
+          windowData: {
+            type: "room",
+            localRoomId: fakeRoomId
+          }
+        }));
       });
 
     it("should set error on the store when getRoomData calls back an error",
       function(done) {
 
         var fakeError = new Error("fake error");
         fakeMozLoop.rooms.getRoomData.
           withArgs(fakeRoomId).
@@ -97,14 +110,18 @@ describe("loop.store.LocalRoomStore", fu
           store, // |this| to call it on
           fakeError); // args to call the callback with...
 
         store.once("change", function() {
           expect(this.getStoreState()).to.have.property('error', fakeError);
           done();
         });
 
-        dispatcher.dispatch(
-          new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
+        dispatcher.dispatch(new sharedActions.SetupWindowData({
+          windowData: {
+            type: "room",
+            localRoomId: fakeRoomId
+          }
+        }));
       });
 
   });
 });
--- a/browser/components/loop/test/xpcshell/test_loopservice_directcall.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_directcall.js
@@ -21,29 +21,29 @@ add_task(function test_startDirectCall_o
     openedUrl = url;
   };
 
   LoopCalls.startDirectCall(contact, "audio-video");
 
   do_check_true(!!openedUrl, "should open a chat window");
 
   // Stop the busy kicking in for following tests.
-  let callId = openedUrl.match(/about:loopconversation\#outgoing\/(.*)/)[1];
+  let callId = openedUrl.match(/about:loopconversation\#(\d+)$/)[1];
   LoopCalls.releaseCallData(callId);
 });
 
 add_task(function test_startDirectCall_getCallData() {
   let openedUrl;
   Chat.open = function(contentWindow, origin, title, url) {
     openedUrl = url;
   };
 
   LoopCalls.startDirectCall(contact, "audio-video");
 
-  let callId = openedUrl.match(/about:loopconversation\#outgoing\/(.*)/)[1];
+  let callId = openedUrl.match(/about:loopconversation\#(\d+)$/)[1];
 
   let callData = LoopCalls.getCallData(callId);
 
   do_check_eq(callData.callType, "audio-video", "should have the correct call type");
   do_check_eq(callData.contact, contact, "should have the contact details");
 
   // Stop the busy kicking in for following tests.
   LoopCalls.releaseCallData(callId);