Bug 1074686 - Part 2: Implement room views for Loop Desktop. r=Standard8 a=loop-only
authorNicolas Perriault <nperriault@gmail.com>
Mon, 10 Nov 2014 14:42:39 +0000
changeset 233913 b2276210d8b80dbbe4856535350d09936ac1de91
parent 233912 2f9a72986443e6068f11f2474c87f088b4a282d8
child 233914 fcc39c3f52532de8c8d2f85fd13031e14875e108
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8, loop-only
bugs1074686
milestone35.0a2
Bug 1074686 - Part 2: Implement room views for Loop Desktop. r=Standard8 a=loop-only
browser/components/loop/content/js/conversation.js
browser/components/loop/content/js/conversation.jsx
browser/components/loop/content/js/roomViews.js
browser/components/loop/content/js/roomViews.jsx
browser/components/loop/content/shared/css/conversation.css
browser/components/loop/content/shared/js/activeRoomStore.js
browser/components/loop/content/shared/js/mixins.js
browser/components/loop/content/shared/js/views.js
browser/components/loop/content/shared/js/views.jsx
browser/components/loop/test/desktop-local/conversation_test.js
browser/components/loop/test/desktop-local/roomViews_test.js
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -13,17 +13,17 @@ loop.conversation = (function(mozL10n) {
 
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
   var sharedActions = loop.shared.actions;
 
   var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
   var CallIdentifierView = loop.conversationViews.CallIdentifierView;
-  var DesktopRoomView = loop.roomViews.DesktopRoomView;
+  var DesktopRoomControllerView = loop.roomViews.DesktopRoomControllerView;
 
   var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
     mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
 
     propTypes: {
       model: React.PropTypes.object.isRequired,
       video: React.PropTypes.bool.isRequired
     },
@@ -579,18 +579,19 @@ loop.conversation = (function(mozL10n) {
         }
         case "outgoing": {
           return (OutgoingConversationView({
             store: this.props.conversationStore, 
             dispatcher: this.props.dispatcher}
           ));
         }
         case "room": {
-          return (DesktopRoomView({
+          return (DesktopRoomControllerView({
             mozLoop: navigator.mozLoop, 
+            dispatcher: this.props.dispatcher, 
             roomStore: this.props.roomStore}
           ));
         }
         case "failed": {
           return (GenericFailureView({
             cancelCall: this.closeWindow}
           ));
         }
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -13,17 +13,17 @@ loop.conversation = (function(mozL10n) {
 
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
   var sharedActions = loop.shared.actions;
 
   var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
   var CallIdentifierView = loop.conversationViews.CallIdentifierView;
-  var DesktopRoomView = loop.roomViews.DesktopRoomView;
+  var DesktopRoomControllerView = loop.roomViews.DesktopRoomControllerView;
 
   var IncomingCallView = React.createClass({
     mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
 
     propTypes: {
       model: React.PropTypes.object.isRequired,
       video: React.PropTypes.bool.isRequired
     },
@@ -579,18 +579,19 @@ loop.conversation = (function(mozL10n) {
         }
         case "outgoing": {
           return (<OutgoingConversationView
             store={this.props.conversationStore}
             dispatcher={this.props.dispatcher}
           />);
         }
         case "room": {
-          return (<DesktopRoomView
+          return (<DesktopRoomControllerView
             mozLoop={navigator.mozLoop}
+            dispatcher={this.props.dispatcher}
             roomStore={this.props.roomStore}
           />);
         }
         case "failed": {
           return (<GenericFailureView
             cancelCall={this.closeWindow}
           />);
         }
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -1,76 +1,206 @@
 /** @jsx React.DOM */
 
 /* 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/. */
 
+/* jshint newcap:false */
 /* global loop:true, React */
 
 var loop = loop || {};
 loop.roomViews = (function(mozL10n) {
   "use strict";
 
   var ROOM_STATES = loop.store.ROOM_STATES;
+  var sharedViews = loop.shared.views;
 
-  var DesktopRoomView = React.createClass({displayName: 'DesktopRoomView',
-    mixins: [Backbone.Events, loop.shared.mixins.DocumentTitleMixin],
+  function noop() {}
+
+  /**
+   * ActiveRoomStore mixin.
+   * @type {Object}
+   */
+  var ActiveRoomStoreMixin = {
+    mixins: [Backbone.Events],
 
     propTypes: {
-      mozLoop:   React.PropTypes.object.isRequired,
-      roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired,
-    },
-
-    getInitialState: function() {
-      return this.props.roomStore.getStoreState("activeRoom");
+      roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
 
     componentWillMount: function() {
       this.listenTo(this.props.roomStore, "change:activeRoom",
                     this._onActiveRoomStateChanged);
     },
 
-    /**
-     * Handles a "change" event on the roomStore, and updates this.state
-     * to match the store.
-     *
-     * @private
-     */
+    componentWillUnmount: function() {
+      this.stopListening(this.props.roomStore);
+    },
+
     _onActiveRoomStateChanged: function() {
       this.setState(this.props.roomStore.getStoreState("activeRoom"));
     },
 
-    componentWillUnmount: function() {
-      this.stopListening(this.props.roomStore);
+    getInitialState: function() {
+      return this.props.roomStore.getStoreState("activeRoom");
+    }
+  };
+
+  /**
+   * Desktop room invitation view (overlay).
+   */
+  var DesktopRoomInvitationView = React.createClass({displayName: 'DesktopRoomInvitationView',
+    mixins: [ActiveRoomStoreMixin],
+
+    propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+    },
+
+    handleFormSubmit: function(event) {
+      event.preventDefault();
+      // XXX
+    },
+
+    handleEmailButtonClick: function(event) {
+      event.preventDefault();
+      // XXX
+    },
+
+    handleCopyButtonClick: function(event) {
+      event.preventDefault();
+      // XXX
+    },
+
+    render: function() {
+      return (
+        React.DOM.div({className: "room-conversation-wrapper"}, 
+          React.DOM.div({className: "room-invitation-overlay"}, 
+            React.DOM.form({onSubmit: this.handleFormSubmit}, 
+              React.DOM.input({type: "text", ref: "roomName", 
+                placeholder: mozL10n.get("rooms_name_this_room_label")})
+            ), 
+            React.DOM.p(null, mozL10n.get("invite_header_text")), 
+            React.DOM.div({className: "btn-group call-action-group"}, 
+              React.DOM.button({className: "btn btn-info btn-email", 
+                      onClick: this.handleEmailButtonClick}, 
+                mozL10n.get("share_button2")
+              ), 
+              React.DOM.button({className: "btn btn-info btn-copy", 
+                      onClick: this.handleCopyButtonClick}, 
+                mozL10n.get("copy_url_button2")
+              )
+            )
+          ), 
+          DesktopRoomConversationView({roomStore: this.props.roomStore})
+        )
+      );
+    }
+  });
+
+  /**
+   * Desktop room conversation view.
+   */
+  var DesktopRoomConversationView = React.createClass({displayName: 'DesktopRoomConversationView',
+    mixins: [ActiveRoomStoreMixin],
+
+    propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      video: React.PropTypes.object,
+      audio: React.PropTypes.object,
+      displayInvitation: React.PropTypes.bool
     },
 
-    /**
-     * Closes the window if the cancel button is pressed in the generic failure view.
-     */
+    getDefaultProps: function() {
+      return {
+        video: {enabled: true, visible: true},
+        audio: {enabled: true, visible: true}
+      };
+    },
+
+    render: function() {
+      var localStreamClasses = React.addons.classSet({
+        local: true,
+        "local-stream": true,
+        "local-stream-audio": !this.props.video.enabled
+      });
+      return (
+        React.DOM.div({className: "room-conversation-wrapper"}, 
+          React.DOM.div({className: "video-layout-wrapper"}, 
+            React.DOM.div({className: "conversation room-conversation"}, 
+              React.DOM.div({className: "media nested"}, 
+                React.DOM.div({className: "video_wrapper remote_wrapper"}, 
+                  React.DOM.div({className: "video_inner remote"})
+                ), 
+                React.DOM.div({className: localStreamClasses})
+              ), 
+              sharedViews.ConversationToolbar({
+                video: this.props.video, 
+                audio: this.props.audio, 
+                publishStream: noop, 
+                hangup: noop})
+            )
+          )
+        )
+      );
+    }
+  });
+
+  /**
+   * Desktop room controller view.
+   */
+  var DesktopRoomControllerView = React.createClass({displayName: 'DesktopRoomControllerView',
+    mixins: [ActiveRoomStoreMixin, loop.shared.mixins.DocumentTitleMixin],
+
+    propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+    },
+
     closeWindow: function() {
       window.close();
     },
 
+    _renderRoomView: function(roomState) {
+      switch (roomState) {
+        case ROOM_STATES.FAILED: {
+          return loop.conversation.GenericFailureView({
+            cancelCall: this.closeWindow}
+          );
+        }
+        case ROOM_STATES.INIT:
+        case ROOM_STATES.GATHER:
+        case ROOM_STATES.READY:
+        case ROOM_STATES.JOINED: {
+          return DesktopRoomInvitationView({
+            dispatcher: this.props.dispatcher, 
+            roomStore: this.props.roomStore}
+          );
+        }
+        // XXX needs bug 1074686/1074702
+        case ROOM_STATES.HAS_PARTICIPANTS: {
+          return DesktopRoomConversationView({
+            dispatcher: this.props.dispatcher, 
+            roomStore: this.props.roomStore}
+          );
+        }
+      }
+    },
+
     render: function() {
       if (this.state.roomName) {
         this.setTitle(this.state.roomName);
       }
-
-      if (this.state.roomState === ROOM_STATES.FAILED) {
-        return (loop.conversation.GenericFailureView({
-          cancelCall: this.closeWindow}
-        ));
-      }
-
       return (
-        React.DOM.div(null, 
-          React.DOM.div(null, mozL10n.get("invite_header_text"))
+        React.DOM.div({className: "room-conversation-wrapper"}, 
+          this._renderRoomView(this.state.roomState)
         )
       );
     }
   });
 
   return {
-    DesktopRoomView: DesktopRoomView
+    ActiveRoomStoreMixin: ActiveRoomStoreMixin,
+    DesktopRoomControllerView: DesktopRoomControllerView,
+    DesktopRoomConversationView: DesktopRoomConversationView,
+    DesktopRoomInvitationView: DesktopRoomInvitationView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -1,76 +1,206 @@
 /** @jsx React.DOM */
 
 /* 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/. */
 
+/* jshint newcap:false */
 /* global loop:true, React */
 
 var loop = loop || {};
 loop.roomViews = (function(mozL10n) {
   "use strict";
 
   var ROOM_STATES = loop.store.ROOM_STATES;
+  var sharedViews = loop.shared.views;
 
-  var DesktopRoomView = React.createClass({
-    mixins: [Backbone.Events, loop.shared.mixins.DocumentTitleMixin],
+  function noop() {}
+
+  /**
+   * ActiveRoomStore mixin.
+   * @type {Object}
+   */
+  var ActiveRoomStoreMixin = {
+    mixins: [Backbone.Events],
 
     propTypes: {
-      mozLoop:   React.PropTypes.object.isRequired,
-      roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired,
-    },
-
-    getInitialState: function() {
-      return this.props.roomStore.getStoreState("activeRoom");
+      roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
 
     componentWillMount: function() {
       this.listenTo(this.props.roomStore, "change:activeRoom",
                     this._onActiveRoomStateChanged);
     },
 
-    /**
-     * Handles a "change" event on the roomStore, and updates this.state
-     * to match the store.
-     *
-     * @private
-     */
+    componentWillUnmount: function() {
+      this.stopListening(this.props.roomStore);
+    },
+
     _onActiveRoomStateChanged: function() {
       this.setState(this.props.roomStore.getStoreState("activeRoom"));
     },
 
-    componentWillUnmount: function() {
-      this.stopListening(this.props.roomStore);
+    getInitialState: function() {
+      return this.props.roomStore.getStoreState("activeRoom");
+    }
+  };
+
+  /**
+   * Desktop room invitation view (overlay).
+   */
+  var DesktopRoomInvitationView = React.createClass({
+    mixins: [ActiveRoomStoreMixin],
+
+    propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+    },
+
+    handleFormSubmit: function(event) {
+      event.preventDefault();
+      // XXX
+    },
+
+    handleEmailButtonClick: function(event) {
+      event.preventDefault();
+      // XXX
+    },
+
+    handleCopyButtonClick: function(event) {
+      event.preventDefault();
+      // XXX
+    },
+
+    render: function() {
+      return (
+        <div className="room-conversation-wrapper">
+          <div className="room-invitation-overlay">
+            <form onSubmit={this.handleFormSubmit}>
+              <input type="text" ref="roomName"
+                placeholder={mozL10n.get("rooms_name_this_room_label")} />
+            </form>
+            <p>{mozL10n.get("invite_header_text")}</p>
+            <div className="btn-group call-action-group">
+              <button className="btn btn-info btn-email"
+                      onClick={this.handleEmailButtonClick}>
+                {mozL10n.get("share_button2")}
+              </button>
+              <button className="btn btn-info btn-copy"
+                      onClick={this.handleCopyButtonClick}>
+                {mozL10n.get("copy_url_button2")}
+              </button>
+            </div>
+          </div>
+          <DesktopRoomConversationView roomStore={this.props.roomStore} />
+        </div>
+      );
+    }
+  });
+
+  /**
+   * Desktop room conversation view.
+   */
+  var DesktopRoomConversationView = React.createClass({
+    mixins: [ActiveRoomStoreMixin],
+
+    propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      video: React.PropTypes.object,
+      audio: React.PropTypes.object,
+      displayInvitation: React.PropTypes.bool
     },
 
-    /**
-     * Closes the window if the cancel button is pressed in the generic failure view.
-     */
+    getDefaultProps: function() {
+      return {
+        video: {enabled: true, visible: true},
+        audio: {enabled: true, visible: true}
+      };
+    },
+
+    render: function() {
+      var localStreamClasses = React.addons.classSet({
+        local: true,
+        "local-stream": true,
+        "local-stream-audio": !this.props.video.enabled
+      });
+      return (
+        <div className="room-conversation-wrapper">
+          <div className="video-layout-wrapper">
+            <div className="conversation room-conversation">
+              <div className="media nested">
+                <div className="video_wrapper remote_wrapper">
+                  <div className="video_inner remote"></div>
+                </div>
+                <div className={localStreamClasses}></div>
+              </div>
+              <sharedViews.ConversationToolbar
+                video={this.props.video}
+                audio={this.props.audio}
+                publishStream={noop}
+                hangup={noop} />
+            </div>
+          </div>
+        </div>
+      );
+    }
+  });
+
+  /**
+   * Desktop room controller view.
+   */
+  var DesktopRoomControllerView = React.createClass({
+    mixins: [ActiveRoomStoreMixin, loop.shared.mixins.DocumentTitleMixin],
+
+    propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+    },
+
     closeWindow: function() {
       window.close();
     },
 
+    _renderRoomView: function(roomState) {
+      switch (roomState) {
+        case ROOM_STATES.FAILED: {
+          return <loop.conversation.GenericFailureView
+            cancelCall={this.closeWindow}
+          />;
+        }
+        case ROOM_STATES.INIT:
+        case ROOM_STATES.GATHER:
+        case ROOM_STATES.READY:
+        case ROOM_STATES.JOINED: {
+          return <DesktopRoomInvitationView
+            dispatcher={this.props.dispatcher}
+            roomStore={this.props.roomStore}
+          />;
+        }
+        // XXX needs bug 1074686/1074702
+        case ROOM_STATES.HAS_PARTICIPANTS: {
+          return <DesktopRoomConversationView
+            dispatcher={this.props.dispatcher}
+            roomStore={this.props.roomStore}
+          />;
+        }
+      }
+    },
+
     render: function() {
       if (this.state.roomName) {
         this.setTitle(this.state.roomName);
       }
-
-      if (this.state.roomState === ROOM_STATES.FAILED) {
-        return (<loop.conversation.GenericFailureView
-          cancelCall={this.closeWindow}
-        />);
-      }
-
       return (
-        <div>
-          <div>{mozL10n.get("invite_header_text")}</div>
-        </div>
+        <div className="room-conversation-wrapper">{
+          this._renderRoomView(this.state.roomState)
+        }</div>
       );
     }
   });
 
   return {
-    DesktopRoomView: DesktopRoomView
+    ActiveRoomStoreMixin: ActiveRoomStoreMixin,
+    DesktopRoomControllerView: DesktopRoomControllerView,
+    DesktopRoomConversationView: DesktopRoomConversationView,
+    DesktopRoomInvitationView: DesktopRoomInvitationView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -672,8 +672,56 @@ html, .fx-embedded, #main,
   /* Restore video height so that we get
    * vertical centering for free on a small screen
    **/
   .standalone .conversation .media video {
     height: 100%;
   }
 }
 
+/**
+ * Rooms
+ */
+
+.room-conversation-wrapper {
+  position: relative;
+  height: 100%;
+}
+
+/**
+ * Hides the hangup button for room conversations.
+ */
+.room-conversation .conversation-toolbar .btn-hangup-entry {
+  display: none;
+}
+
+.room-invitation-overlay {
+  position: absolute;
+  background: rgba(0, 0, 0, .6);
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  text-align: center;
+  color: #fff;
+  z-index: 1010;
+}
+
+.room-invitation-overlay form {
+  padding: 8em 0 2.5em 0;
+}
+
+.room-invitation-overlay input[type="text"] {
+  display: block;
+  background: rgba(0, 0, 0, .5);
+  color: #fff;
+  font-size: 1.2em;
+  border: none;
+  border-radius: 3px;
+  padding: .5em;
+  width: 200px;
+  margin: 0 auto;
+}
+
+.room-invitation-overlay .btn-group {
+  position: absolute;
+  bottom: 10px;
+}
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -16,17 +16,19 @@ loop.store.ActiveRoomStore = (function()
     INIT: "room-init",
     // The store is gathering the room data
     GATHER: "room-gather",
     // The store has got the room data
     READY: "room-ready",
     // The room is known to be joined on the loop-server
     JOINED: "room-joined",
     // There was an issue with the room
-    FAILED: "room-failed"
+    FAILED: "room-failed",
+    // XXX to be implemented in bug 1074686/1074702
+    HAS_PARTICIPANTS: "room-has-participants"
   };
 
   /**
    * Store for things that are local to this instance (in this profile, on
    * this machine) of this roomRoom store, in addition to a mirror of some
    * remote-state.
    *
    * @extends {Backbone.Events}
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -139,17 +139,18 @@ loop.shared.mixins = (function() {
    * Audio mixin. Allows playing a single audio file and ensuring it
    * is stopped when the component is unmounted.
    */
   var AudioMixin = {
     audio: null,
     _audioRequest: null,
 
     _isLoopDesktop: function() {
-      return typeof rootObject.navigator.mozLoop === "object";
+      return rootObject.navigator &&
+             typeof rootObject.navigator.mozLoop === "object";
     },
 
     /**
      * Starts playing an audio file, stopping any audio that is already in progress.
      *
      * @param {String} name The filename to play (excluding the extension).
      */
     play: function(name, options) {
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -103,17 +103,17 @@ loop.shared.views = (function(_, OT, l10
     handleToggleAudio: function() {
       this.props.publishStream("audio", !this.props.audio.enabled);
     },
 
     render: function() {
       var cx = React.addons.classSet;
       return (
         React.DOM.ul({className: "conversation-toolbar"}, 
-          React.DOM.li({className: "conversation-toolbar-btn-box"}, 
+          React.DOM.li({className: "conversation-toolbar-btn-box btn-hangup-entry"}, 
             React.DOM.button({className: "btn btn-hangup", onClick: this.handleClickHangup, 
                     title: l10n.get("hangup_button_title")}, 
               l10n.get("hangup_button_caption2")
             )
           ), 
           React.DOM.li({className: "conversation-toolbar-btn-box"}, 
             MediaControlButton({action: this.handleToggleVideo, 
                                 enabled: this.props.video.enabled, 
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -103,17 +103,17 @@ loop.shared.views = (function(_, OT, l10
     handleToggleAudio: function() {
       this.props.publishStream("audio", !this.props.audio.enabled);
     },
 
     render: function() {
       var cx = React.addons.classSet;
       return (
         <ul className="conversation-toolbar">
-          <li className="conversation-toolbar-btn-box">
+          <li className="conversation-toolbar-btn-box btn-hangup-entry">
             <button className="btn btn-hangup" onClick={this.handleClickHangup}
                     title={l10n.get("hangup_button_title")}>
               {l10n.get("hangup_button_caption2")}
             </button>
           </li>
           <li className="conversation-toolbar-btn-box">
             <MediaControlButton action={this.handleToggleVideo}
                                 enabled={this.props.video.enabled}
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -209,17 +209,17 @@ describe("loop.conversation", function()
     });
 
     it("should display the RoomView for rooms", function() {
       conversationAppStore.setStoreState({windowType: "room"});
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
-        loop.roomViews.DesktopRoomView);
+        loop.roomViews.DesktopRoomControllerView);
     });
 
     it("should display the GenericFailureView for failures", function() {
       conversationAppStore.setStoreState({windowType: "failed"});
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -1,10 +1,12 @@
 var expect = chai.expect;
 
+/* jshint newcap:false */
+
 describe("loop.roomViews", function () {
   "use strict";
 
   var ROOM_STATES = loop.store.ROOM_STATES;
 
   var sandbox, dispatcher, roomStore, activeRoomStore, fakeWindow;
 
   beforeEach(function() {
@@ -23,59 +25,111 @@ describe("loop.roomViews", function () {
     loop.shared.mixins.setRootObject(fakeWindow);
 
     // XXX These stubs should be hoisted in a common file
     // Bug 1040968
     sandbox.stub(document.mozL10n, "get", function(x) {
       return x;
     });
 
-
     activeRoomStore = new loop.store.ActiveRoomStore({
       dispatcher: dispatcher,
       mozLoop: {}
     });
     roomStore = new loop.store.RoomStore({
       dispatcher: dispatcher,
       mozLoop: {},
       activeRoomStore: activeRoomStore
     });
   });
 
   afterEach(function() {
     sandbox.restore();
     loop.shared.mixins.setRootObject(window);
   });
 
-  describe("DesktopRoomView", function() {
+  describe("ActiveRoomStoreMixin", function() {
+    it("should merge initial state", function() {
+      var TestView = React.createClass({
+        mixins: [loop.roomViews.ActiveRoomStoreMixin],
+        getInitialState: function() {
+          return {foo: "bar"};
+        },
+        render: function() { return React.DOM.div(); }
+      });
+
+      var testView = TestUtils.renderIntoDocument(TestView({
+        roomStore: activeRoomStore
+      }));
+
+      expect(testView.state).eql({
+        roomState: ROOM_STATES.INIT,
+        foo: "bar"
+      });
+    });
+
+    it("should listen to store changes", function() {
+      var TestView = React.createClass({
+        mixins: [loop.roomViews.ActiveRoomStoreMixin],
+        render: function() { return React.DOM.div(); }
+      });
+      var testView = TestUtils.renderIntoDocument(TestView({
+        roomStore: activeRoomStore
+      }));
+
+      activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
+
+      expect(testView.state).eql({roomState: ROOM_STATES.READY});
+    });
+  });
+
+  describe("DesktopRoomControllerView", function() {
     var view;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
-        new loop.roomViews.DesktopRoomView({
+        new loop.roomViews.DesktopRoomControllerView({
           mozLoop: {},
           roomStore: roomStore
         }));
     }
 
     describe("#render", function() {
       it("should set document.title to store.serverData.roomName", function() {
         mountTestComponent();
 
         activeRoomStore.setStoreState({roomName: "fakeName"});
 
         expect(fakeWindow.document.title).to.equal("fakeName");
       });
 
-      it("should render the GenericFailureView if the roomState is `FAILED`", function() {
-        activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED});
+      it("should render the GenericFailureView if the roomState is `FAILED`",
+        function() {
+          activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED});
+
+          view = mountTestComponent();
 
-        view = mountTestComponent();
+          TestUtils.findRenderedComponentWithType(view,
+            loop.conversation.GenericFailureView);
+        });
+
+      it("should render the DesktopRoomInvitationView if roomState is `JOINED`",
+        function() {
+          activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
 
-        TestUtils.findRenderedComponentWithType(view,
-          loop.conversation.GenericFailureView);
-      });
+          view = mountTestComponent();
+
+          TestUtils.findRenderedComponentWithType(view,
+            loop.roomViews.DesktopRoomInvitationView);
+        });
 
-      // XXX Implement this when we do the rooms views in bug 1074686 and others.
-      it("should display the main view");
+      it("should render the DesktopRoomConversationView if roomState is `HAS_PARTICIPANTS`",
+        function() {
+          activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
+
+          view = mountTestComponent();
+
+          TestUtils.findRenderedComponentWithType(view,
+            loop.roomViews.DesktopRoomConversationView);
+        });
     });
   });
 });
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -16,16 +16,18 @@
 
   // 1. Desktop components
   // 1.1 Panel
   var PanelView = loop.panel.PanelView;
   // 1.2. Conversation Window
   var IncomingCallView = loop.conversation.IncomingCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var CallFailedView = loop.conversationViews.CallFailedView;
+  var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
+  var DesktopRoomInvitationView = loop.roomViews.DesktopRoomInvitationView;
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var CallUrlExpiredView      = loop.webapp.CallUrlExpiredView;
   var PendingConversationView = loop.webapp.PendingConversationView;
   var StartConversationView   = loop.webapp.StartConversationView;
@@ -52,16 +54,20 @@
   // which is available at https://input.allizom.org
   var stageFeedbackApiClient = new loop.FeedbackAPIClient(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
   var dispatcher = new loop.Dispatcher();
+  var activeRoomStore = new loop.store.ActiveRoomStore({
+    dispatcher: dispatcher,
+    mozLoop: navigator.mozLoop
+  });
   var roomStore = new loop.store.RoomStore({
     dispatcher: dispatcher,
     mozLoop: navigator.mozLoop
   });
 
   // Local mocks
 
   var mockContact = {
@@ -522,16 +528,34 @@
           Section({name: "UnsupportedDeviceView"}, 
             Example({summary: "Standalone Unsupported Device"}, 
               React.DOM.div({className: "standalone"}, 
                 UnsupportedDeviceView(null)
               )
             )
           ), 
 
+          Section({name: "DesktopRoomInvitationView"}, 
+            Example({summary: "Desktop room invitation", dashed: "true", 
+                     style: {width: "260px", height: "265px"}}, 
+              React.DOM.div({className: "fx-embedded"}, 
+                DesktopRoomInvitationView({roomStore: roomStore})
+              )
+            )
+          ), 
+
+          Section({name: "DesktopRoomConversationView"}, 
+            Example({summary: "Desktop room conversation", dashed: "true", 
+                     style: {width: "260px", height: "265px"}}, 
+              React.DOM.div({className: "fx-embedded"}, 
+                DesktopRoomConversationView({roomStore: roomStore})
+              )
+            )
+          ), 
+
           Section({name: "SVG icons preview"}, 
             Example({summary: "16x16"}, 
               SVGIcons(null)
             )
           )
 
         )
       );
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -16,16 +16,18 @@
 
   // 1. Desktop components
   // 1.1 Panel
   var PanelView = loop.panel.PanelView;
   // 1.2. Conversation Window
   var IncomingCallView = loop.conversation.IncomingCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var CallFailedView = loop.conversationViews.CallFailedView;
+  var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
+  var DesktopRoomInvitationView = loop.roomViews.DesktopRoomInvitationView;
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var CallUrlExpiredView      = loop.webapp.CallUrlExpiredView;
   var PendingConversationView = loop.webapp.PendingConversationView;
   var StartConversationView   = loop.webapp.StartConversationView;
@@ -52,16 +54,20 @@
   // which is available at https://input.allizom.org
   var stageFeedbackApiClient = new loop.FeedbackAPIClient(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
   var dispatcher = new loop.Dispatcher();
+  var activeRoomStore = new loop.store.ActiveRoomStore({
+    dispatcher: dispatcher,
+    mozLoop: navigator.mozLoop
+  });
   var roomStore = new loop.store.RoomStore({
     dispatcher: dispatcher,
     mozLoop: navigator.mozLoop
   });
 
   // Local mocks
 
   var mockContact = {
@@ -522,16 +528,34 @@
           <Section name="UnsupportedDeviceView">
             <Example summary="Standalone Unsupported Device">
               <div className="standalone">
                 <UnsupportedDeviceView />
               </div>
             </Example>
           </Section>
 
+          <Section name="DesktopRoomInvitationView">
+            <Example summary="Desktop room invitation" dashed="true"
+                     style={{width: "260px", height: "265px"}}>
+              <div className="fx-embedded">
+                <DesktopRoomInvitationView roomStore={roomStore} />
+              </div>
+            </Example>
+          </Section>
+
+          <Section name="DesktopRoomConversationView">
+            <Example summary="Desktop room conversation" dashed="true"
+                     style={{width: "260px", height: "265px"}}>
+              <div className="fx-embedded">
+                <DesktopRoomConversationView roomStore={roomStore} />
+              </div>
+            </Example>
+          </Section>
+
           <Section name="SVG icons preview">
             <Example summary="16x16">
               <SVGIcons />
             </Example>
           </Section>
 
         </ShowCase>
       );