Bug 1162909: update the context views inside the Hello conversation window with latest UX updates. Make saving context data always work and more resilient to failure. r=Standard8
authorMike de Boer <mdeboer@mozilla.com>
Thu, 28 May 2015 11:04:42 +0200
changeset 265528 df0e99b0e7e59b046083d0990967551b3d39113f
parent 265527 9905af238c6c6bee2b2b11919a401136470d76fa
child 265529 9bd7e886acd38c872ecaeb600dab8561a1adc1c4
push id8157
push userjlund@mozilla.com
push dateMon, 29 Jun 2015 20:36:23 +0000
treeherdermozilla-aurora@d480e05bd276 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1162909
milestone41.0a1
Bug 1162909: update the context views inside the Hello conversation window with latest UX updates. Make saving context data always work and more resilient to failure. r=Standard8
browser/components/loop/content/js/conversation.js
browser/components/loop/content/js/conversation.jsx
browser/components/loop/content/js/roomStore.js
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/img/icons-10x10.svg
browser/components/loop/content/shared/js/actions.js
browser/components/loop/content/shared/js/views.js
browser/components/loop/content/shared/js/views.jsx
browser/components/loop/test/desktop-local/roomStore_test.js
browser/components/loop/test/desktop-local/roomViews_test.js
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -158,17 +158,17 @@ loop.conversation = (function(mozL10n) {
       dispatcher.dispatch(new sharedActions.WindowUnload());
     });
 
     React.render(React.createElement(AppControllerView, {
       roomStore: roomStore, 
       dispatcher: dispatcher, 
       mozLoop: navigator.mozLoop}), document.querySelector("#main"));
 
-    document.body.setAttribute("dir", mozL10n.getDirection());
+    document.body.setAttribute("dir", "rtl");//mozL10n.getDirection());
     document.body.setAttribute("platform", loop.shared.utils.getPlatform());
 
     dispatcher.dispatch(new sharedActions.GetWindowData({
       windowId: windowId
     }));
   }
 
   return {
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -158,17 +158,17 @@ loop.conversation = (function(mozL10n) {
       dispatcher.dispatch(new sharedActions.WindowUnload());
     });
 
     React.render(<AppControllerView
       roomStore={roomStore}
       dispatcher={dispatcher}
       mozLoop={navigator.mozLoop} />, document.querySelector("#main"));
 
-    document.body.setAttribute("dir", mozL10n.getDirection());
+    document.body.setAttribute("dir", "rtl");//mozL10n.getDirection());
     document.body.setAttribute("platform", loop.shared.utils.getPlatform());
 
     dispatcher.dispatch(new sharedActions.GetWindowData({
       windowId: windowId
     }));
   }
 
   return {
--- a/browser/components/loop/content/js/roomStore.js
+++ b/browser/components/loop/content/js/roomStore.js
@@ -86,16 +86,17 @@ loop.store = loop.store || {};
       "deleteRoom",
       "deleteRoomError",
       "emailRoomUrl",
       "getAllRooms",
       "getAllRoomsError",
       "openRoom",
       "shareRoomUrl",
       "updateRoomContext",
+      "updateRoomContextDone",
       "updateRoomContextError",
       "updateRoomList"
     ],
 
     initialize: function(options) {
       if (!options.mozLoop) {
         throw new Error("Missing option mozLoop");
       }
@@ -110,17 +111,18 @@ loop.store = loop.store || {};
     },
 
     getInitialStoreState: function() {
       return {
         activeRoom: this.activeRoomStore ? this.activeRoomStore.getStoreState() : {},
         error: null,
         pendingCreation: false,
         pendingInitialRetrieval: false,
-        rooms: []
+        rooms: [],
+        savingContext: false
       };
     },
 
     /**
      * Registers mozLoop.rooms events.
      */
     startListeningToRoomEvents: function() {
       // Rooms event registration
@@ -468,16 +470,17 @@ loop.store = loop.store || {};
     },
 
     /**
      * Updates the context data attached to a room.
      *
      * @param {sharedActions.UpdateRoomContext} actionData
      */
     updateRoomContext: function(actionData) {
+      this.setStoreState({ savingContext: true });
       this._mozLoop.rooms.get(actionData.roomToken, function(err, room) {
         if (err) {
           this.dispatchAction(new sharedActions.UpdateRoomContextError({
             error: err
           }));
           return;
         }
 
@@ -515,33 +518,43 @@ loop.store = loop.store || {};
         }
         // TODO: there currently is no clear UX defined on what to do when all
         // context data was cleared, e.g. when diff.removed contains all the
         // context properties. Until then, we can't deal with context removal here.
 
         // When no properties have been set on the roomData object, there's nothing
         // to save.
         if (!Object.getOwnPropertyNames(roomData).length) {
+          this.dispatchAction(new sharedActions.UpdateRoomContextDone());
           return;
         }
 
         this.setStoreState({error: null});
         this._mozLoop.rooms.update(actionData.roomToken, roomData,
           function(err, data) {
-            if (err) {
-              this.dispatchAction(new sharedActions.UpdateRoomContextError({
-                error: err
-              }));
-            }
+            var action = err ?
+              new sharedActions.UpdateRoomContextError({ error: err }) :
+              new sharedActions.UpdateRoomContextDone();
+            this.dispatchAction(action);
           }.bind(this));
       }.bind(this));
     },
 
     /**
+     * Handles the updateRoomContextDone action.
+     */
+    updateRoomContextDone: function() {
+      this.setStoreState({ savingContext: false });
+    },
+
+    /**
      * Updating the context data attached to a room error.
      *
      * @param {sharedActions.UpdateRoomContextError} actionData
      */
     updateRoomContextError: function(actionData) {
-      this.setStoreState({error: actionData.error});
+      this.setStoreState({
+        error: actionData.error,
+        savingContext: false
+      });
     }
   });
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -24,16 +24,18 @@ loop.roomViews = (function(mozL10n) {
       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);
+      this.listenTo(this.props.roomStore, "change:savingContext",
+                    this._onRoomSavingContext);
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.props.roomStore);
     },
 
     _onActiveRoomStateChanged: function() {
       // Only update the state if we're mounted, to avoid the problem where
@@ -48,21 +50,31 @@ loop.roomViews = (function(mozL10n) {
       // 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")});
       }
     },
 
+    _onRoomSavingContext: 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({savingContext: this.props.roomStore.getStoreState("savingContext")});
+      }
+    },
+
     getInitialState: function() {
       var storeState = this.props.roomStore.getStoreState("activeRoom");
       return _.extend({
         // Used by the UI showcase.
-        roomState: this.props.roomState || storeState.roomState
+        roomState: this.props.roomState || storeState.roomState,
+        savingContext: false
       }, storeState);
     }
   };
 
   var SocialShareDropdown = React.createClass({displayName: "SocialShareDropdown",
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       roomUrl: React.PropTypes.string,
@@ -169,16 +181,17 @@ loop.roomViews = (function(mozL10n) {
     mixins: [sharedMixins.DropdownMenuMixin(".room-invitation-overlay")],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       error: React.PropTypes.object,
       mozLoop: React.PropTypes.object.isRequired,
       // This data is supplied by the activeRoomStore.
       roomData: React.PropTypes.object.isRequired,
+      savingContext: React.PropTypes.bool,
       show: React.PropTypes.bool.isRequired,
       showContext: React.PropTypes.bool.isRequired
     },
 
     getInitialState: function() {
       return {
         copiedUrl: false,
         editMode: false,
@@ -238,17 +251,21 @@ loop.roomViews = (function(mozL10n) {
             React.createElement("p", {className: cx({hide: this.state.editMode})}, 
               mozL10n.get("invite_header_text")
             ), 
             React.createElement("a", {className: cx({hide: !canAddContext, "room-invitation-addcontext": true}), 
                onClick: this.handleAddContextClick}, 
               mozL10n.get("context_add_some_label")
             )
           ), 
-          React.createElement("div", {className: "btn-group call-action-group"}, 
+          React.createElement("div", {className: cx({
+            "btn-group": true,
+            "call-action-group": true,
+            hide: this.state.editMode
+          })}, 
             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")
@@ -265,16 +282,17 @@ loop.roomViews = (function(mozL10n) {
             show: this.state.showMenu, 
             socialShareButtonAvailable: this.props.socialShareButtonAvailable, 
             socialShareProviders: this.props.socialShareProviders, 
             ref: "menu"}), 
           React.createElement(DesktopRoomContextView, {
             dispatcher: this.props.dispatcher, 
             editMode: this.state.editMode, 
             error: this.props.error, 
+            savingContext: this.props.savingContext, 
             mozLoop: this.props.mozLoop, 
             onEditModeChange: this.handleEditModeChange, 
             roomData: this.props.roomData, 
             show: this.props.showContext || this.state.editMode})
         )
       );
     }
   });
@@ -285,16 +303,17 @@ loop.roomViews = (function(mozL10n) {
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       editMode: React.PropTypes.bool,
       error: React.PropTypes.object,
       mozLoop: React.PropTypes.object.isRequired,
       onEditModeChange: React.PropTypes.func,
       // This data is supplied by the activeRoomStore.
       roomData: React.PropTypes.object.isRequired,
+      savingContext: React.PropTypes.bool.isRequired,
       show: React.PropTypes.bool.isRequired
     },
 
     componentWillReceiveProps: function(nextProps) {
       var newState = {};
       // 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) {
@@ -374,16 +393,28 @@ loop.roomViews = (function(mozL10n) {
         if (this.props.onEditModeChange) {
           this.props.onEditModeChange(false);
         }
         return;
       }
       this.setState({ show: false });
     },
 
+    handleContextClick: function(event) {
+      event.stopPropagation();
+      event.preventDefault();
+
+      var url = this._getURL();
+      if (!url || !url.location) {
+        return;
+      }
+
+      this.props.mozLoop.openURL(url.location);
+    },
+
     handleEditClick: function(event) {
       event.preventDefault();
 
       this.setState({ editMode: true });
       if (this.props.onEditModeChange) {
         this.props.onEditModeChange(true);
       }
     },
@@ -392,23 +423,23 @@ loop.roomViews = (function(mozL10n) {
       if (state.checked) {
         // The checkbox was checked, prefill the fields with the values available
         // in `availableContext`.
         var context = this.state.availableContext;
         this.setState({
           newRoomURL: context.url,
           newRoomDescription: context.description,
           newRoomThumbnail: context.previewImage
-        }, this.handleFormSubmit);
+        });
       } else {
         this.setState({
           newRoomURL: "",
           newRoomDescription: "",
           newRoomThumbnail: ""
-        }, this.handleFormSubmit);
+        });
       }
     },
 
     handleFormSubmit: function(event) {
       event && event.preventDefault();
 
       this.props.dispatcher.dispatch(new sharedActions.UpdateRoomContext({
         roomToken: this.props.roomData.roomToken,
@@ -464,96 +495,100 @@ loop.roomViews = (function(mozL10n) {
       if (!this.state.show && !this.state.editMode) {
         return null;
       }
 
       var url = this._getURL();
       var thumbnail = url && url.thumbnail || "";
       var urlDescription = url && url.description || "";
       var location = url && url.location || "";
-      var checkboxLabel = null;
       var locationData = null;
       if (location) {
         locationData = checkboxLabel = sharedUtils.formatURL(location);
       }
       if (!checkboxLabel) {
         try {
           checkboxLabel = sharedUtils.formatURL((this.state.availableContext ?
             this.state.availableContext.url : ""));
         } catch (ex) {}
       }
 
       var cx = React.addons.classSet;
       if (this.state.editMode) {
+        var availableContext = this.state.availableContext;
+        // The checkbox shows as checked when there's already context data
+        // attached to this room.
+        var checked = !!urlDescription;
+        var checkboxLabel = urlDescription || (availableContext && availableContext.url ?
+          availableContext.description : "");
+
         return (
-          React.createElement("div", {className: "room-context"}, 
-            React.createElement("div", {className: "room-context-content"}, 
-              React.createElement("p", {className: cx({"error": !!this.props.error,
-                                "error-display-area": true})}, 
-                mozL10n.get("rooms_change_failed_label")
-              ), 
-              React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")), 
-              React.createElement(sharedViews.Checkbox, {
-                checked: !!url, 
-                disabled: !!url || !checkboxLabel, 
-                label: mozL10n.get("context_edit_activate_label", {
-                  title: checkboxLabel ? checkboxLabel.hostname : ""
-                }), 
-                onChange: this.handleCheckboxChange, 
-                value: location}), 
-              React.createElement("form", {onSubmit: this.handleFormSubmit}, 
-                React.createElement("textarea", {rows: "2", type: "text", className: "room-context-name", 
-                  onBlur: this.handleFormSubmit, 
-                  onKeyDown: this.handleTextareaKeyDown, 
-                  placeholder: mozL10n.get("context_edit_name_placeholder"), 
-                  valueLink: this.linkState("newRoomName")}), 
-                React.createElement("input", {type: "text", className: "room-context-url", 
-                  onBlur: this.handleFormSubmit, 
-                  onKeyDown: this.handleTextareaKeyDown, 
-                  placeholder: "https://", 
-                  valueLink: this.linkState("newRoomURL")}), 
-                React.createElement("textarea", {rows: "4", type: "text", className: "room-context-comments", 
-                  onBlur: this.handleFormSubmit, 
-                  onKeyDown: this.handleTextareaKeyDown, 
-                  placeholder: mozL10n.get("context_edit_comments_placeholder"), 
-                  valueLink: this.linkState("newRoomDescription")})
-              ), 
-              React.createElement("button", {className: "room-context-btn-close", 
-                      onClick: this.handleCloseClick, 
-                      title: mozL10n.get("cancel_button")})
-            )
+          React.createElement("div", {className: "room-context editMode"}, 
+            React.createElement("p", {className: cx({"error": !!this.props.error,
+                              "error-display-area": true})}, 
+              mozL10n.get("rooms_change_failed_label")
+            ), 
+            React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")), 
+            React.createElement(sharedViews.Checkbox, {
+              additionalClass: cx({ hide: !checkboxLabel }), 
+              checked: checked, 
+              disabled: checked, 
+              label: checkboxLabel, 
+              onChange: this.handleCheckboxChange, 
+              value: location}), 
+            React.createElement("form", {onSubmit: this.handleFormSubmit}, 
+              React.createElement("input", {type: "text", className: "room-context-name", 
+                onKeyDown: this.handleTextareaKeyDown, 
+                placeholder: mozL10n.get("context_edit_name_placeholder"), 
+                valueLink: this.linkState("newRoomName")}), 
+              React.createElement("input", {type: "text", className: "room-context-url", 
+                onKeyDown: this.handleTextareaKeyDown, 
+                placeholder: "https://", 
+                disabled: availableContext && availableContext.url === this.state.newRoomURL, 
+                valueLink: this.linkState("newRoomURL")}), 
+              React.createElement("textarea", {rows: "3", type: "text", className: "room-context-comments", 
+                onKeyDown: this.handleTextareaKeyDown, 
+                placeholder: mozL10n.get("context_edit_comments_placeholder"), 
+                valueLink: this.linkState("newRoomDescription")})
+            ), 
+            React.createElement("button", {className: "btn btn-info", 
+                    disabled: this.props.savingContext, 
+                    onClick: this.handleFormSubmit}, 
+              mozL10n.get("context_save_label")
+            ), 
+            React.createElement("button", {className: "room-context-btn-close", 
+                    onClick: this.handleCloseClick, 
+                    title: mozL10n.get("cancel_button")})
           )
         );
       }
 
       if (!locationData) {
         return null;
       }
 
       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-label"}, mozL10n.get("context_inroom_label")), 
+          React.createElement("div", {className: "room-context-content", 
+               onClick: this.handleContextClick}, 
+            React.createElement("img", {className: "room-context-thumbnail", src: thumbnail}), 
             React.createElement("div", {className: "room-context-description", 
-                 title: urlDescription}, this._truncate(urlDescription)), 
-            React.createElement("a", {className: "room-context-url", 
-               href: location, 
-               target: "_blank", 
-               title: locationData.location}, locationData.hostname), 
-            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, 
-                    title: mozL10n.get("context_hide_tooltip")}), 
-            React.createElement("button", {className: "room-context-btn-edit", 
-                    onClick: this.handleEditClick, 
-                    title: mozL10n.get("context_edit_tooltip")})
-          )
+                 title: urlDescription}, 
+              this._truncate(urlDescription), 
+              React.createElement("a", {className: "room-context-url", 
+                 title: locationData.location}, locationData.hostname)
+            )
+          ), 
+          React.createElement("button", {className: "room-context-btn-close", 
+                  onClick: this.handleCloseClick, 
+                  title: mozL10n.get("context_hide_tooltip")}), 
+          React.createElement("button", {className: "room-context-btn-edit", 
+                  onClick: this.handleEditClick, 
+                  title: mozL10n.get("context_edit_tooltip")})
         )
       );
     }
   });
 
   /**
    * Desktop room conversation view.
    */
@@ -666,16 +701,17 @@ loop.roomViews = (function(mozL10n) {
           return (
             React.createElement("div", {className: "room-conversation-wrapper"}, 
               React.createElement(sharedViews.TextChatView, {dispatcher: this.props.dispatcher}), 
               React.createElement(DesktopRoomInvitationView, {
                 dispatcher: this.props.dispatcher, 
                 error: this.state.error, 
                 mozLoop: this.props.mozLoop, 
                 roomData: roomData, 
+                savingContext: this.state.savingContext, 
                 show: shouldRenderInvitationOverlay, 
                 showContext: shouldRenderContextView, 
                 socialShareButtonAvailable: this.state.socialShareButtonAvailable, 
                 socialShareProviders: this.state.socialShareProviders}), 
               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"}, 
@@ -691,16 +727,17 @@ loop.roomViews = (function(mozL10n) {
                     publishStream: this.publishStream, 
                     hangup: this.leaveRoom, 
                     screenShare: screenShareData})
                 )
               ), 
               React.createElement(DesktopRoomContextView, {
                 dispatcher: this.props.dispatcher, 
                 error: this.state.error, 
+                savingContext: this.state.savingContext, 
                 mozLoop: this.props.mozLoop, 
                 roomData: roomData, 
                 show: !shouldRenderInvitationOverlay && shouldRenderContextView})
             )
           );
         }
       }
     }
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -24,16 +24,18 @@ loop.roomViews = (function(mozL10n) {
       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);
+      this.listenTo(this.props.roomStore, "change:savingContext",
+                    this._onRoomSavingContext);
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.props.roomStore);
     },
 
     _onActiveRoomStateChanged: function() {
       // Only update the state if we're mounted, to avoid the problem where
@@ -48,21 +50,31 @@ loop.roomViews = (function(mozL10n) {
       // 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")});
       }
     },
 
+    _onRoomSavingContext: 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({savingContext: this.props.roomStore.getStoreState("savingContext")});
+      }
+    },
+
     getInitialState: function() {
       var storeState = this.props.roomStore.getStoreState("activeRoom");
       return _.extend({
         // Used by the UI showcase.
-        roomState: this.props.roomState || storeState.roomState
+        roomState: this.props.roomState || storeState.roomState,
+        savingContext: false
       }, storeState);
     }
   };
 
   var SocialShareDropdown = React.createClass({
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       roomUrl: React.PropTypes.string,
@@ -169,16 +181,17 @@ loop.roomViews = (function(mozL10n) {
     mixins: [sharedMixins.DropdownMenuMixin(".room-invitation-overlay")],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       error: React.PropTypes.object,
       mozLoop: React.PropTypes.object.isRequired,
       // This data is supplied by the activeRoomStore.
       roomData: React.PropTypes.object.isRequired,
+      savingContext: React.PropTypes.bool,
       show: React.PropTypes.bool.isRequired,
       showContext: React.PropTypes.bool.isRequired
     },
 
     getInitialState: function() {
       return {
         copiedUrl: false,
         editMode: false,
@@ -238,17 +251,21 @@ loop.roomViews = (function(mozL10n) {
             <p className={cx({hide: this.state.editMode})}>
               {mozL10n.get("invite_header_text")}
             </p>
             <a className={cx({hide: !canAddContext, "room-invitation-addcontext": true})}
                onClick={this.handleAddContextClick}>
               {mozL10n.get("context_add_some_label")}
             </a>
           </div>
-          <div className="btn-group call-action-group">
+          <div className={cx({
+            "btn-group": true,
+            "call-action-group": true,
+            hide: this.state.editMode
+          })}>
             <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")}
@@ -265,16 +282,17 @@ loop.roomViews = (function(mozL10n) {
             show={this.state.showMenu}
             socialShareButtonAvailable={this.props.socialShareButtonAvailable}
             socialShareProviders={this.props.socialShareProviders}
             ref="menu" />
           <DesktopRoomContextView
             dispatcher={this.props.dispatcher}
             editMode={this.state.editMode}
             error={this.props.error}
+            savingContext={this.props.savingContext}
             mozLoop={this.props.mozLoop}
             onEditModeChange={this.handleEditModeChange}
             roomData={this.props.roomData}
             show={this.props.showContext || this.state.editMode} />
         </div>
       );
     }
   });
@@ -285,16 +303,17 @@ loop.roomViews = (function(mozL10n) {
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       editMode: React.PropTypes.bool,
       error: React.PropTypes.object,
       mozLoop: React.PropTypes.object.isRequired,
       onEditModeChange: React.PropTypes.func,
       // This data is supplied by the activeRoomStore.
       roomData: React.PropTypes.object.isRequired,
+      savingContext: React.PropTypes.bool.isRequired,
       show: React.PropTypes.bool.isRequired
     },
 
     componentWillReceiveProps: function(nextProps) {
       var newState = {};
       // 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) {
@@ -374,16 +393,28 @@ loop.roomViews = (function(mozL10n) {
         if (this.props.onEditModeChange) {
           this.props.onEditModeChange(false);
         }
         return;
       }
       this.setState({ show: false });
     },
 
+    handleContextClick: function(event) {
+      event.stopPropagation();
+      event.preventDefault();
+
+      var url = this._getURL();
+      if (!url || !url.location) {
+        return;
+      }
+
+      this.props.mozLoop.openURL(url.location);
+    },
+
     handleEditClick: function(event) {
       event.preventDefault();
 
       this.setState({ editMode: true });
       if (this.props.onEditModeChange) {
         this.props.onEditModeChange(true);
       }
     },
@@ -392,23 +423,23 @@ loop.roomViews = (function(mozL10n) {
       if (state.checked) {
         // The checkbox was checked, prefill the fields with the values available
         // in `availableContext`.
         var context = this.state.availableContext;
         this.setState({
           newRoomURL: context.url,
           newRoomDescription: context.description,
           newRoomThumbnail: context.previewImage
-        }, this.handleFormSubmit);
+        });
       } else {
         this.setState({
           newRoomURL: "",
           newRoomDescription: "",
           newRoomThumbnail: ""
-        }, this.handleFormSubmit);
+        });
       }
     },
 
     handleFormSubmit: function(event) {
       event && event.preventDefault();
 
       this.props.dispatcher.dispatch(new sharedActions.UpdateRoomContext({
         roomToken: this.props.roomData.roomToken,
@@ -464,96 +495,100 @@ loop.roomViews = (function(mozL10n) {
       if (!this.state.show && !this.state.editMode) {
         return null;
       }
 
       var url = this._getURL();
       var thumbnail = url && url.thumbnail || "";
       var urlDescription = url && url.description || "";
       var location = url && url.location || "";
-      var checkboxLabel = null;
       var locationData = null;
       if (location) {
         locationData = checkboxLabel = sharedUtils.formatURL(location);
       }
       if (!checkboxLabel) {
         try {
           checkboxLabel = sharedUtils.formatURL((this.state.availableContext ?
             this.state.availableContext.url : ""));
         } catch (ex) {}
       }
 
       var cx = React.addons.classSet;
       if (this.state.editMode) {
+        var availableContext = this.state.availableContext;
+        // The checkbox shows as checked when there's already context data
+        // attached to this room.
+        var checked = !!urlDescription;
+        var checkboxLabel = urlDescription || (availableContext && availableContext.url ?
+          availableContext.description : "");
+
         return (
-          <div className="room-context">
-            <div className="room-context-content">
-              <p className={cx({"error": !!this.props.error,
-                                "error-display-area": true})}>
-                {mozL10n.get("rooms_change_failed_label")}
-              </p>
-              <div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
-              <sharedViews.Checkbox
-                checked={!!url}
-                disabled={!!url || !checkboxLabel}
-                label={mozL10n.get("context_edit_activate_label", {
-                  title: checkboxLabel ? checkboxLabel.hostname : ""
-                })}
-                onChange={this.handleCheckboxChange}
-                value={location} />
-              <form onSubmit={this.handleFormSubmit}>
-                <textarea rows="2" type="text" className="room-context-name"
-                  onBlur={this.handleFormSubmit}
-                  onKeyDown={this.handleTextareaKeyDown}
-                  placeholder={mozL10n.get("context_edit_name_placeholder")}
-                  valueLink={this.linkState("newRoomName")} />
-                <input type="text" className="room-context-url"
-                  onBlur={this.handleFormSubmit}
-                  onKeyDown={this.handleTextareaKeyDown}
-                  placeholder="https://"
-                  valueLink={this.linkState("newRoomURL")} />
-                <textarea rows="4" type="text" className="room-context-comments"
-                  onBlur={this.handleFormSubmit}
-                  onKeyDown={this.handleTextareaKeyDown}
-                  placeholder={mozL10n.get("context_edit_comments_placeholder")}
-                  valueLink={this.linkState("newRoomDescription")} />
-              </form>
-              <button className="room-context-btn-close"
-                      onClick={this.handleCloseClick}
-                      title={mozL10n.get("cancel_button")}/>
-            </div>
+          <div className="room-context editMode">
+            <p className={cx({"error": !!this.props.error,
+                              "error-display-area": true})}>
+              {mozL10n.get("rooms_change_failed_label")}
+            </p>
+            <div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
+            <sharedViews.Checkbox
+              additionalClass={cx({ hide: !checkboxLabel })}
+              checked={checked}
+              disabled={checked}
+              label={checkboxLabel}
+              onChange={this.handleCheckboxChange}
+              value={location} />
+            <form onSubmit={this.handleFormSubmit}>
+              <input type="text" className="room-context-name"
+                onKeyDown={this.handleTextareaKeyDown}
+                placeholder={mozL10n.get("context_edit_name_placeholder")}
+                valueLink={this.linkState("newRoomName")} />
+              <input type="text" className="room-context-url"
+                onKeyDown={this.handleTextareaKeyDown}
+                placeholder="https://"
+                disabled={availableContext && availableContext.url === this.state.newRoomURL}
+                valueLink={this.linkState("newRoomURL")} />
+              <textarea rows="3" type="text" className="room-context-comments"
+                onKeyDown={this.handleTextareaKeyDown}
+                placeholder={mozL10n.get("context_edit_comments_placeholder")}
+                valueLink={this.linkState("newRoomDescription")} />
+            </form>
+            <button className="btn btn-info"
+                    disabled={this.props.savingContext}
+                    onClick={this.handleFormSubmit}>
+              {mozL10n.get("context_save_label")}
+            </button>
+            <button className="room-context-btn-close"
+                    onClick={this.handleCloseClick}
+                    title={mozL10n.get("cancel_button")}/>
           </div>
         );
       }
 
       if (!locationData) {
         return null;
       }
 
       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-label">{mozL10n.get("context_inroom_label")}</div>
+          <div className="room-context-content"
+               onClick={this.handleContextClick}>
+            <img className="room-context-thumbnail" src={thumbnail}/>
             <div className="room-context-description"
-                 title={urlDescription}>{this._truncate(urlDescription)}</div>
-            <a className="room-context-url"
-               href={location}
-               target="_blank"
-               title={locationData.location}>{locationData.hostname}</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}
-                    title={mozL10n.get("context_hide_tooltip")}/>
-            <button className="room-context-btn-edit"
-                    onClick={this.handleEditClick}
-                    title={mozL10n.get("context_edit_tooltip")}/>
+                 title={urlDescription}>
+              {this._truncate(urlDescription)}
+              <a className="room-context-url"
+                 title={locationData.location}>{locationData.hostname}</a>
+            </div>
           </div>
+          <button className="room-context-btn-close"
+                  onClick={this.handleCloseClick}
+                  title={mozL10n.get("context_hide_tooltip")}/>
+          <button className="room-context-btn-edit"
+                  onClick={this.handleEditClick}
+                  title={mozL10n.get("context_edit_tooltip")}/>
         </div>
       );
     }
   });
 
   /**
    * Desktop room conversation view.
    */
@@ -666,16 +701,17 @@ loop.roomViews = (function(mozL10n) {
           return (
             <div className="room-conversation-wrapper">
               <sharedViews.TextChatView dispatcher={this.props.dispatcher} />
               <DesktopRoomInvitationView
                 dispatcher={this.props.dispatcher}
                 error={this.state.error}
                 mozLoop={this.props.mozLoop}
                 roomData={roomData}
+                savingContext={this.state.savingContext}
                 show={shouldRenderInvitationOverlay}
                 showContext={shouldRenderContextView}
                 socialShareButtonAvailable={this.state.socialShareButtonAvailable}
                 socialShareProviders={this.state.socialShareProviders} />
               <div className="video-layout-wrapper">
                 <div className="conversation room-conversation">
                   <div className="media nested">
                     <div className="video_wrapper remote_wrapper">
@@ -691,16 +727,17 @@ loop.roomViews = (function(mozL10n) {
                     publishStream={this.publishStream}
                     hangup={this.leaveRoom}
                     screenShare={screenShareData} />
                 </div>
               </div>
               <DesktopRoomContextView
                 dispatcher={this.props.dispatcher}
                 error={this.state.error}
+                savingContext={this.state.savingContext}
                 mozLoop={this.props.mozLoop}
                 roomData={roomData}
                 show={!shouldRenderInvitationOverlay && shouldRenderContextView} />
             </div>
           );
         }
       }
     }
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -308,17 +308,18 @@
 }
 
 .call-action-group {
   display: flex;
   padding: 2.5em 4px 0 4px;
   width: 100%;
 }
 
-.call-action-group > .btn {
+.call-action-group > .btn,
+.room-context > .btn {
   min-height: 26px;
   border-radius: 2px;
   margin: 0 4px;
   min-width: 64px;
 }
 
 .call-action-group .btn-group-chevron,
 .call-action-group .btn-group {
@@ -1000,157 +1001,198 @@ body[dir=rtl] .share-service-dropdown .s
   padding: .5rem;
   max-height: 400px;
   position: absolute;
   left: 0;
   bottom: 0;
   width: 100%;
   font-size: .9em;
   display: flex;
-  flex-flow: row nowrap;
+  flex-flow: column nowrap;
   align-content: flex-start;
   align-items: flex-start;
   overflow-x: hidden;
   overflow-y: auto;
+  /* Make the context view float atop the video elements. */
+  z-index: 2;
+}
+
+.room-context.editMode {
+  /* Stretch to the maximum available space whilst not covering the conversation
+     toolbar (26px). */
+  height: calc(100% - 26px);
 }
 
 .room-invitation-overlay .room-context {
   position: relative;
   left: auto;
   bottom: auto;
   flex: 0 1 auto;
 }
 
+.room-invitation-overlay .room-context.editMode {
+  height: 100%;
+}
+
+.room-context-content {
+  flex: 1 1 auto;
+  text-align: start;
+  display: flex;
+  flex-flow: row nowrap;
+  font-size: .9em;
+}
+
 .room-context-thumbnail {
-  width: 16px;
+  /* 16px icon size + 3px border width. */
+  width: 19px;
+  max-height: 19px;
+  border: 3px solid #fff;
+  border-radius: 3px;
+  background-color: #fff;
   -moz-margin-end: 1ch;
-  margin-bottom: 1em;
-  order: 1;
   flex: 0 1 auto;
 }
 
 .room-context-thumbnail[src=""] {
   display: none;
 }
 
-.room-context-content {
-  order: 2;
-  flex: 1 1 auto;
-  text-align: start;
-}
-
-.room-context-content > .error-display-area.error {
+.room-context > .error-display-area.error {
   display: block;
   background-color: rgba(215,67,69,.8);
   border-radius: 3px;
   padding: .5em;
 }
 
-.room-context-content > .error-display-area {
+.room-context > .error-display-area {
   display: none;
 }
 
-.room-context-content > .error-display-area.error {
+.room-context > .error-display-area.error {
   margin: 1em 0 .5em 0;
   text-align: center;
   text-shadow: 1px 1px 0 rgba(0,0,0,.3);
 }
 
-.room-context-content > .checkbox-wrapper {
+.room-context > .checkbox-wrapper {
   margin-bottom: .5em;
 }
 
 .room-context-label {
   margin-bottom: 1em;
 }
 
-.room-context-label,
 .room-context-description,
-.room-context-content > .checkbox-wrapper > label {
+.room-context > .checkbox-wrapper > label {
   color: #fff;
 }
 
 .room-context-comment {
   color: #707070;
 }
 
 .room-context-description,
 .room-context-comment {
   word-wrap: break-word;
 }
 
-.room-context-url {
-  color: #59A1D7;
+:not(input).room-context-url {
+  color: #0095dd;
   font-style: italic;
   text-decoration: none;
-  margin-bottom: 1em;
+  display: block;
+  cursor: pointer;
 }
 
 .room-context-url:hover {
   text-decoration: underline;
 }
 
-.room-context-content > form > textarea,
-.room-context-content > form > input[type="text"] {
+.room-context > form {
+  width: 100%;
+}
+
+.room-context > form > textarea,
+.room-context > form > input[type="text"] {
   display: block;
   background: rgba(0,0,0,.5);
-  color: #fff;
   font-family: "Helvetica Neue", Arial, sans;
-  font-size: 1em;
-  border: 1px solid #999;
+  border: 1px solid rgba(255,255,255,.2);
   width: 100%;
-  padding: .2em .4em;
+  padding: .5em;
   border-radius: 3px;
   resize: none;
+  color: #fff;
+}
+
+.room-context > form > textarea {
+  font-size: 1em;
+}
+
+.room-context > form > input:not([disabled]).room-context-url {
+  color: #0095dd;
 }
 
-.room-context-content > form > textarea:not(:last-of-type),
-.room-context-content > form > input[type="text"] {
+.room-context > form > input[disabled] {
+  background-color: rgba(255,255,255,.2);
+  color: rgba(255,255,255,.4);
+}
+
+.room-context > form > textarea:not(:last-of-type),
+.room-context > form > input[type="text"] {
   margin: 0 0 .5em 0;
 }
 
+.room-context > .btn {
+  margin: .5em 0 0;
+  font-size: 1.1em;
+  padding: 0 .5em;
+  align-self: flex-end;
+}
+
 .room-context-btn-close,
 .room-context-btn-edit {
   position: absolute;
-  right: 5px;
-  top: 5px;
+  right: 8px;
+  /* 8px offset + 2px border-top */
+  top: 10px;
   width: 8px;
   height: 8px;
   background-color: transparent;
-  background-image: url("../img/icons-10x10.svg#close");
+  background-image: url("../img/icons-10x10.svg#close-darkergrey");
   background-size: 8px 8px;
   background-repeat: no-repeat;
   border: 0;
   padding: 0;
   cursor: pointer;
 }
 
 .room-context-btn-edit {
-  right: 18px;
-  background-image: url("../img/icons-10x10.svg#edit");
+  right: 20px;
+  background-image: url("../img/icons-10x10.svg#edit-darkergrey");
 }
 
 .room-context-btn-edit:hover,
 .room-context-btn-edit:hover:active {
   background-image: url("../img/icons-10x10.svg#edit-active");
 }
 
 .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,
 body[dir=rtl] .room-context-btn-edit {
   right: auto;
-  left: 5px;
+  left: 8px;
 }
 
 body[dir=rtl] .room-context-btn-edit {
-  left: 18px;
+  left: 20px;
 }
 
 /* Standalone rooms */
 
 .standalone .room-conversation-wrapper {
   position: relative;
   height: 100%;
   background: #000;
--- a/browser/components/loop/content/shared/img/icons-10x10.svg
+++ b/browser/components/loop/content/shared/img/icons-10x10.svg
@@ -17,33 +17,38 @@
       fill: #0095dd;
     }
     use[id$="-white"] {
       fill: rgba(255,255,255,0.8);
     }
     use[id$="-disabled"] {
       fill: rgba(255,255,255,0.4);
     }
+    use[id$="-darkergrey"] {
+      fill: #999;
+    }
   </style>
   <defs>
     <polygon id="close-shape" points="10,1.717 8.336,0.049 5.024,3.369 1.663,0 0,1.668 3.36,5.037 0.098,8.307 1.762,9.975 5.025,6.705 8.311,10 9.975,8.332 6.688,5.037"/>
     <path id="dropdown-shape" fill-rule="evenodd" d="M9,3L4.984,7L1,3H9z"/>
     <polygon id="expand-shape" points="10,0 4.838,0 6.506,1.669 0,8.175 1.825,10 8.331,3.494 10,5.162"/>
     <path id="edit-shape" d="M5.493,1.762l2.745,2.745L2.745,10H0V7.255L5.493,1.762z M2.397,9.155l0.601-0.601L1.446,7.002L0.845,7.603 V8.31H1.69v0.845H2.397z M5.849,3.028c0-0.096-0.048-0.144-0.146-0.144c-0.044,0-0.081,0.015-0.112,0.046L2.014,6.508 C1.983,6.538,1.968,6.577,1.968,6.619c0,0.098,0.048,0.146,0.144,0.146c0.044,0,0.081-0.015,0.112-0.046l3.579-3.577 C5.834,3.111,5.849,3.073,5.849,3.028z M10,2.395c0,0.233-0.081,0.431-0.245,0.595L8.66,4.085L5.915,1.34L7.01,0.25 C7.168,0.083,7.366,0,7.605,0c0.233,0,0.433,0.083,0.601,0.25l1.55,1.544C9.919,1.966,10,2.166,10,2.395z"/>
     <rect id="minimize-shape" y="3.6" width="10" height="2.8"/>
   </defs>
   <use id="close" xlink:href="#close-shape"/>
   <use id="close-active" xlink:href="#close-shape"/>
   <use id="close-disabled" xlink:href="#close-shape"/>
+  <use id="close-darkergrey" xlink:href="#close-shape"/>
   <use id="dropdown" xlink:href="#dropdown-shape"/>
   <use id="dropdown-white" xlink:href="#dropdown-shape"/>
   <use id="dropdown-active" xlink:href="#dropdown-shape"/>
   <use id="dropdown-disabled" xlink:href="#dropdown-shape"/>
   <use id="edit" xlink:href="#edit-shape"/>
   <use id="edit-active" xlink:href="#edit-shape"/>
   <use id="edit-disabled" xlink:href="#edit-shape"/>
+  <use id="edit-darkergrey" xlink:href="#edit-shape"/>
   <use id="expand" xlink:href="#expand-shape"/>
   <use id="expand-active" xlink:href="#expand-shape"/>
   <use id="expand-disabled" xlink:href="#expand-shape"/>
   <use id="minimize" xlink:href="#minimize-shape"/>
   <use id="minimize-active" xlink:href="#minimize-shape"/>
   <use id="minimize-disabled" xlink:href="#minimize-shape"/>
 </svg>
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -364,16 +364,22 @@ loop.shared.actions = (function() {
     /**
      * Updating the context data attached to a room error.
      */
     UpdateRoomContextError: Action.define("updateRoomContextError", {
       error: [Error, Object]
     }),
 
     /**
+     * Updating the context data attached to a room finished successfully.
+     */
+    UpdateRoomContextDone: Action.define("updateRoomContextDone", {
+    }),
+
+    /**
      * Copy a room url into the user's clipboard.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     CopyRoomUrl: Action.define("copyRoomUrl", {
       roomUrl: String
     }),
 
     /**
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -658,17 +658,17 @@ loop.shared.views = (function(_, l10n) {
         disabled: this.props.disabled
       };
       var checkClasses = {
         checkbox: true,
         checked: this.state.checked,
         disabled: this.props.disabled
       };
       if (this.props.additionalClass) {
-        checkClasses[this.props.additionalClass] = true;
+        wrapperClasses[this.props.additionalClass] = true;
       }
       return (
         React.createElement("div", {className: cx(wrapperClasses), 
              disabled: this.props.disabled, 
              onClick: this._handleClick}, 
           React.createElement("div", {className: cx(checkClasses)}), 
           this.props.label ?
             React.createElement("label", null, this.props.label) :
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -658,17 +658,17 @@ loop.shared.views = (function(_, l10n) {
         disabled: this.props.disabled
       };
       var checkClasses = {
         checkbox: true,
         checked: this.state.checked,
         disabled: this.props.disabled
       };
       if (this.props.additionalClass) {
-        checkClasses[this.props.additionalClass] = true;
+        wrapperClasses[this.props.additionalClass] = true;
       }
       return (
         <div className={cx(wrapperClasses)}
              disabled={this.props.disabled}
              onClick={this._handleClick}>
           <div className={cx(checkClasses)} />
           {this.props.label ?
             <label>{this.props.label}</label> :
--- a/browser/components/loop/test/desktop-local/roomStore_test.js
+++ b/browser/components/loop/test/desktop-local/roomStore_test.js
@@ -639,39 +639,59 @@ describe("loop.store.RoomStore", functio
 
       sinon.assert.calledOnce(fakeMozLoop.rooms.get);
       sinon.assert.calledOnce(fakeMozLoop.rooms.update);
       sinon.assert.calledWith(fakeMozLoop.rooms.update, "42abc", {
         roomName: "silly name"
       });
     });
 
+    it("should flag the the store as saving context", function() {
+      expect(store.getStoreState().savingContext).to.eql(false);
+
+      sandbox.stub(fakeMozLoop.rooms, "update", function(roomToken, roomData, cb) {
+        expect(store.getStoreState().savingContext).to.eql(true);
+        cb();
+      });
+
+      dispatcher.dispatch(new sharedActions.UpdateRoomContext({
+        roomToken: "42abc",
+        newRoomName: "silly name"
+      }));
+
+      expect(store.getStoreState().savingContext).to.eql(false);
+    });
+
     it("should store any update-encountered error", function() {
       var err = new Error("fake");
       sandbox.stub(fakeMozLoop.rooms, "update", function(roomToken, roomData, cb) {
+        expect(store.getStoreState().savingContext).to.eql(true);
         cb(err);
       });
 
       dispatcher.dispatch(new sharedActions.UpdateRoomContext({
         roomToken: "42abc",
         newRoomName: "silly name"
       }));
 
-      expect(store.getStoreState().error).eql(err);
+      var state = store.getStoreState();
+      expect(state.error).eql(err);
+      expect(state.savingContext).to.eql(false);
     });
 
     it("should ensure only submitting a non-empty room name", function() {
       fakeMozLoop.rooms.update = sinon.spy();
 
       dispatcher.dispatch(new sharedActions.UpdateRoomContext({
         roomToken: "42abc",
         newRoomName: " \t  \t "
       }));
 
       sinon.assert.notCalled(fakeMozLoop.rooms.update);
+      expect(store.getStoreState().savingContext).to.eql(false);
     });
 
     it("should save updated context information", function() {
       fakeMozLoop.rooms.update = sinon.spy();
 
       dispatcher.dispatch(new sharedActions.UpdateRoomContext({
         roomToken: "42abc",
         // Room name doesn't need to change.
@@ -718,11 +738,12 @@ describe("loop.store.RoomStore", functio
           // Room name doesn't need to change.
           newRoomName: "sillier name",
           newRoomDescription: "",
           newRoomThumbnail: "",
           newRoomURL: ""
         }));
 
         sinon.assert.notCalled(fakeMozLoop.rooms.update);
+        expect(store.getStoreState().savingContext).to.eql(false);
       });
   });
 });
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -91,17 +91,17 @@ describe("loop.roomViews", function () {
         render: function() { return React.DOM.div(); }
       });
 
       var testView = TestUtils.renderIntoDocument(
         React.createElement(TestView, {
           roomStore: roomStore
         }));
 
-      var expectedState = _.extend({foo: "bar"},
+      var expectedState = _.extend({foo: "bar", savingContext: false},
         activeRoomStore.getInitialStoreState());
 
       expect(testView.state).eql(expectedState);
     });
 
     it("should listen to store changes", function() {
       var TestView = React.createClass({
         mixins: [loop.roomViews.ActiveRoomStoreMixin],
@@ -671,20 +671,18 @@ describe("loop.roomViews", function () {
             roomContextUrls: [fakeContextURL]
           }
         });
 
         var node = view.getDOMNode();
         expect(node).to.not.eql(null);
         expect(node.querySelector(".room-context-thumbnail").src).to.
           eql(fakeContextURL.thumbnail);
-        expect(node.querySelector(".room-context-description").textContent).to.
-          eql(fakeContextURL.description);
-        expect(node.querySelector(".room-context-comment").textContent).to.
-          eql(view.props.roomData.roomDescription);
+        expect(node.querySelector(".room-context-description").firstChild.textContent).
+          to.eql(fakeContextURL.description);
       });
 
       it("should not render optional data", function() {
         view = mountTestComponent({
           roomData: { roomContextUrls: [fakeContextURL] }
         });
 
         expect(view.getDOMNode().querySelector(".room-context-comment")).to.
@@ -722,22 +720,23 @@ describe("loop.roomViews", function () {
         var node = view.getDOMNode();
         expect(node.querySelector("form")).to.not.eql(null);
         // Check the contents of the form fields.
         expect(node.querySelector(".room-context-name").value).to.eql(roomName);
         expect(node.querySelector(".room-context-url").value).to.eql(fakeContextURL.location);
         expect(node.querySelector(".room-context-comments").value).to.eql(fakeContextURL.description);
       });
 
-      it("should show the checkbox as disabled when no context is available", function() {
+      it("should show the checkbox as disabled when context is already set", function() {
         view = mountTestComponent({
           editMode: true,
           roomData: {
             roomToken: "fakeToken",
-            roomName: "fakeName"
+            roomName: "fakeName",
+            roomContextUrls: [fakeContextURL]
           }
         });
 
         var checkbox = view.getDOMNode().querySelector(".checkbox");
         expect(checkbox.classList.contains("disabled")).to.eql(true);
       });
 
       it("should render the editMode view when the edit button is clicked", function(next) {
@@ -753,23 +752,46 @@ describe("loop.roomViews", function () {
         // Switch to editMode via setting the prop, since we can control that
         // better.
         view.setProps({ editMode: true }, function() {
           // First check if availableContext is set correctly.
           expect(view.state.availableContext).to.not.eql(null);
           expect(view.state.availableContext.previewImage).to.eql(favicon);
 
           var node = view.getDOMNode();
+          expect(node.querySelector(".checkbox-wrapper").classList.contains("disabled")).to.eql(true);
           expect(node.querySelector(".room-context-name").value).to.eql(roomName);
           expect(node.querySelector(".room-context-url").value).to.eql(fakeContextURL.location);
           expect(node.querySelector(".room-context-comments").value).to.eql(fakeContextURL.description);
 
           next();
         });
       });
+
+      it("should hide the checkbox when no context data is stored or available", function(next) {
+        view = mountTestComponent({
+          roomData: {
+            roomToken: "fakeToken",
+            roomName: "Hello, is it me you're looking for?"
+          }
+        });
+
+        // Switch to editMode via setting the prop, since we can control that
+        // better.
+        view.setProps({ editMode: true }, function() {
+          // First check if availableContext is set correctly.
+          expect(view.state.availableContext).to.not.eql(null);
+          expect(view.state.availableContext.previewImage).to.eql(favicon);
+
+          var node = view.getDOMNode();
+          expect(node.querySelector(".checkbox-wrapper").classList.contains("hide")).to.eql(true);
+
+          next();
+        });
+      });
     });
 
     describe("Update Room", function() {
       var roomNameBox;
 
       beforeEach(function() {
         sandbox.stub(dispatcher, "dispatch");
 
@@ -780,23 +802,23 @@ describe("loop.roomViews", function () {
             roomName: "fakeName",
             roomContextUrls: [fakeContextURL]
           }
         });
 
         roomNameBox = view.getDOMNode().querySelector(".room-context-name");
       });
 
-      it("should dispatch a UpdateRoomContext action when the focus is lost",
+      it("should dispatch a UpdateRoomContext action when the save button is clicked",
         function() {
           React.addons.TestUtils.Simulate.change(roomNameBox, { target: {
             value: "reallyFake"
           }});
 
-          React.addons.TestUtils.Simulate.blur(roomNameBox);
+          React.addons.TestUtils.Simulate.click(view.getDOMNode().querySelector(".btn-info"));
 
           sinon.assert.calledOnce(dispatcher.dispatch);
           sinon.assert.calledWithExactly(dispatcher.dispatch,
             new sharedActions.UpdateRoomContext({
               roomToken: "fakeToken",
               newRoomName: "reallyFake",
               newRoomDescription: fakeContextURL.description,
               newRoomURL: fakeContextURL.location,