Bug 1142514: show context information in the Loop conversation window. r=Standard8
authorMike de Boer <mdeboer@mozilla.com>
Tue, 21 Apr 2015 12:06:59 +0200
changeset 240194 7ca87bde48a4e9712b1ccaee20532cbff9fffa20
parent 240193 76ff9ca8cc1a41573243d755f48d990988b34210
child 240195 d7b5d8ab603415137a9c77bfc82d32efbd42d61b
push id28626
push userkwierso@gmail.com
push dateTue, 21 Apr 2015 22:16:34 +0000
treeherdermozilla-central@4cd40d0c7860 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1142514
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1142514: show context information in the Loop conversation window. r=Standard8
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/actions.js
browser/components/loop/content/shared/js/activeRoomStore.js
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -44,24 +44,23 @@ loop.conversation = (function(mozL10n) {
 
     render: function() {
       switch(this.state.windowType) {
         // CallControllerView is used for both.
         case "incoming":
         case "outgoing": {
           return (React.createElement(CallControllerView, {
             dispatcher: this.props.dispatcher, 
-            mozLoop: this.props.mozLoop}
-          ));
+            mozLoop: this.props.mozLoop}));
         }
         case "room": {
           return (React.createElement(DesktopRoomConversationView, {
             dispatcher: this.props.dispatcher, 
-            roomStore: this.props.roomStore}
-          ));
+            mozLoop: this.props.mozLoop, 
+            roomStore: this.props.roomStore}));
         }
         case "failed": {
           return React.createElement(GenericFailureView, {cancelCall: this.closeWindow});
         }
         default: {
           // If we don't have a windowType, we don't know what we are yet,
           // so don't display anything.
           return null;
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -44,24 +44,23 @@ loop.conversation = (function(mozL10n) {
 
     render: function() {
       switch(this.state.windowType) {
         // CallControllerView is used for both.
         case "incoming":
         case "outgoing": {
           return (<CallControllerView
             dispatcher={this.props.dispatcher}
-            mozLoop={this.props.mozLoop}
-          />);
+            mozLoop={this.props.mozLoop} />);
         }
         case "room": {
           return (<DesktopRoomConversationView
             dispatcher={this.props.dispatcher}
-            roomStore={this.props.roomStore}
-          />);
+            mozLoop={this.props.mozLoop}
+            roomStore={this.props.roomStore} />);
         }
         case "failed": {
           return <GenericFailureView cancelCall={this.closeWindow} />;
         }
         default: {
           // If we don't have a windowType, we don't know what we are yet,
           // so don't display anything.
           return null;
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -26,46 +26,58 @@ loop.roomViews = (function(mozL10n) {
 
     propTypes: {
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
 
     componentWillMount: function() {
       this.listenTo(this.props.roomStore, "change:activeRoom",
                     this._onActiveRoomStateChanged);
+      this.listenTo(this.props.roomStore, "change:error",
+                    this._onRoomError);
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.props.roomStore);
     },
 
     _onActiveRoomStateChanged: function() {
       // Only update the state if we're mounted, to avoid the problem where
       // stopListening doesn't nuke the active listeners during a event
       // processing.
       if (this.isMounted()) {
         this.setState(this.props.roomStore.getStoreState("activeRoom"));
       }
     },
 
+    _onRoomError: function() {
+      // Only update the state if we're mounted, to avoid the problem where
+      // stopListening doesn't nuke the active listeners during a event
+      // processing.
+      if (this.isMounted()) {
+        this.setState({error: this.props.roomStore.getStoreState("error")});
+      }
+    },
+
     getInitialState: function() {
       var storeState = this.props.roomStore.getStoreState("activeRoom");
       return _.extend({
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState
       }, storeState);
     }
   };
 
   var SocialShareDropdown = React.createClass({displayName: "SocialShareDropdown",
-    mixins: [ActiveRoomStoreMixin],
-
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      show: React.PropTypes.bool.isRequired
+      roomUrl: React.PropTypes.string,
+      show: React.PropTypes.bool.isRequired,
+      socialShareButtonAvailable: React.PropTypes.bool,
+      socialShareProviders: React.PropTypes.array
     },
 
     handleToolbarAddButtonClick: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(new sharedActions.AddSocialShareButton());
     },
 
@@ -74,44 +86,44 @@ loop.roomViews = (function(mozL10n) {
 
       this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());
     },
 
     handleProviderClick: function(event) {
       event.preventDefault();
 
       var origin = event.currentTarget.dataset.provider;
-      var provider = this.state.socialShareProviders.filter(function(provider) {
+      var provider = this.props.socialShareProviders.filter(function(provider) {
         return provider.origin == origin;
       })[0];
 
       this.props.dispatcher.dispatch(new sharedActions.ShareRoomUrl({
         provider: provider,
-        roomUrl: this.state.roomUrl,
+        roomUrl: this.props.roomUrl,
         previews: []
       }));
     },
 
     render: function() {
       // Don't render a thing when no data has been fetched yet.
-      if (!this.state.socialShareProviders) {
+      if (!this.props.socialShareProviders) {
         return null;
       }
 
       var cx = React.addons.classSet;
       var shareDropdown = cx({
         "share-service-dropdown": true,
         "dropdown-menu": true,
-        "share-button-unavailable": !this.state.socialShareButtonAvailable,
+        "share-button-unavailable": !this.props.socialShareButtonAvailable,
         "hide": !this.props.show
       });
 
       // When the button is not yet available, we offer to put it in the navbar
       // for the user.
-      if (!this.state.socialShareButtonAvailable) {
+      if (!this.props.socialShareButtonAvailable) {
         return (
           React.createElement("div", {className: shareDropdown}, 
             React.createElement("div", {className: "share-panel-header"}, 
               mozL10n.get("share_panel_header")
             ), 
             React.createElement("div", {className: "share-panel-body"}, 
               
                 mozL10n.get("share_panel_body", {
@@ -129,19 +141,19 @@ loop.roomViews = (function(mozL10n) {
       }
 
       return (
         React.createElement("ul", {className: shareDropdown}, 
           React.createElement("li", {className: "dropdown-menu-item", onClick: this.handleAddServiceClick}, 
             React.createElement("i", {className: "icon icon-add-share-service"}), 
             React.createElement("span", null, mozL10n.get("share_add_service_button"))
           ), 
-          this.state.socialShareProviders.length ? React.createElement("li", {className: "dropdown-menu-separator"}) : null, 
+          this.props.socialShareProviders.length ? React.createElement("li", {className: "dropdown-menu-separator"}) : null, 
           
-            this.state.socialShareProviders.map(function(provider, idx) {
+            this.props.socialShareProviders.map(function(provider, idx) {
               return (
                 React.createElement("li", {className: "dropdown-menu-item", 
                     key: "provider-" + idx, 
                     "data-provider": provider.origin, 
                     onClick: this.handleProviderClick}, 
                   React.createElement("img", {className: "icon", src: provider.iconURL}), 
                   React.createElement("span", null, provider.name)
                 )
@@ -152,126 +164,169 @@ loop.roomViews = (function(mozL10n) {
       );
     }
   });
 
   /**
    * Desktop room invitation view (overlay).
    */
   var DesktopRoomInvitationView = React.createClass({displayName: "DesktopRoomInvitationView",
-    mixins: [ActiveRoomStoreMixin, React.addons.LinkedStateMixin,
-             sharedMixins.DropdownMenuMixin],
+    mixins: [React.addons.LinkedStateMixin, sharedMixins.DropdownMenuMixin],
 
     propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      error: React.PropTypes.object,
+      // This data is supplied by the activeRoomStore.
+      roomData: React.PropTypes.object.isRequired,
+      show: React.PropTypes.bool.isRequired,
+      showContext: React.PropTypes.bool.isRequired
     },
 
     getInitialState: function() {
       return {
         copiedUrl: false,
-        newRoomName: "",
-        error: null,
+        newRoomName: ""
       };
     },
 
-    componentWillMount: function() {
-      this.listenTo(this.props.roomStore, "change:error",
-                    this.onRoomError);
-    },
-
-    componentWillUnmount: function() {
-      this.stopListening(this.props.roomStore);
-    },
-
     handleTextareaKeyDown: function(event) {
       // Submit the form as soon as the user press Enter in that field
       // Note: We're using a textarea instead of a simple text input to display
       // placeholder and entered text on two lines, to circumvent l10n
       // rendering/UX issues for some locales.
       if (event.which === 13) {
         this.handleFormSubmit(event);
       }
     },
 
     handleFormSubmit: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(new sharedActions.RenameRoom({
-        roomToken: this.state.roomToken,
+        roomToken: this.props.roomData.roomToken,
         newRoomName: this.state.newRoomName
       }));
     },
 
     handleEmailButtonClick: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(
-        new sharedActions.EmailRoomUrl({roomUrl: this.state.roomUrl}));
+        new sharedActions.EmailRoomUrl({roomUrl: this.props.roomData.roomUrl}));
     },
 
     handleCopyButtonClick: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(
-        new sharedActions.CopyRoomUrl({roomUrl: this.state.roomUrl}));
+        new sharedActions.CopyRoomUrl({roomUrl: this.props.roomData.roomUrl}));
 
       this.setState({copiedUrl: true});
     },
 
     handleShareButtonClick: function(event) {
       event.preventDefault();
 
       this.toggleDropdownMenu();
     },
 
-    onRoomError: function() {
-      // Only update the state if we're mounted, to avoid the problem where
-      // stopListening doesn't nuke the active listeners during a event
-      // processing.
-      if (this.isMounted()) {
-        this.setState({error: this.props.roomStore.getStoreState("error")});
+    render: function() {
+      if (!this.props.show) {
+        return null;
       }
-    },
 
-    render: function() {
       var cx = React.addons.classSet;
       return (
         React.createElement("div", {className: "room-invitation-overlay"}, 
-          React.createElement("p", {className: cx({"error": !!this.state.error,
-                            "error-display-area": true})}, 
-            mozL10n.get("rooms_name_change_failed_label")
-          ), 
-          React.createElement("form", {onSubmit: this.handleFormSubmit}, 
-            React.createElement("textarea", {rows: "2", type: "text", className: "input-room-name", 
-              valueLink: this.linkState("newRoomName"), 
-              onBlur: this.handleFormSubmit, 
-              onKeyDown: this.handleTextareaKeyDown, 
-              placeholder: mozL10n.get("rooms_name_this_room_label")})
+          React.createElement("div", {className: "room-invitation-content"}, 
+            React.createElement("p", {className: cx({"error": !!this.props.error,
+                              "error-display-area": true})}, 
+              mozL10n.get("rooms_name_change_failed_label")
+            ), 
+            React.createElement("form", {onSubmit: this.handleFormSubmit}, 
+              React.createElement("textarea", {rows: "2", type: "text", className: "input-room-name", 
+                valueLink: this.linkState("newRoomName"), 
+                onBlur: this.handleFormSubmit, 
+                onKeyDown: this.handleTextareaKeyDown, 
+                placeholder: mozL10n.get("rooms_name_this_room_label")})
+            ), 
+            React.createElement("p", null, mozL10n.get("invite_header_text")), 
+            React.createElement("div", {className: "btn-group call-action-group"}, 
+              React.createElement("button", {className: "btn btn-info btn-email", 
+                      onClick: this.handleEmailButtonClick}, 
+                mozL10n.get("email_link_button")
+              ), 
+              React.createElement("button", {className: "btn btn-info btn-copy", 
+                      onClick: this.handleCopyButtonClick}, 
+                this.state.copiedUrl ? mozL10n.get("copied_url_button") :
+                                        mozL10n.get("copy_url_button2")
+              ), 
+              React.createElement("button", {className: "btn btn-info btn-share", 
+                      ref: "anchor", 
+                      onClick: this.handleShareButtonClick}, 
+                mozL10n.get("share_button3")
+              )
+            ), 
+            React.createElement(SocialShareDropdown, {dispatcher: this.props.dispatcher, 
+                                 roomUrl: this.props.roomData.roomUrl, 
+                                 show: this.state.showMenu, 
+                                 ref: "menu"})
           ), 
-          React.createElement("p", null, mozL10n.get("invite_header_text")), 
-          React.createElement("div", {className: "btn-group call-action-group"}, 
-            React.createElement("button", {className: "btn btn-info btn-email", 
-                    onClick: this.handleEmailButtonClick}, 
-              mozL10n.get("email_link_button")
-            ), 
-            React.createElement("button", {className: "btn btn-info btn-copy", 
-                    onClick: this.handleCopyButtonClick}, 
-              this.state.copiedUrl ? mozL10n.get("copied_url_button") :
-                                      mozL10n.get("copy_url_button2")
-            ), 
-            React.createElement("button", {className: "btn btn-info btn-share", 
-                    ref: "anchor", 
-                    onClick: this.handleShareButtonClick}, 
-              mozL10n.get("share_button3")
-            )
-          ), 
-          React.createElement(SocialShareDropdown, {dispatcher: this.props.dispatcher, 
-                               roomStore: this.props.roomStore, 
-                               show: this.state.showMenu, 
-                               ref: "menu"})
+          React.createElement(DesktopRoomContextView, {
+            roomData: this.props.roomData, 
+            show: this.props.showContext})
+        )
+      );
+    }
+  });
+
+  var DesktopRoomContextView = React.createClass({displayName: "DesktopRoomContextView",
+    propTypes: {
+      // This data is supplied by the activeRoomStore.
+      roomData: React.PropTypes.object.isRequired,
+      show: React.PropTypes.bool.isRequired
+    },
+
+    componentWillReceiveProps: function(nextProps) {
+      // When the 'show' prop is changed from outside this component, we do need
+      // to update the state.
+      if (("show" in nextProps) && nextProps.show !== this.props.show) {
+        this.setState({ show: nextProps.show });
+      }
+    },
+
+    getInitialState: function() {
+      return { show: this.props.show };
+    },
+
+    handleCloseClick: function() {
+      this.setState({ show: false });
+    },
+
+    render: function() {
+      if (!this.state.show)
+        return null;
+
+      var URL = this.props.roomData.roomContextUrls && this.props.roomData.roomContextUrls[0];
+      var thumbnail = URL && URL.thumbnail || "";
+      var URLDescription = URL && URL.description || "";
+      var location = URL && URL.location || "";
+      return (
+        React.createElement("div", {className: "room-context"}, 
+          React.createElement("img", {className: "room-context-thumbnail", src: thumbnail}), 
+          React.createElement("div", {className: "room-context-content"}, 
+            React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")), 
+            React.createElement("div", {className: "room-context-description"}, URLDescription), 
+            React.createElement("a", {className: "room-context-url", href: location, target: "_blank"}, location), 
+            this.props.roomData.roomDescription ?
+              React.createElement("div", {className: "room-context-comment"}, this.props.roomData.roomDescription) :
+              null, 
+            React.createElement("button", {className: "room-context-btn-close", 
+                    onClick: this.handleCloseClick})
+          )
         )
       );
     }
   });
 
   /**
    * Desktop room conversation view.
    */
@@ -280,28 +335,18 @@ loop.roomViews = (function(mozL10n) {
       ActiveRoomStoreMixin,
       sharedMixins.DocumentTitleMixin,
       sharedMixins.MediaSetupMixin,
       sharedMixins.RoomsAudioMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
-    },
-
-    _renderInvitationOverlay: function() {
-      if (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS) {
-        return (
-          React.createElement(DesktopRoomInvitationView, {
-            roomStore: this.props.roomStore, 
-            dispatcher: this.props.dispatcher})
-        );
-      }
-      return null;
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      mozLoop: React.PropTypes.object.isRequired
     },
 
     componentWillUpdate: function(nextProps, nextState) {
       // The SDK needs to know about the configuration and the elements to use
       // for display. So the best way seems to pass the information here - ideally
       // the sdk wouldn't need to know this, but we can't change that.
       if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
           nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
@@ -336,16 +381,27 @@ loop.roomViews = (function(mozL10n) {
     publishStream: function(type, enabled) {
       this.props.dispatcher.dispatch(
         new sharedActions.SetMute({
           type: type,
           enabled: enabled
         }));
     },
 
+    _shouldRenderInvitationOverlay: function() {
+      return (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS);
+    },
+
+    _shouldRenderContextView: function() {
+      return !!(
+        this.props.mozLoop.getLoopPref("contextInConversations.enabled") &&
+        (this.state.roomContextUrls || this.state.roomDescription)
+      );
+    },
+
     render: function() {
       if (this.state.roomName) {
         this.setTitle(this.state.roomName);
       }
 
       var localStreamClasses = React.addons.classSet({
         local: true,
         "local-stream": true,
@@ -353,16 +409,20 @@ loop.roomViews = (function(mozL10n) {
         "room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS
       });
 
       var screenShareData = {
         state: this.state.screenSharingState,
         visible: true
       };
 
+      var shouldRenderInvitationOverlay = this._shouldRenderInvitationOverlay();
+      var shouldRenderContextView = this._shouldRenderContextView();
+      var roomData = this.props.roomStore.getStoreState("activeRoom");
+
       switch(this.state.roomState) {
         case ROOM_STATES.FAILED:
         case ROOM_STATES.FULL: {
           // Note: While rooms are set to hold a maximum of 2 participants, the
           //       FULL case should never happen on desktop.
           return (
             React.createElement(loop.conversationViews.GenericFailureView, {
               cancelCall: this.closeWindow, 
@@ -373,17 +433,22 @@ loop.roomViews = (function(mozL10n) {
           return (
             React.createElement(sharedViews.FeedbackView, {
               onAfterFeedbackReceived: this.closeWindow})
           );
         }
         default: {
           return (
             React.createElement("div", {className: "room-conversation-wrapper"}, 
-              this._renderInvitationOverlay(), 
+              React.createElement(DesktopRoomInvitationView, {
+                dispatcher: this.props.dispatcher, 
+                error: this.state.error, 
+                roomData: roomData, 
+                show: shouldRenderInvitationOverlay, 
+                showContext: shouldRenderContextView}), 
               React.createElement("div", {className: "video-layout-wrapper"}, 
                 React.createElement("div", {className: "conversation room-conversation"}, 
                   React.createElement("div", {className: "media nested"}, 
                     React.createElement("div", {className: "video_wrapper remote_wrapper"}, 
                       React.createElement("div", {className: "video_inner remote focus-stream"})
                     ), 
                     React.createElement("div", {className: localStreamClasses}), 
                     React.createElement("div", {className: "screen hide"})
@@ -391,24 +456,28 @@ loop.roomViews = (function(mozL10n) {
                   React.createElement(sharedViews.ConversationToolbar, {
                     dispatcher: this.props.dispatcher, 
                     video: {enabled: !this.state.videoMuted, visible: true}, 
                     audio: {enabled: !this.state.audioMuted, visible: true}, 
                     publishStream: this.publishStream, 
                     hangup: this.leaveRoom, 
                     screenShare: screenShareData})
                 )
-              )
+              ), 
+              React.createElement(DesktopRoomContextView, {
+                roomData: roomData, 
+                show: !shouldRenderInvitationOverlay && shouldRenderContextView})
             )
           );
         }
       }
     }
   });
 
   return {
     ActiveRoomStoreMixin: ActiveRoomStoreMixin,
     SocialShareDropdown: SocialShareDropdown,
+    DesktopRoomContextView: DesktopRoomContextView,
     DesktopRoomConversationView: DesktopRoomConversationView,
     DesktopRoomInvitationView: DesktopRoomInvitationView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -26,46 +26,58 @@ loop.roomViews = (function(mozL10n) {
 
     propTypes: {
       roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
 
     componentWillMount: function() {
       this.listenTo(this.props.roomStore, "change:activeRoom",
                     this._onActiveRoomStateChanged);
+      this.listenTo(this.props.roomStore, "change:error",
+                    this._onRoomError);
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.props.roomStore);
     },
 
     _onActiveRoomStateChanged: function() {
       // Only update the state if we're mounted, to avoid the problem where
       // stopListening doesn't nuke the active listeners during a event
       // processing.
       if (this.isMounted()) {
         this.setState(this.props.roomStore.getStoreState("activeRoom"));
       }
     },
 
+    _onRoomError: function() {
+      // Only update the state if we're mounted, to avoid the problem where
+      // stopListening doesn't nuke the active listeners during a event
+      // processing.
+      if (this.isMounted()) {
+        this.setState({error: this.props.roomStore.getStoreState("error")});
+      }
+    },
+
     getInitialState: function() {
       var storeState = this.props.roomStore.getStoreState("activeRoom");
       return _.extend({
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState
       }, storeState);
     }
   };
 
   var SocialShareDropdown = React.createClass({
-    mixins: [ActiveRoomStoreMixin],
-
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      show: React.PropTypes.bool.isRequired
+      roomUrl: React.PropTypes.string,
+      show: React.PropTypes.bool.isRequired,
+      socialShareButtonAvailable: React.PropTypes.bool,
+      socialShareProviders: React.PropTypes.array
     },
 
     handleToolbarAddButtonClick: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(new sharedActions.AddSocialShareButton());
     },
 
@@ -74,44 +86,44 @@ loop.roomViews = (function(mozL10n) {
 
       this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());
     },
 
     handleProviderClick: function(event) {
       event.preventDefault();
 
       var origin = event.currentTarget.dataset.provider;
-      var provider = this.state.socialShareProviders.filter(function(provider) {
+      var provider = this.props.socialShareProviders.filter(function(provider) {
         return provider.origin == origin;
       })[0];
 
       this.props.dispatcher.dispatch(new sharedActions.ShareRoomUrl({
         provider: provider,
-        roomUrl: this.state.roomUrl,
+        roomUrl: this.props.roomUrl,
         previews: []
       }));
     },
 
     render: function() {
       // Don't render a thing when no data has been fetched yet.
-      if (!this.state.socialShareProviders) {
+      if (!this.props.socialShareProviders) {
         return null;
       }
 
       var cx = React.addons.classSet;
       var shareDropdown = cx({
         "share-service-dropdown": true,
         "dropdown-menu": true,
-        "share-button-unavailable": !this.state.socialShareButtonAvailable,
+        "share-button-unavailable": !this.props.socialShareButtonAvailable,
         "hide": !this.props.show
       });
 
       // When the button is not yet available, we offer to put it in the navbar
       // for the user.
-      if (!this.state.socialShareButtonAvailable) {
+      if (!this.props.socialShareButtonAvailable) {
         return (
           <div className={shareDropdown}>
             <div className="share-panel-header">
               {mozL10n.get("share_panel_header")}
             </div>
             <div className="share-panel-body">
               {
                 mozL10n.get("share_panel_body", {
@@ -129,19 +141,19 @@ loop.roomViews = (function(mozL10n) {
       }
 
       return (
         <ul className={shareDropdown}>
           <li className="dropdown-menu-item" onClick={this.handleAddServiceClick}>
             <i className="icon icon-add-share-service"></i>
             <span>{mozL10n.get("share_add_service_button")}</span>
           </li>
-          {this.state.socialShareProviders.length ? <li className="dropdown-menu-separator"/> : null}
+          {this.props.socialShareProviders.length ? <li className="dropdown-menu-separator"/> : null}
           {
-            this.state.socialShareProviders.map(function(provider, idx) {
+            this.props.socialShareProviders.map(function(provider, idx) {
               return (
                 <li className="dropdown-menu-item"
                     key={"provider-" + idx}
                     data-provider={provider.origin}
                     onClick={this.handleProviderClick}>
                   <img className="icon" src={provider.iconURL}/>
                   <span>{provider.name}</span>
                 </li>
@@ -152,126 +164,169 @@ loop.roomViews = (function(mozL10n) {
       );
     }
   });
 
   /**
    * Desktop room invitation view (overlay).
    */
   var DesktopRoomInvitationView = React.createClass({
-    mixins: [ActiveRoomStoreMixin, React.addons.LinkedStateMixin,
-             sharedMixins.DropdownMenuMixin],
+    mixins: [React.addons.LinkedStateMixin, sharedMixins.DropdownMenuMixin],
 
     propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      error: React.PropTypes.object,
+      // This data is supplied by the activeRoomStore.
+      roomData: React.PropTypes.object.isRequired,
+      show: React.PropTypes.bool.isRequired,
+      showContext: React.PropTypes.bool.isRequired
     },
 
     getInitialState: function() {
       return {
         copiedUrl: false,
-        newRoomName: "",
-        error: null,
+        newRoomName: ""
       };
     },
 
-    componentWillMount: function() {
-      this.listenTo(this.props.roomStore, "change:error",
-                    this.onRoomError);
-    },
-
-    componentWillUnmount: function() {
-      this.stopListening(this.props.roomStore);
-    },
-
     handleTextareaKeyDown: function(event) {
       // Submit the form as soon as the user press Enter in that field
       // Note: We're using a textarea instead of a simple text input to display
       // placeholder and entered text on two lines, to circumvent l10n
       // rendering/UX issues for some locales.
       if (event.which === 13) {
         this.handleFormSubmit(event);
       }
     },
 
     handleFormSubmit: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(new sharedActions.RenameRoom({
-        roomToken: this.state.roomToken,
+        roomToken: this.props.roomData.roomToken,
         newRoomName: this.state.newRoomName
       }));
     },
 
     handleEmailButtonClick: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(
-        new sharedActions.EmailRoomUrl({roomUrl: this.state.roomUrl}));
+        new sharedActions.EmailRoomUrl({roomUrl: this.props.roomData.roomUrl}));
     },
 
     handleCopyButtonClick: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(
-        new sharedActions.CopyRoomUrl({roomUrl: this.state.roomUrl}));
+        new sharedActions.CopyRoomUrl({roomUrl: this.props.roomData.roomUrl}));
 
       this.setState({copiedUrl: true});
     },
 
     handleShareButtonClick: function(event) {
       event.preventDefault();
 
       this.toggleDropdownMenu();
     },
 
-    onRoomError: function() {
-      // Only update the state if we're mounted, to avoid the problem where
-      // stopListening doesn't nuke the active listeners during a event
-      // processing.
-      if (this.isMounted()) {
-        this.setState({error: this.props.roomStore.getStoreState("error")});
+    render: function() {
+      if (!this.props.show) {
+        return null;
       }
-    },
 
-    render: function() {
       var cx = React.addons.classSet;
       return (
         <div className="room-invitation-overlay">
-          <p className={cx({"error": !!this.state.error,
-                            "error-display-area": true})}>
-            {mozL10n.get("rooms_name_change_failed_label")}
-          </p>
-          <form onSubmit={this.handleFormSubmit}>
-            <textarea rows="2" type="text" className="input-room-name"
-              valueLink={this.linkState("newRoomName")}
-              onBlur={this.handleFormSubmit}
-              onKeyDown={this.handleTextareaKeyDown}
-              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("email_link_button")}
-            </button>
-            <button className="btn btn-info btn-copy"
-                    onClick={this.handleCopyButtonClick}>
-              {this.state.copiedUrl ? mozL10n.get("copied_url_button") :
-                                      mozL10n.get("copy_url_button2")}
-            </button>
-            <button className="btn btn-info btn-share"
-                    ref="anchor"
-                    onClick={this.handleShareButtonClick}>
-              {mozL10n.get("share_button3")}
-            </button>
+          <div className="room-invitation-content">
+            <p className={cx({"error": !!this.props.error,
+                              "error-display-area": true})}>
+              {mozL10n.get("rooms_name_change_failed_label")}
+            </p>
+            <form onSubmit={this.handleFormSubmit}>
+              <textarea rows="2" type="text" className="input-room-name"
+                valueLink={this.linkState("newRoomName")}
+                onBlur={this.handleFormSubmit}
+                onKeyDown={this.handleTextareaKeyDown}
+                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("email_link_button")}
+              </button>
+              <button className="btn btn-info btn-copy"
+                      onClick={this.handleCopyButtonClick}>
+                {this.state.copiedUrl ? mozL10n.get("copied_url_button") :
+                                        mozL10n.get("copy_url_button2")}
+              </button>
+              <button className="btn btn-info btn-share"
+                      ref="anchor"
+                      onClick={this.handleShareButtonClick}>
+                {mozL10n.get("share_button3")}
+              </button>
+            </div>
+            <SocialShareDropdown dispatcher={this.props.dispatcher}
+                                 roomUrl={this.props.roomData.roomUrl}
+                                 show={this.state.showMenu}
+                                 ref="menu"/>
           </div>
-          <SocialShareDropdown dispatcher={this.props.dispatcher}
-                               roomStore={this.props.roomStore}
-                               show={this.state.showMenu}
-                               ref="menu"/>
+          <DesktopRoomContextView
+            roomData={this.props.roomData}
+            show={this.props.showContext} />
+        </div>
+      );
+    }
+  });
+
+  var DesktopRoomContextView = React.createClass({
+    propTypes: {
+      // This data is supplied by the activeRoomStore.
+      roomData: React.PropTypes.object.isRequired,
+      show: React.PropTypes.bool.isRequired
+    },
+
+    componentWillReceiveProps: function(nextProps) {
+      // When the 'show' prop is changed from outside this component, we do need
+      // to update the state.
+      if (("show" in nextProps) && nextProps.show !== this.props.show) {
+        this.setState({ show: nextProps.show });
+      }
+    },
+
+    getInitialState: function() {
+      return { show: this.props.show };
+    },
+
+    handleCloseClick: function() {
+      this.setState({ show: false });
+    },
+
+    render: function() {
+      if (!this.state.show)
+        return null;
+
+      var URL = this.props.roomData.roomContextUrls && this.props.roomData.roomContextUrls[0];
+      var thumbnail = URL && URL.thumbnail || "";
+      var URLDescription = URL && URL.description || "";
+      var location = URL && URL.location || "";
+      return (
+        <div className="room-context">
+          <img className="room-context-thumbnail" src={thumbnail}/>
+          <div className="room-context-content">
+            <div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
+            <div className="room-context-description">{URLDescription}</div>
+            <a className="room-context-url" href={location} target="_blank">{location}</a>
+            {this.props.roomData.roomDescription ?
+              <div className="room-context-comment">{this.props.roomData.roomDescription}</div> :
+              null}
+            <button className="room-context-btn-close"
+                    onClick={this.handleCloseClick}/>
+          </div>
         </div>
       );
     }
   });
 
   /**
    * Desktop room conversation view.
    */
@@ -280,28 +335,18 @@ loop.roomViews = (function(mozL10n) {
       ActiveRoomStoreMixin,
       sharedMixins.DocumentTitleMixin,
       sharedMixins.MediaSetupMixin,
       sharedMixins.RoomsAudioMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
-    },
-
-    _renderInvitationOverlay: function() {
-      if (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS) {
-        return (
-          <DesktopRoomInvitationView
-            roomStore={this.props.roomStore}
-            dispatcher={this.props.dispatcher} />
-        );
-      }
-      return null;
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      mozLoop: React.PropTypes.object.isRequired
     },
 
     componentWillUpdate: function(nextProps, nextState) {
       // The SDK needs to know about the configuration and the elements to use
       // for display. So the best way seems to pass the information here - ideally
       // the sdk wouldn't need to know this, but we can't change that.
       if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
           nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
@@ -336,16 +381,27 @@ loop.roomViews = (function(mozL10n) {
     publishStream: function(type, enabled) {
       this.props.dispatcher.dispatch(
         new sharedActions.SetMute({
           type: type,
           enabled: enabled
         }));
     },
 
+    _shouldRenderInvitationOverlay: function() {
+      return (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS);
+    },
+
+    _shouldRenderContextView: function() {
+      return !!(
+        this.props.mozLoop.getLoopPref("contextInConversations.enabled") &&
+        (this.state.roomContextUrls || this.state.roomDescription)
+      );
+    },
+
     render: function() {
       if (this.state.roomName) {
         this.setTitle(this.state.roomName);
       }
 
       var localStreamClasses = React.addons.classSet({
         local: true,
         "local-stream": true,
@@ -353,16 +409,20 @@ loop.roomViews = (function(mozL10n) {
         "room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS
       });
 
       var screenShareData = {
         state: this.state.screenSharingState,
         visible: true
       };
 
+      var shouldRenderInvitationOverlay = this._shouldRenderInvitationOverlay();
+      var shouldRenderContextView = this._shouldRenderContextView();
+      var roomData = this.props.roomStore.getStoreState("activeRoom");
+
       switch(this.state.roomState) {
         case ROOM_STATES.FAILED:
         case ROOM_STATES.FULL: {
           // Note: While rooms are set to hold a maximum of 2 participants, the
           //       FULL case should never happen on desktop.
           return (
             <loop.conversationViews.GenericFailureView
               cancelCall={this.closeWindow}
@@ -373,17 +433,22 @@ loop.roomViews = (function(mozL10n) {
           return (
             <sharedViews.FeedbackView
               onAfterFeedbackReceived={this.closeWindow} />
           );
         }
         default: {
           return (
             <div className="room-conversation-wrapper">
-              {this._renderInvitationOverlay()}
+              <DesktopRoomInvitationView
+                dispatcher={this.props.dispatcher}
+                error={this.state.error}
+                roomData={roomData}
+                show={shouldRenderInvitationOverlay}
+                showContext={shouldRenderContextView} />
               <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 focus-stream"></div>
                     </div>
                     <div className={localStreamClasses}></div>
                     <div className="screen hide"></div>
@@ -392,23 +457,27 @@ loop.roomViews = (function(mozL10n) {
                     dispatcher={this.props.dispatcher}
                     video={{enabled: !this.state.videoMuted, visible: true}}
                     audio={{enabled: !this.state.audioMuted, visible: true}}
                     publishStream={this.publishStream}
                     hangup={this.leaveRoom}
                     screenShare={screenShareData} />
                 </div>
               </div>
+              <DesktopRoomContextView
+                roomData={roomData}
+                show={!shouldRenderInvitationOverlay && shouldRenderContextView} />
             </div>
           );
         }
       }
     }
   });
 
   return {
     ActiveRoomStoreMixin: ActiveRoomStoreMixin,
     SocialShareDropdown: SocialShareDropdown,
+    DesktopRoomContextView: DesktopRoomContextView,
     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
@@ -848,22 +848,35 @@ html, .fx-embedded, #main,
   background-image: url("../img/icons-16x16.svg#leave");
 }
 
 .room-invitation-overlay {
   position: absolute;
   background: rgba(0,0,0,.6);
   /* This matches .fx-embedded .conversation toolbar height */
   top: 26px;
+  height: calc(100% - 26px);
   right: 0;
-  bottom: 0;
   left: 0;
   text-align: center;
   color: #fff;
   z-index: 1010;
+  display: flex;
+  flex-flow: column nowrap;
+  justify-content: flex-start;
+  align-items: stretch;
+}
+
+.room-invitation-content {
+  order: 1;
+  flex: 1 1 auto;
+  display: flex;
+  flex-flow: column nowrap;
+  justify-content: center;
+  align-items: center;
 }
 
 .room-invitation-overlay .error-display-area.error,
 .room-invitation-overlay input[type="text"] {
   display: block;
   background-color: rgba(0,0,0,.5);
   border-radius: 3px;
   padding: .5em;
@@ -878,38 +891,33 @@ html, .fx-embedded, #main,
   top: 2em;
   left: 1em;
   right: 1em;
   text-align: start;
   width: calc(258px - 2em);
   color: #d74345;
 }
 
-.room-invitation-overlay form {
-  padding: 6em 0 2em 0;
+.room-invitation-overlay .btn-group {
+  padding: 0;
 }
 
 .room-invitation-overlay textarea {
   display: block;
   background: rgba(0, 0, 0, .5);
   color: #fff;
   font-family: "Helvetica Neue", Arial, sans;
   font-size: 1.2em;
   border: none;
   width: 200px;
   margin: 0 auto;
   padding: .2em .4em;
   border-radius: .5em;
 }
 
-.room-invitation-overlay .btn-group {
-  position: absolute;
-  bottom: 10px;
-}
-
 .share-service-dropdown {
   color: #000;
   text-align: start;
   bottom: auto;
   top: 0;
 }
 
 .share-service-dropdown.share-button-unavailable {
@@ -957,16 +965,122 @@ body[dir=rtl] .share-service-dropdown .s
 .dropdown-menu-item:hover > .icon-add-share-service {
   background-image: url("../img/icons-16x16.svg#add-hover");
 }
 
 .dropdown-menu-item:hover:active > .icon-add-share-service {
   background-image: url("../img/icons-16x16.svg#add-active");
 }
 
+.room-context {
+  background: rgba(0,0,0,.6);
+  border-top: 2px solid #444;
+  border-bottom: 2px solid #444;
+  padding: .5rem;
+  max-height: 120px;
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  width: 100%;
+  font-size: .9em;
+  display: flex;
+  flex-flow: row nowrap;
+  align-content: flex-start;
+  align-items: flex-start;
+}
+
+.room-invitation-overlay .room-context {
+  position: relative;
+  left: auto;
+  bottom: auto;
+  order: 2;
+  flex: 0 1 auto;
+}
+
+.room-context-thumbnail {
+  width: 100px;
+  max-height: 200px;
+  -moz-margin-end: 1ch;
+  margin-bottom: 1em;
+  order: 1;
+  flex: 0 1 auto;
+}
+
+body[dir=rtl] .room-context-thumbnail {
+  order: 2;
+}
+
+.room-context-thumbnail[src=""] {
+  display: none;
+}
+
+.room-context-content {
+  order: 2;
+  flex: 1 1 auto;
+  text-align: start;
+}
+
+body[dir=rtl] .room-context-content {
+  order: 1;
+}
+
+.room-context-label {
+  margin-bottom: 1em;
+}
+
+.room-context-label,
+.room-context-description {
+  color: #fff;
+}
+
+.room-context-comment {
+  color: #707070;
+}
+
+.room-context-description,
+.room-context-comment {
+  word-wrap: break-word;
+}
+
+.room-context-url {
+  color: #59A1D7;
+  font-style: italic;
+  text-decoration: none;
+  margin-bottom: 1em;
+}
+
+.room-context-url:hover {
+  text-decoration: underline;
+}
+
+.room-context-btn-close {
+  position: absolute;
+  right: 5px;
+  top: 5px;
+  width: 8px;
+  height: 8px;
+  background-color: transparent;
+  background-image: url("../img/icons-10x10.svg#close");
+  background-size: 8px 8px;
+  background-repeat: no-repeat;
+  border: 0;
+  padding: 0;
+  cursor: pointer;
+}
+
+.room-context-btn-close:hover,
+.room-context-btn-close:hover:active {
+  background-image: url("../img/icons-10x10.svg#close-active");
+}
+
+body[dir=rtl] .room-context-btn-close {
+  right: auto;
+  left: 5px;
+}
+
 /* Standalone rooms */
 
 .standalone .room-conversation-wrapper {
   position: relative;
   height: 100%;
   background: #000;
 }
 
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -396,32 +396,34 @@ loop.shared.actions = (function() {
 
     /**
      * Sets up the room information when it is received.
      * XXX: should move to some roomActions module - refs bug 1079284
      *
      * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
      */
     SetupRoomInfo: Action.define("setupRoomInfo", {
+      // roomContextUrls: Array - Optional.
+      // roomDescription: String - Optional.
       // roomName: String - Optional.
       roomOwner: String,
       roomToken: String,
       roomUrl: String,
       socialShareButtonAvailable: Boolean,
       socialShareProviders: Array
     }),
 
     /**
      * Updates the room information when it is received.
      * XXX: should move to some roomActions module - refs bug 1079284
      *
      * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
      */
     UpdateRoomInfo: Action.define("updateRoomInfo", {
-      // context: Object - Optional.
+      // description: String - Optional.
       // roomName: String - Optional.
       roomOwner: String,
       roomUrl: String
       // urls: Array - Optional.
       // See https://wiki.mozilla.org/Loop/Architecture/Context#Format_of_context.value
     }),
 
     /**
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -18,16 +18,23 @@ loop.store.ActiveRoomStore = (function()
   // Error numbers taken from
   // https://github.com/mozilla-services/loop-server/blob/master/loop/errno.json
   var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
 
   var ROOM_STATES = loop.store.ROOM_STATES;
 
   var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
 
+  var OPTIONAL_ROOMINFO_FIELDS = {
+    urls: "roomContextUrls",
+    description: "roomDescription",
+    roomInfoFailure: "roomInfoFailure",
+    roomName: "roomName"
+  };
+
   /**
    * Active room store.
    *
    * @param {loop.Dispatcher} dispatcher  The dispatcher for dispatching actions
    *                                      and registering to consume actions.
    * @param {Object} options Options object:
    * - {mozLoop}     mozLoop    The MozLoop API object.
    * - {OTSdkDriver} sdkDriver  The SDK driver instance.
@@ -79,16 +86,18 @@ loop.store.ActiveRoomStore = (function()
         localVideoDimensions: {},
         remoteVideoDimensions: {},
         screenSharingState: SCREEN_SHARE_STATES.INACTIVE,
         receivingScreenShare: false,
         // Any urls (aka context) associated with the room.
         roomContextUrls: null,
         // The roomCryptoKey to decode the context data if necessary.
         roomCryptoKey: null,
+        // The description for a room as stored in the context data.
+        roomDescription: null,
         // Room information failed to be obtained for a reason. See ROOM_INFO_FAILURES.
         roomInfoFailure: null,
         // The name of the room.
         roomName: null,
         // Social API state.
         socialShareButtonAvailable: false,
         socialShareProviders: null
       };
@@ -180,16 +189,18 @@ loop.store.ActiveRoomStore = (function()
               error: error,
               failedJoinRequest: false
             }));
             return;
           }
 
           this.dispatchAction(new sharedActions.SetupRoomInfo({
             roomToken: actionData.roomToken,
+            roomContextUrls: roomData.decryptedContext.urls,
+            roomDescription: roomData.decryptedContext.description,
             roomName: roomData.decryptedContext.roomName,
             roomOwner: roomData.roomOwner,
             roomUrl: roomData.roomUrl,
             socialShareButtonAvailable: this._mozLoop.isSocialShareButtonAvailable(),
             socialShareProviders: this._mozLoop.getSocialShareProviders()
           }));
 
           // For the conversation window, we need to automatically
@@ -270,16 +281,17 @@ loop.store.ActiveRoomStore = (function()
         }
 
         var dispatcher = this.dispatcher;
 
         crypto.decryptBytes(roomCryptoKey, result.context.value)
               .then(function(decryptedResult) {
           var realResult = JSON.parse(decryptedResult);
 
+          roomInfoData.description = realResult.description;
           roomInfoData.urls = realResult.urls;
           roomInfoData.roomName = realResult.roomName;
 
           dispatcher.dispatch(roomInfoData);
         }, function(err) {
           roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.DECRYPT_FAILED;
           dispatcher.dispatch(roomInfoData);
         });
@@ -294,16 +306,18 @@ loop.store.ActiveRoomStore = (function()
      */
     setupRoomInfo: function(actionData) {
       if (this._onUpdateListener) {
         console.error("Room info already set up!");
         return;
       }
 
       this.setStoreState({
+        roomContextUrls: actionData.roomContextUrls,
+        roomDescription: actionData.roomDescription,
         roomName: actionData.roomName,
         roomOwner: actionData.roomOwner,
         roomState: ROOM_STATES.READY,
         roomToken: actionData.roomToken,
         roomUrl: actionData.roomUrl,
         socialShareButtonAvailable: actionData.socialShareButtonAvailable,
         socialShareProviders: actionData.socialShareProviders
       });
@@ -319,23 +333,28 @@ loop.store.ActiveRoomStore = (function()
     },
 
     /**
      * Handles the updateRoomInfo action. Updates the room data.
      *
      * @param {sharedActions.UpdateRoomInfo} actionData
      */
     updateRoomInfo: function(actionData) {
-      this.setStoreState({
-        roomContextUrls: actionData.urls,
-        roomInfoFailure: actionData.roomInfoFailure,
-        roomName: actionData.roomName,
+      var newState = {
         roomOwner: actionData.roomOwner,
         roomUrl: actionData.roomUrl
+      };
+      // Iterate over the optional fields that _may_ be present on the actionData
+      // object.
+      Object.keys(OPTIONAL_ROOMINFO_FIELDS).forEach(function(field) {
+        if (actionData[field]) {
+          newState[OPTIONAL_ROOMINFO_FIELDS[field]] = actionData[field];
+        }
       });
+      this.setStoreState(newState);
     },
 
     /**
      * Handles the updateSocialShareInfo action. Updates the room data with new
      * Social API info.
      *
      * @param  {sharedActions.UpdateSocialShareInfo} actionData
      */